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 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