walopy 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- walopy/__init__.py +162 -0
- walopy/__main__.py +101 -0
- walopy/_utils.py +67 -0
- walopy/advanced.py +803 -0
- walopy/bottleneck.py +155 -0
- walopy/fitting.py +188 -0
- walopy/inventory.py +407 -0
- walopy/kpi.py +274 -0
- walopy/network.py +227 -0
- walopy/operations.py +328 -0
- walopy/plotting.py +626 -0
- walopy/queuing.py +302 -0
- walopy/solver.py +612 -0
- walopy-0.2.1.dist-info/METADATA +214 -0
- walopy-0.2.1.dist-info/RECORD +18 -0
- walopy-0.2.1.dist-info/WHEEL +5 -0
- walopy-0.2.1.dist-info/licenses/LICENSE +21 -0
- walopy-0.2.1.dist-info/top_level.txt +1 -0
walopy/__init__.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""walopy — Queuing theory, operations analysis and KPI trees for Python."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
# Queuing
|
|
5
|
+
from .queuing import (
|
|
6
|
+
QueueResult,
|
|
7
|
+
littles_law,
|
|
8
|
+
mm1,
|
|
9
|
+
mmc,
|
|
10
|
+
md1,
|
|
11
|
+
kingman,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# Operations
|
|
15
|
+
from .operations import (
|
|
16
|
+
OEEResult,
|
|
17
|
+
UtilizationResult,
|
|
18
|
+
UnitCostResult,
|
|
19
|
+
oee,
|
|
20
|
+
utilization_efficiency,
|
|
21
|
+
unit_cost,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Bottleneck
|
|
25
|
+
from .bottleneck import (
|
|
26
|
+
BottleneckResult,
|
|
27
|
+
StationResult,
|
|
28
|
+
bottleneck_analysis,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# KPI trees
|
|
32
|
+
from .kpi import (
|
|
33
|
+
KPINode,
|
|
34
|
+
oee_kpi_tree,
|
|
35
|
+
throughput_kpi_tree,
|
|
36
|
+
roi_kpi_tree,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Solvers
|
|
40
|
+
from .solver import (
|
|
41
|
+
SolverResult,
|
|
42
|
+
OptimizeResult,
|
|
43
|
+
solve_lam,
|
|
44
|
+
solve_mu,
|
|
45
|
+
solve_servers,
|
|
46
|
+
optimize_servers,
|
|
47
|
+
sensitivity,
|
|
48
|
+
batch_model,
|
|
49
|
+
compare,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Advanced models
|
|
53
|
+
from .advanced import (
|
|
54
|
+
SimulationResult,
|
|
55
|
+
LineBalanceResult,
|
|
56
|
+
BreakEvenResult,
|
|
57
|
+
PriorityQueueResult,
|
|
58
|
+
erlang_b,
|
|
59
|
+
mm1k,
|
|
60
|
+
mmck,
|
|
61
|
+
mm1_priority,
|
|
62
|
+
monte_carlo_gg1,
|
|
63
|
+
takt_time,
|
|
64
|
+
line_balance,
|
|
65
|
+
break_even,
|
|
66
|
+
queue_length_pmf,
|
|
67
|
+
sojourn_cdf,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Fitting
|
|
71
|
+
from .fitting import (
|
|
72
|
+
FitResult,
|
|
73
|
+
fit_from_data,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Inventory
|
|
77
|
+
from .inventory import (
|
|
78
|
+
EOQResult,
|
|
79
|
+
ReorderResult,
|
|
80
|
+
NewsvendorResult,
|
|
81
|
+
eoq,
|
|
82
|
+
reorder_point,
|
|
83
|
+
newsvendor,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Network
|
|
87
|
+
from .network import (
|
|
88
|
+
StationMetrics,
|
|
89
|
+
JacksonResult,
|
|
90
|
+
jackson_network,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
94
|
+
try:
|
|
95
|
+
__version__: str = version("walopy")
|
|
96
|
+
except PackageNotFoundError:
|
|
97
|
+
__version__ = "0.2.0" # fallback when running from source without install
|
|
98
|
+
|
|
99
|
+
__all__ = [
|
|
100
|
+
# queuing
|
|
101
|
+
"QueueResult",
|
|
102
|
+
"littles_law",
|
|
103
|
+
"mm1",
|
|
104
|
+
"mmc",
|
|
105
|
+
"md1",
|
|
106
|
+
"kingman",
|
|
107
|
+
# operations
|
|
108
|
+
"OEEResult",
|
|
109
|
+
"UtilizationResult",
|
|
110
|
+
"UnitCostResult",
|
|
111
|
+
"oee",
|
|
112
|
+
"utilization_efficiency",
|
|
113
|
+
"unit_cost",
|
|
114
|
+
# bottleneck
|
|
115
|
+
"BottleneckResult",
|
|
116
|
+
"StationResult",
|
|
117
|
+
"bottleneck_analysis",
|
|
118
|
+
# kpi
|
|
119
|
+
"KPINode",
|
|
120
|
+
"oee_kpi_tree",
|
|
121
|
+
"throughput_kpi_tree",
|
|
122
|
+
"roi_kpi_tree",
|
|
123
|
+
# solver
|
|
124
|
+
"SolverResult",
|
|
125
|
+
"OptimizeResult",
|
|
126
|
+
"solve_lam",
|
|
127
|
+
"solve_mu",
|
|
128
|
+
"solve_servers",
|
|
129
|
+
"optimize_servers",
|
|
130
|
+
"sensitivity",
|
|
131
|
+
"batch_model",
|
|
132
|
+
"compare",
|
|
133
|
+
# advanced
|
|
134
|
+
"SimulationResult",
|
|
135
|
+
"LineBalanceResult",
|
|
136
|
+
"BreakEvenResult",
|
|
137
|
+
"PriorityQueueResult",
|
|
138
|
+
"erlang_b",
|
|
139
|
+
"mm1k",
|
|
140
|
+
"mmck",
|
|
141
|
+
"mm1_priority",
|
|
142
|
+
"monte_carlo_gg1",
|
|
143
|
+
"takt_time",
|
|
144
|
+
"line_balance",
|
|
145
|
+
"break_even",
|
|
146
|
+
"queue_length_pmf",
|
|
147
|
+
"sojourn_cdf",
|
|
148
|
+
# fitting
|
|
149
|
+
"FitResult",
|
|
150
|
+
"fit_from_data",
|
|
151
|
+
# inventory
|
|
152
|
+
"EOQResult",
|
|
153
|
+
"ReorderResult",
|
|
154
|
+
"NewsvendorResult",
|
|
155
|
+
"eoq",
|
|
156
|
+
"reorder_point",
|
|
157
|
+
"newsvendor",
|
|
158
|
+
# network
|
|
159
|
+
"StationMetrics",
|
|
160
|
+
"JacksonResult",
|
|
161
|
+
"jackson_network",
|
|
162
|
+
]
|
walopy/__main__.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""CLI entry point: python -m walopy [--version] <model> [params]"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from . import __version__
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _add_lam_mu(p: argparse.ArgumentParser) -> None:
|
|
11
|
+
p.add_argument("--lam", type=float, required=True, metavar="LAM",
|
|
12
|
+
help="Arrival rate λ")
|
|
13
|
+
p.add_argument("--mu", type=float, required=True, metavar="MU",
|
|
14
|
+
help="Service rate μ per server")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main(argv: list[str] | None = None) -> None:
|
|
18
|
+
parser = argparse.ArgumentParser(
|
|
19
|
+
prog="walopy",
|
|
20
|
+
description="Queuing theory and operations analysis toolkit.",
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument("--version", action="version", version=f"walopy {__version__}")
|
|
23
|
+
|
|
24
|
+
sub = parser.add_subparsers(dest="model", metavar="MODEL")
|
|
25
|
+
|
|
26
|
+
# --- mm1 ---
|
|
27
|
+
p1 = sub.add_parser("mm1", help="M/M/1 single-server queue")
|
|
28
|
+
_add_lam_mu(p1)
|
|
29
|
+
|
|
30
|
+
# --- mmc ---
|
|
31
|
+
pc = sub.add_parser("mmc", help="M/M/c multi-server queue")
|
|
32
|
+
_add_lam_mu(pc)
|
|
33
|
+
pc.add_argument("--c", type=int, required=True, metavar="C", help="Number of servers")
|
|
34
|
+
|
|
35
|
+
# --- md1 ---
|
|
36
|
+
pd1 = sub.add_parser("md1", help="M/D/1 deterministic service queue")
|
|
37
|
+
_add_lam_mu(pd1)
|
|
38
|
+
|
|
39
|
+
# --- gg1 ---
|
|
40
|
+
pg = sub.add_parser("gg1", help="G/G/1 Kingman approximation")
|
|
41
|
+
_add_lam_mu(pg)
|
|
42
|
+
pg.add_argument("--ca2", type=float, required=True, metavar="CA2",
|
|
43
|
+
help="Squared CV of inter-arrival times")
|
|
44
|
+
pg.add_argument("--cs2", type=float, required=True, metavar="CS2",
|
|
45
|
+
help="Squared CV of service times")
|
|
46
|
+
|
|
47
|
+
# --- littles ---
|
|
48
|
+
pl = sub.add_parser("littles", help="Solve Little's Law for the missing variable")
|
|
49
|
+
pl.add_argument("--L", type=float, default=None, metavar="L")
|
|
50
|
+
pl.add_argument("--lam", type=float, default=None, metavar="LAM")
|
|
51
|
+
pl.add_argument("--W", type=float, default=None, metavar="W")
|
|
52
|
+
|
|
53
|
+
# --- eoq ---
|
|
54
|
+
pe = sub.add_parser("eoq", help="Economic Order Quantity")
|
|
55
|
+
pe.add_argument("--demand", type=float, required=True, metavar="D",
|
|
56
|
+
help="Demand rate (units/period)")
|
|
57
|
+
pe.add_argument("--ordering", type=float, required=True, metavar="K",
|
|
58
|
+
help="Fixed cost per order")
|
|
59
|
+
pe.add_argument("--holding", type=float, required=True, metavar="H",
|
|
60
|
+
help="Holding cost per unit per period")
|
|
61
|
+
|
|
62
|
+
args = parser.parse_args(argv)
|
|
63
|
+
|
|
64
|
+
if args.model is None:
|
|
65
|
+
parser.print_help()
|
|
66
|
+
sys.exit(0)
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
if args.model == "mm1":
|
|
70
|
+
from .queuing import mm1
|
|
71
|
+
print(mm1(args.lam, args.mu).summary())
|
|
72
|
+
|
|
73
|
+
elif args.model == "mmc":
|
|
74
|
+
from .queuing import mmc
|
|
75
|
+
print(mmc(args.lam, args.mu, args.c).summary())
|
|
76
|
+
|
|
77
|
+
elif args.model == "md1":
|
|
78
|
+
from .queuing import md1
|
|
79
|
+
print(md1(args.lam, args.mu).summary())
|
|
80
|
+
|
|
81
|
+
elif args.model == "gg1":
|
|
82
|
+
from .queuing import kingman
|
|
83
|
+
print(kingman(args.lam, args.mu, args.ca2, args.cs2).summary())
|
|
84
|
+
|
|
85
|
+
elif args.model == "littles":
|
|
86
|
+
from .queuing import littles_law
|
|
87
|
+
result = littles_law(L=args.L, lam=args.lam, W=args.W)
|
|
88
|
+
missing = [k for k, v in {"L": args.L, "lam": args.lam, "W": args.W}.items() if v is None]
|
|
89
|
+
print(f"{missing[0]} = {result:.6g}")
|
|
90
|
+
|
|
91
|
+
elif args.model == "eoq":
|
|
92
|
+
from .inventory import eoq
|
|
93
|
+
print(eoq(args.demand, args.ordering, args.holding).summary())
|
|
94
|
+
|
|
95
|
+
except (ValueError, TypeError) as exc:
|
|
96
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
main()
|
walopy/_utils.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Internal validation helpers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def as_positive(value: float, name: str) -> float:
|
|
8
|
+
"""Validate that a scalar is finite and strictly positive."""
|
|
9
|
+
try:
|
|
10
|
+
v = float(value)
|
|
11
|
+
except (TypeError, ValueError):
|
|
12
|
+
raise TypeError(f"'{name}' must be a number, got {type(value).__name__!r}.")
|
|
13
|
+
if not np.isfinite(v) or v <= 0:
|
|
14
|
+
raise ValueError(f"'{name}' must be a finite positive number, got {value!r}.")
|
|
15
|
+
return v
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def as_nonneg(value: float, name: str) -> float:
|
|
19
|
+
"""Validate that a scalar is finite and non-negative."""
|
|
20
|
+
try:
|
|
21
|
+
v = float(value)
|
|
22
|
+
except (TypeError, ValueError):
|
|
23
|
+
raise TypeError(f"'{name}' must be a number, got {type(value).__name__!r}.")
|
|
24
|
+
if not np.isfinite(v) or v < 0:
|
|
25
|
+
raise ValueError(f"'{name}' must be finite and >= 0, got {value!r}.")
|
|
26
|
+
return v
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def as_fraction(value: float, name: str) -> float:
|
|
30
|
+
"""Validate that a scalar is in [0, 1]."""
|
|
31
|
+
try:
|
|
32
|
+
v = float(value)
|
|
33
|
+
except (TypeError, ValueError):
|
|
34
|
+
raise TypeError(f"'{name}' must be a number, got {type(value).__name__!r}.")
|
|
35
|
+
if not np.isfinite(v) or not (0.0 <= v <= 1.0):
|
|
36
|
+
raise ValueError(f"'{name}' must be a finite value in [0, 1], got {value!r}.")
|
|
37
|
+
return v
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def as_int_positive(value: int, name: str) -> int:
|
|
41
|
+
"""Validate that a value is a positive integer (accepts numpy integer types)."""
|
|
42
|
+
# Accept Python int and numpy integer scalars; reject float (even 2.0)
|
|
43
|
+
if not isinstance(value, (int, np.integer)):
|
|
44
|
+
raise TypeError(
|
|
45
|
+
f"'{name}' must be a positive integer, got {type(value).__name__!r} = {value!r}."
|
|
46
|
+
)
|
|
47
|
+
v = int(value)
|
|
48
|
+
if v < 1:
|
|
49
|
+
raise ValueError(f"'{name}' must be >= 1, got {value!r}.")
|
|
50
|
+
return v
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def as_nonempty(seq: list, name: str) -> None:
|
|
54
|
+
"""Raise ValueError if sequence is empty."""
|
|
55
|
+
if len(seq) == 0:
|
|
56
|
+
raise ValueError(f"'{name}' must contain at least one element.")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def as_finite_scalar(value: float, name: str) -> float:
|
|
60
|
+
"""Accept any finite real number (positive, zero, or negative)."""
|
|
61
|
+
try:
|
|
62
|
+
v = float(value)
|
|
63
|
+
except (TypeError, ValueError):
|
|
64
|
+
raise TypeError(f"'{name}' must be a number, got {type(value).__name__!r}.")
|
|
65
|
+
if not np.isfinite(v):
|
|
66
|
+
raise ValueError(f"'{name}' must be a finite number, got {value!r}.")
|
|
67
|
+
return v
|