most-queue 2.6__py3-none-any.whl → 2.8__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.
- most_queue/__init__.py +1 -1
- most_queue/io/plots.py +41 -5
- most_queue/misc/calc_v_pls.py +1 -1
- most_queue/random/distributions.py +62 -0
- most_queue/random/map_ph.py +282 -0
- most_queue/random/utils/create.py +9 -0
- most_queue/sim/__init__.py +2 -1
- most_queue/sim/base.py +194 -45
- most_queue/sim/base_core.py +87 -0
- most_queue/sim/batch.py +99 -99
- most_queue/sim/finite_source.py +246 -241
- most_queue/sim/flow_sum.py +240 -240
- most_queue/sim/fork_join.py +264 -266
- most_queue/sim/fork_join_delta.py +7 -0
- most_queue/sim/impatient.py +177 -184
- most_queue/sim/negative.py +248 -75
- most_queue/sim/networks/base_network_sim.py +201 -1
- most_queue/sim/networks/negative_network.py +622 -0
- most_queue/sim/networks/network.py +28 -27
- most_queue/sim/networks/priority_network.py +33 -35
- most_queue/sim/priority.py +71 -58
- most_queue/sim/retrial.py +79 -0
- most_queue/sim/single_server_disciplines.py +225 -0
- most_queue/sim/size_based.py +248 -0
- most_queue/sim/unreliable.py +95 -0
- most_queue/sim/utils/events.py +123 -0
- most_queue/sim/utils/predictor.py +44 -0
- most_queue/sim/utils/qs_queue.py +12 -0
- most_queue/sim/utils/queue_priority_size.py +112 -0
- most_queue/sim/utils/servers.py +45 -1
- most_queue/sim/utils/stats_update.py +2 -2
- most_queue/sim/utils/tasks.py +21 -0
- most_queue/sim/vacations.py +364 -339
- most_queue/structs.py +32 -1
- most_queue/theory/__init__.py +11 -7
- most_queue/theory/fifo/__init__.py +4 -0
- most_queue/theory/fifo/erlang.py +249 -0
- most_queue/theory/fifo/gi_g_approx.py +223 -0
- most_queue/theory/fifo/gi_m_n.py +257 -258
- most_queue/theory/fifo/gmc_takahasi.py +331 -0
- most_queue/theory/fifo/hkhk_takahasi.py +475 -0
- most_queue/theory/fifo/m_g_inf.py +114 -0
- most_queue/theory/fifo/mg1_lcfs_pr.py +106 -0
- most_queue/theory/fifo/mg1_ps.py +119 -0
- most_queue/theory/fifo/mgn_takahasi.py +250 -61
- most_queue/theory/fifo/takahasi_base.py +211 -0
- most_queue/theory/impatience/mmn.py +147 -0
- most_queue/theory/matrix/__init__.py +7 -0
- most_queue/theory/matrix/map_ph1.py +190 -0
- most_queue/theory/matrix/qbd.py +147 -0
- most_queue/theory/negative/mg1_disasters.py +112 -18
- most_queue/theory/negative/mgn_disaster.py +377 -143
- most_queue/theory/negative/mgn_rcs.py +530 -96
- most_queue/theory/negative/structs.py +0 -1
- most_queue/theory/networks/__init__.py +8 -0
- most_queue/theory/networks/negative_network.py +247 -0
- most_queue/theory/retrial/__init__.py +8 -0
- most_queue/theory/retrial/mg1.py +102 -0
- most_queue/theory/retrial/mm1.py +160 -0
- most_queue/theory/srpt/__init__.py +22 -0
- most_queue/theory/srpt/_base.py +136 -0
- most_queue/theory/srpt/mg1_fb.py +68 -0
- most_queue/theory/srpt/mg1_psjf.py +53 -0
- most_queue/theory/srpt/mg1_sjf.py +46 -0
- most_queue/theory/srpt/mg1_spjf.py +96 -0
- most_queue/theory/srpt/mg1_srpt.py +61 -0
- most_queue/theory/srpt/utils/__init__.py +12 -0
- most_queue/theory/srpt/utils/load_below.py +123 -0
- most_queue/theory/srpt/utils/predictor.py +153 -0
- most_queue/theory/utils/busy_periods.py +1 -2
- most_queue/theory/utils/conditional.py +1 -1
- most_queue/theory/utils/derivative.py +93 -0
- most_queue/theory/utils/passage_time.py +4 -2
- most_queue/theory/utils/restarts.py +241 -0
- most_queue/theory/utils/timing.py +2 -2
- most_queue/theory/vacations/m_h2_h2warm.py +1 -1
- most_queue/theory/vacations/mg1_unreliable.py +159 -0
- most_queue/theory/vacations/mg1_vacations.py +228 -0
- most_queue/theory/vacations/mg1_warm_calc.py +1 -1
- most_queue/theory/vacations/mgn_with_h2_delay_cold_warm.py +1 -1
- most_queue/theory/vacations/mmn_with_h2_cold_and_h2_warmup.py +1 -1
- most_queue-2.8.dist-info/METADATA +185 -0
- most_queue-2.8.dist-info/RECORD +154 -0
- {most_queue-2.6.dist-info → most_queue-2.8.dist-info}/WHEEL +1 -1
- most_queue-2.6.dist-info/METADATA +0 -247
- most_queue-2.6.dist-info/RECORD +0 -114
- {most_queue-2.6.dist-info → most_queue-2.8.dist-info}/licenses/LICENSE +0 -0
most_queue/__init__.py
CHANGED
|
@@ -5,7 +5,7 @@ This package provides both simulation and numerical calculation methods
|
|
|
5
5
|
for various types of queueing systems and networks.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
__version__ = "2.
|
|
8
|
+
__version__ = "2.7"
|
|
9
9
|
|
|
10
10
|
# Import main simulation and theory classes for easy access
|
|
11
11
|
from most_queue.sim.base import QsSim
|
most_queue/io/plots.py
CHANGED
|
@@ -17,6 +17,7 @@ class DependsType(Enum):
|
|
|
17
17
|
UTILIZATION_FACTOR = 1
|
|
18
18
|
CHANNELS_NUMBER = 2
|
|
19
19
|
COEFFICIENT_OF_VARIATION = 3
|
|
20
|
+
NEGATIVE_RATE = 4
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
class Plotter:
|
|
@@ -43,12 +44,26 @@ class Plotter:
|
|
|
43
44
|
ax.set_ylabel(ylabel)
|
|
44
45
|
if self.depends_on == DependsType.UTILIZATION_FACTOR:
|
|
45
46
|
ax.set_xlabel(r"$\rho$")
|
|
47
|
+
elif self.depends_on == DependsType.NEGATIVE_RATE:
|
|
48
|
+
ax.set_xlabel(r"$\delta$")
|
|
46
49
|
elif self.depends_on == DependsType.COEFFICIENT_OF_VARIATION:
|
|
47
50
|
ax.set_xlabel(r"$\nu$")
|
|
48
51
|
else:
|
|
49
52
|
ax.set_xlabel("n")
|
|
50
53
|
plt.xticks(range(1, len(self.xs) + 1))
|
|
51
54
|
|
|
55
|
+
@staticmethod
|
|
56
|
+
def _series_style():
|
|
57
|
+
"""
|
|
58
|
+
Centralized plot style for series used in figures.
|
|
59
|
+
Keeps line styles (solid/dashed) but uses distinct colors for readability in print/PDF.
|
|
60
|
+
"""
|
|
61
|
+
return {
|
|
62
|
+
"sim": {"color": "tab:orange", "linestyle": "--", "linewidth": 2.0},
|
|
63
|
+
"calc": {"color": "tab:blue", "linestyle": "-", "linewidth": 2.0},
|
|
64
|
+
"err": {"color": "tab:red", "linestyle": "-", "linewidth": 2.0},
|
|
65
|
+
}
|
|
66
|
+
|
|
52
67
|
def plot_errors(self, save_path: str = None):
|
|
53
68
|
"""
|
|
54
69
|
Plots the simulation and calculated moments for a given list of x values.
|
|
@@ -56,7 +71,8 @@ class Plotter:
|
|
|
56
71
|
_fig, ax = plt.subplots()
|
|
57
72
|
errors = [100 * (w_sim - w_tt) / w_tt for w_sim, w_tt in zip(self.sim_results, self.calc_results)]
|
|
58
73
|
|
|
59
|
-
|
|
74
|
+
style = self._series_style()
|
|
75
|
+
ax.plot(self.xs, errors, **style["err"])
|
|
60
76
|
self._configure_plot(ax, r"$\varepsilon$, %")
|
|
61
77
|
|
|
62
78
|
if save_path is not None:
|
|
@@ -71,8 +87,9 @@ class Plotter:
|
|
|
71
87
|
Plots the simulation and calculated queue waiting times.
|
|
72
88
|
"""
|
|
73
89
|
_fig, ax = plt.subplots()
|
|
74
|
-
|
|
75
|
-
ax.plot(self.xs, self.
|
|
90
|
+
style = self._series_style()
|
|
91
|
+
ax.plot(self.xs, self.sim_results, label="ИМ", **style["sim"])
|
|
92
|
+
ax.plot(self.xs, self.calc_results, label="Числ", **style["calc"])
|
|
76
93
|
ax.legend()
|
|
77
94
|
self._configure_plot(ax, r"$\omega_{1}$")
|
|
78
95
|
|
|
@@ -88,8 +105,9 @@ class Plotter:
|
|
|
88
105
|
Plots the simulation and calculated sojourn times.
|
|
89
106
|
"""
|
|
90
107
|
_fig, ax = plt.subplots()
|
|
91
|
-
|
|
92
|
-
ax.plot(self.xs, self.
|
|
108
|
+
style = self._series_style()
|
|
109
|
+
ax.plot(self.xs, self.sim_results, label="ИМ", **style["sim"])
|
|
110
|
+
ax.plot(self.xs, self.calc_results, label="Числ", **style["calc"])
|
|
93
111
|
ax.legend()
|
|
94
112
|
self._configure_plot(ax, r"$\upsilon_{1}$")
|
|
95
113
|
|
|
@@ -99,3 +117,21 @@ class Plotter:
|
|
|
99
117
|
plt.show()
|
|
100
118
|
|
|
101
119
|
plt.close(_fig)
|
|
120
|
+
|
|
121
|
+
def plot_series(self, ylabel: str, save_path: str = None):
|
|
122
|
+
"""
|
|
123
|
+
Generic plot: two series (sim vs calc) with a custom y-label.
|
|
124
|
+
"""
|
|
125
|
+
_fig, ax = plt.subplots()
|
|
126
|
+
style = self._series_style()
|
|
127
|
+
ax.plot(self.xs, self.sim_results, label="ИМ", **style["sim"])
|
|
128
|
+
ax.plot(self.xs, self.calc_results, label="Числ", **style["calc"])
|
|
129
|
+
ax.legend()
|
|
130
|
+
self._configure_plot(ax, ylabel)
|
|
131
|
+
|
|
132
|
+
if save_path is not None:
|
|
133
|
+
plt.savefig(save_path, dpi=300)
|
|
134
|
+
else:
|
|
135
|
+
plt.show()
|
|
136
|
+
|
|
137
|
+
plt.close(_fig)
|
most_queue/misc/calc_v_pls.py
CHANGED
|
@@ -7,11 +7,11 @@ import math
|
|
|
7
7
|
from functools import lru_cache
|
|
8
8
|
|
|
9
9
|
import numpy as np
|
|
10
|
-
from scipy.misc import derivative
|
|
11
10
|
|
|
12
11
|
from most_queue.random.distributions import H2Distribution, H2Params
|
|
13
12
|
from most_queue.theory.fifo.mgn_takahasi import MGnCalc, TakahashiTakamiParams
|
|
14
13
|
from most_queue.theory.utils.conv import conv_moments
|
|
14
|
+
from most_queue.theory.utils.derivative import derivative
|
|
15
15
|
from most_queue.theory.utils.transforms import lst_exp
|
|
16
16
|
|
|
17
17
|
|
|
@@ -251,6 +251,24 @@ class NormalDistribution(Distribution):
|
|
|
251
251
|
std_dev = cv * mean
|
|
252
252
|
return GaussianParams(mean, std_dev)
|
|
253
253
|
|
|
254
|
+
@staticmethod
|
|
255
|
+
def get_pdf(params: GaussianParams, t: float) -> float:
|
|
256
|
+
"""
|
|
257
|
+
PDF of the Gaussian distribution. Returns 0 for t < 0 (truncated at zero).
|
|
258
|
+
"""
|
|
259
|
+
if t < 0:
|
|
260
|
+
return 0.0
|
|
261
|
+
return float(stats.norm.pdf(t, loc=params.mean, scale=params.std_dev))
|
|
262
|
+
|
|
263
|
+
@staticmethod
|
|
264
|
+
def get_cdf(params: GaussianParams, t: float) -> float:
|
|
265
|
+
"""
|
|
266
|
+
CDF of the Gaussian distribution. Returns 0 for t < 0 (truncated at zero).
|
|
267
|
+
"""
|
|
268
|
+
if t < 0:
|
|
269
|
+
return 0.0
|
|
270
|
+
return float(stats.norm.cdf(t, loc=params.mean, scale=params.std_dev))
|
|
271
|
+
|
|
254
272
|
|
|
255
273
|
class UniformDistribution(Distribution):
|
|
256
274
|
"""
|
|
@@ -637,6 +655,26 @@ class DeterministicDistribution(Distribution):
|
|
|
637
655
|
b = f1
|
|
638
656
|
return b
|
|
639
657
|
|
|
658
|
+
@staticmethod
|
|
659
|
+
def get_pdf(_params, _t: float) -> float:
|
|
660
|
+
"""
|
|
661
|
+
Deterministic distribution has no density function.
|
|
662
|
+
|
|
663
|
+
Raises ``ValueError`` unconditionally — size-based SRPT/SJF/PSJF
|
|
664
|
+
formulas require a continuous service distribution.
|
|
665
|
+
"""
|
|
666
|
+
raise ValueError(
|
|
667
|
+
"Deterministic distribution ('D') has no density function; "
|
|
668
|
+
"SRPT/SJF/PSJF formulas require a continuous service distribution."
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
@staticmethod
|
|
672
|
+
def get_cdf(params, t: float) -> float:
|
|
673
|
+
"""
|
|
674
|
+
CDF of D(b): Heaviside step at b. *params* is the deterministic value b (float).
|
|
675
|
+
"""
|
|
676
|
+
return 0.0 if t < float(params) else 1.0
|
|
677
|
+
|
|
640
678
|
|
|
641
679
|
class ParetoDistribution(Distribution):
|
|
642
680
|
"""
|
|
@@ -802,6 +840,16 @@ class ErlangDistribution(Distribution):
|
|
|
802
840
|
"""
|
|
803
841
|
return 1.0 - ErlangDistribution.get_cdf(params, t)
|
|
804
842
|
|
|
843
|
+
@staticmethod
|
|
844
|
+
def get_pdf(params: ErlangParams, t: float) -> float:
|
|
845
|
+
"""
|
|
846
|
+
PDF of the Erlang(r, mu) distribution: f(t) = mu^r * t^(r-1) * exp(-mu*t) / (r-1)!
|
|
847
|
+
"""
|
|
848
|
+
if t < 0:
|
|
849
|
+
return 0.0
|
|
850
|
+
r, mu = params.r, params.mu
|
|
851
|
+
return (mu**r) * (t ** (r - 1)) * math.exp(-mu * t) / math.factorial(r - 1)
|
|
852
|
+
|
|
805
853
|
@staticmethod
|
|
806
854
|
def calc_theory_moments(params: ErlangParams, num: int = 3) -> list[float]:
|
|
807
855
|
"""
|
|
@@ -891,6 +939,20 @@ class ExpDistribution(Distribution):
|
|
|
891
939
|
"""
|
|
892
940
|
return 1 / f1
|
|
893
941
|
|
|
942
|
+
@staticmethod
|
|
943
|
+
def get_pdf(params: float, t: float) -> float:
|
|
944
|
+
"""
|
|
945
|
+
PDF of the exponential distribution with rate *params* (= mu = 1/mean).
|
|
946
|
+
"""
|
|
947
|
+
return ErlangDistribution.get_pdf(ErlangParams(r=1, mu=params), t)
|
|
948
|
+
|
|
949
|
+
@staticmethod
|
|
950
|
+
def get_cdf(params: float, t: float) -> float:
|
|
951
|
+
"""
|
|
952
|
+
CDF of the exponential distribution with rate *params* (= mu = 1/mean).
|
|
953
|
+
"""
|
|
954
|
+
return ErlangDistribution.get_cdf(ErlangParams(r=1, mu=params), t)
|
|
955
|
+
|
|
894
956
|
|
|
895
957
|
class GammaDistribution(Distribution):
|
|
896
958
|
"""
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Phase-type (PH) distributions and Markovian Arrival Processes (MAP).
|
|
3
|
+
|
|
4
|
+
A PH distribution is the time to absorption of a CTMC with initial vector
|
|
5
|
+
alpha and sub-generator T (absorption rates t0 = -T @ 1). A MAP is defined by
|
|
6
|
+
matrices (D0, D1): D0 holds phase transitions without arrivals, D1 —
|
|
7
|
+
transitions that generate an arrival; D0 + D1 is the generator of the phase
|
|
8
|
+
process. MAPs model correlated (bursty) traffic; a renewal PH source and the
|
|
9
|
+
MMPP are special cases.
|
|
10
|
+
|
|
11
|
+
References:
|
|
12
|
+
Neuts M.F. Matrix-Geometric Solutions in Stochastic Models. Johns Hopkins
|
|
13
|
+
University Press, 1981.
|
|
14
|
+
Latouche G., Ramaswami V. Introduction to Matrix Analytic Methods in
|
|
15
|
+
Stochastic Modeling. SIAM, 1999. doi:10.1137/1.9780898719734.
|
|
16
|
+
Fischer W., Meier-Hellstern K. The Markov-modulated Poisson process (MMPP)
|
|
17
|
+
cookbook. Performance Evaluation, 18(2), 1993.
|
|
18
|
+
doi:10.1016/0166-5316(93)90035-s.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import math
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
from scipy.linalg import expm
|
|
26
|
+
|
|
27
|
+
from most_queue.random.utils.params import Cox2Params, ErlangParams, H2Params
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class PHParams:
|
|
32
|
+
"""Phase-type distribution parameters: initial vector and sub-generator."""
|
|
33
|
+
|
|
34
|
+
alpha: np.ndarray # initial probability row vector, shape (m,)
|
|
35
|
+
T: np.ndarray # sub-generator matrix, shape (m, m)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class MAPParams:
|
|
40
|
+
"""Markovian Arrival Process parameters."""
|
|
41
|
+
|
|
42
|
+
D0: np.ndarray # transitions without arrivals (sub-generator), shape (m, m)
|
|
43
|
+
D1: np.ndarray # transitions generating an arrival, shape (m, m)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _validate_ph(alpha: np.ndarray, T: np.ndarray) -> None:
|
|
47
|
+
if T.ndim != 2 or T.shape[0] != T.shape[1]:
|
|
48
|
+
raise ValueError("T must be a square matrix")
|
|
49
|
+
if alpha.shape != (T.shape[0],):
|
|
50
|
+
raise ValueError(f"alpha shape {alpha.shape} does not match T {T.shape}")
|
|
51
|
+
if not math.isclose(float(np.sum(alpha)), 1.0, rel_tol=1e-9):
|
|
52
|
+
raise ValueError("alpha must sum to 1 (mass at zero is not supported)")
|
|
53
|
+
if np.any(np.diag(T) >= 0):
|
|
54
|
+
raise ValueError("diagonal of T must be negative")
|
|
55
|
+
exit_rates = -T @ np.ones(T.shape[0])
|
|
56
|
+
if np.any(exit_rates < -1e-12):
|
|
57
|
+
raise ValueError("row sums of T must be <= 0 (t0 = -T@1 must be non-negative)")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class PHDistribution:
|
|
61
|
+
"""
|
|
62
|
+
Phase-type distribution PH(alpha, T).
|
|
63
|
+
|
|
64
|
+
Follows the library's distribution interface: ``generate()`` draws a
|
|
65
|
+
sample by simulating the absorbing CTMC, static ``calc_theory_moments``
|
|
66
|
+
returns raw moments m_k = k! * alpha @ (-T)^{-k} @ 1.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(self, params: PHParams, generator=None):
|
|
70
|
+
self.params = params
|
|
71
|
+
self.alpha = np.asarray(params.alpha, dtype=float)
|
|
72
|
+
self.T = np.asarray(params.T, dtype=float)
|
|
73
|
+
_validate_ph(self.alpha, self.T)
|
|
74
|
+
self.t0 = -self.T @ np.ones(self.T.shape[0])
|
|
75
|
+
self.type = "PH"
|
|
76
|
+
self.generator = generator if generator is not None else np.random.default_rng()
|
|
77
|
+
|
|
78
|
+
# ------------------------------------------------------------- theory
|
|
79
|
+
@staticmethod
|
|
80
|
+
def calc_theory_moments(params: PHParams, num: int = 4) -> list[float]:
|
|
81
|
+
"""
|
|
82
|
+
Raw moments of PH(alpha, T): m_k = k! * alpha @ (-T)^{-k} @ 1.
|
|
83
|
+
"""
|
|
84
|
+
alpha = np.asarray(params.alpha, dtype=float)
|
|
85
|
+
T = np.asarray(params.T, dtype=float)
|
|
86
|
+
m_inv = np.linalg.inv(-T)
|
|
87
|
+
ones = np.ones(T.shape[0])
|
|
88
|
+
moments, vec = [], alpha.copy()
|
|
89
|
+
for k in range(1, num + 1):
|
|
90
|
+
vec = vec @ m_inv
|
|
91
|
+
moments.append(math.factorial(k) * float(vec @ ones))
|
|
92
|
+
return moments
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def get_pdf(params: PHParams, x: float) -> float:
|
|
96
|
+
"""Probability density f(x) = alpha @ expm(T x) @ t0."""
|
|
97
|
+
alpha = np.asarray(params.alpha, dtype=float)
|
|
98
|
+
T = np.asarray(params.T, dtype=float)
|
|
99
|
+
t0 = -T @ np.ones(T.shape[0])
|
|
100
|
+
return float(alpha @ expm(T * x) @ t0) if x >= 0 else 0.0
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def get_cdf(params: PHParams, x: float) -> float:
|
|
104
|
+
"""Cumulative distribution F(x) = 1 - alpha @ expm(T x) @ 1."""
|
|
105
|
+
alpha = np.asarray(params.alpha, dtype=float)
|
|
106
|
+
T = np.asarray(params.T, dtype=float)
|
|
107
|
+
return 1.0 - float(alpha @ expm(T * x) @ np.ones(T.shape[0])) if x >= 0 else 0.0
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------- converters
|
|
110
|
+
@staticmethod
|
|
111
|
+
def from_exp(mu: float) -> PHParams:
|
|
112
|
+
"""Exponential(mu) as a single-phase PH."""
|
|
113
|
+
return PHParams(alpha=np.array([1.0]), T=np.array([[-mu]]))
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def from_erlang(params: ErlangParams) -> PHParams:
|
|
117
|
+
"""Erlang-r as a chain of r exponential phases."""
|
|
118
|
+
r, mu = params.r, params.mu
|
|
119
|
+
T = -mu * np.eye(r) + mu * np.eye(r, k=1)
|
|
120
|
+
alpha = np.zeros(r)
|
|
121
|
+
alpha[0] = 1.0
|
|
122
|
+
return PHParams(alpha=alpha, T=T)
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def from_h2(params: H2Params) -> PHParams:
|
|
126
|
+
"""Hyperexponential H2 (real parameters only) as a two-phase PH."""
|
|
127
|
+
p1, mu1, mu2 = float(params.p1.real), float(params.mu1.real), float(params.mu2.real)
|
|
128
|
+
return PHParams(alpha=np.array([p1, 1.0 - p1]), T=np.array([[-mu1, 0.0], [0.0, -mu2]]))
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def from_cox(params: Cox2Params) -> PHParams:
|
|
132
|
+
"""Second-order Coxian as a two-phase PH."""
|
|
133
|
+
mu1, mu2, p1 = float(params.mu1.real), float(params.mu2.real), float(params.p1.real)
|
|
134
|
+
return PHParams(alpha=np.array([1.0, 0.0]), T=np.array([[-mu1, p1 * mu1], [0.0, -mu2]]))
|
|
135
|
+
|
|
136
|
+
# ------------------------------------------------------------ sampling
|
|
137
|
+
def generate(self) -> float:
|
|
138
|
+
"""Draw one sample by simulating the absorbing phase process."""
|
|
139
|
+
rng = self.generator
|
|
140
|
+
m = self.T.shape[0]
|
|
141
|
+
phase = int(rng.choice(m, p=self.alpha))
|
|
142
|
+
elapsed = 0.0
|
|
143
|
+
while True:
|
|
144
|
+
rate = -self.T[phase, phase]
|
|
145
|
+
elapsed += rng.exponential(1.0 / rate)
|
|
146
|
+
# transition probabilities from `phase`: to other phases or absorb
|
|
147
|
+
probs = np.append(self.T[phase].copy(), self.t0[phase])
|
|
148
|
+
probs[phase] = 0.0
|
|
149
|
+
probs /= rate
|
|
150
|
+
nxt = int(rng.choice(m + 1, p=probs))
|
|
151
|
+
if nxt == m:
|
|
152
|
+
return elapsed
|
|
153
|
+
phase = nxt
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class MAP:
|
|
157
|
+
"""
|
|
158
|
+
Markovian Arrival Process MAP(D0, D1).
|
|
159
|
+
|
|
160
|
+
``generate()`` returns successive interarrival times (the phase is kept
|
|
161
|
+
between calls, starting from the arrival-stationary distribution).
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
def __init__(self, params: MAPParams, generator=None):
|
|
165
|
+
self.params = params
|
|
166
|
+
self.D0 = np.asarray(params.D0, dtype=float)
|
|
167
|
+
self.D1 = np.asarray(params.D1, dtype=float)
|
|
168
|
+
if self.D0.shape != self.D1.shape or self.D0.ndim != 2 or self.D0.shape[0] != self.D0.shape[1]:
|
|
169
|
+
raise ValueError("D0 and D1 must be square matrices of the same shape")
|
|
170
|
+
gen_row_sums = (self.D0 + self.D1) @ np.ones(self.D0.shape[0])
|
|
171
|
+
if not np.allclose(gen_row_sums, 0.0, atol=1e-9):
|
|
172
|
+
raise ValueError("D0 + D1 must be a generator (zero row sums)")
|
|
173
|
+
if np.any(self.D1 < -1e-12):
|
|
174
|
+
raise ValueError("D1 must be non-negative")
|
|
175
|
+
self.type = "MAP"
|
|
176
|
+
self.generator = generator if generator is not None else np.random.default_rng()
|
|
177
|
+
self._phase = int(self.generator.choice(self.D0.shape[0], p=MAP.arrival_stationary_phase(params)))
|
|
178
|
+
|
|
179
|
+
# ------------------------------------------------------------- theory
|
|
180
|
+
@staticmethod
|
|
181
|
+
def phase_stationary(params: MAPParams) -> np.ndarray:
|
|
182
|
+
"""Stationary distribution pi of the phase generator D = D0 + D1."""
|
|
183
|
+
d = np.asarray(params.D0, dtype=float) + np.asarray(params.D1, dtype=float)
|
|
184
|
+
m = d.shape[0]
|
|
185
|
+
a = np.vstack([d.T, np.ones(m)])
|
|
186
|
+
b = np.zeros(m + 1)
|
|
187
|
+
b[-1] = 1.0
|
|
188
|
+
pi, *_ = np.linalg.lstsq(a, b, rcond=None)
|
|
189
|
+
return pi
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
def arrival_rate(params: MAPParams) -> float:
|
|
193
|
+
"""Fundamental arrival rate lambda = pi @ D1 @ 1."""
|
|
194
|
+
pi = MAP.phase_stationary(params)
|
|
195
|
+
return float(pi @ np.asarray(params.D1, dtype=float) @ np.ones(len(pi)))
|
|
196
|
+
|
|
197
|
+
@staticmethod
|
|
198
|
+
def arrival_stationary_phase(params: MAPParams) -> np.ndarray:
|
|
199
|
+
"""Phase distribution just after an arrival: pi @ D1 / lambda."""
|
|
200
|
+
pi = MAP.phase_stationary(params)
|
|
201
|
+
d1 = np.asarray(params.D1, dtype=float)
|
|
202
|
+
vec = pi @ d1
|
|
203
|
+
return vec / vec.sum()
|
|
204
|
+
|
|
205
|
+
@staticmethod
|
|
206
|
+
def calc_theory_moments(params: MAPParams, num: int = 4) -> list[float]:
|
|
207
|
+
"""
|
|
208
|
+
Raw moments of the stationary interarrival time:
|
|
209
|
+
m_k = k! * pi_a @ (-D0)^{-k} @ 1.
|
|
210
|
+
"""
|
|
211
|
+
pi_a = MAP.arrival_stationary_phase(params)
|
|
212
|
+
m_inv = np.linalg.inv(-np.asarray(params.D0, dtype=float))
|
|
213
|
+
ones = np.ones(len(pi_a))
|
|
214
|
+
moments, vec = [], pi_a.copy()
|
|
215
|
+
for k in range(1, num + 1):
|
|
216
|
+
vec = vec @ m_inv
|
|
217
|
+
moments.append(math.factorial(k) * float(vec @ ones))
|
|
218
|
+
return moments
|
|
219
|
+
|
|
220
|
+
@staticmethod
|
|
221
|
+
def lag_correlation(params: MAPParams, k: int = 1) -> float:
|
|
222
|
+
"""
|
|
223
|
+
Lag-k autocorrelation of interarrival times:
|
|
224
|
+
corr(X_0, X_k) with E[X_0 X_k] = pi_a @ M @ P^k @ M @ 1, M = (-D0)^{-1},
|
|
225
|
+
P = M @ D1 (phase transition matrix over one arrival).
|
|
226
|
+
"""
|
|
227
|
+
if k < 1:
|
|
228
|
+
raise ValueError("lag k must be >= 1")
|
|
229
|
+
pi_a = MAP.arrival_stationary_phase(params)
|
|
230
|
+
m_inv = np.linalg.inv(-np.asarray(params.D0, dtype=float))
|
|
231
|
+
p = m_inv @ np.asarray(params.D1, dtype=float)
|
|
232
|
+
ones = np.ones(len(pi_a))
|
|
233
|
+
m1 = float(pi_a @ m_inv @ ones)
|
|
234
|
+
m2 = 2.0 * float(pi_a @ m_inv @ m_inv @ ones)
|
|
235
|
+
joint = float(pi_a @ m_inv @ np.linalg.matrix_power(p, k) @ m_inv @ ones)
|
|
236
|
+
var = m2 - m1 * m1
|
|
237
|
+
return (joint - m1 * m1) / var
|
|
238
|
+
|
|
239
|
+
# ---------------------------------------------------------- factories
|
|
240
|
+
@staticmethod
|
|
241
|
+
def poisson(rate: float) -> MAPParams:
|
|
242
|
+
"""Poisson process as a trivial one-phase MAP."""
|
|
243
|
+
return MAPParams(D0=np.array([[-rate]]), D1=np.array([[rate]]))
|
|
244
|
+
|
|
245
|
+
@staticmethod
|
|
246
|
+
def from_ph_renewal(ph: PHParams) -> MAPParams:
|
|
247
|
+
"""Renewal process with PH interarrival times: D0 = T, D1 = t0 @ alpha."""
|
|
248
|
+
alpha = np.asarray(ph.alpha, dtype=float)
|
|
249
|
+
T = np.asarray(ph.T, dtype=float)
|
|
250
|
+
t0 = -T @ np.ones(T.shape[0])
|
|
251
|
+
return MAPParams(D0=T, D1=np.outer(t0, alpha))
|
|
252
|
+
|
|
253
|
+
@staticmethod
|
|
254
|
+
def mmpp(rates: list[float], q: np.ndarray) -> MAPParams:
|
|
255
|
+
"""
|
|
256
|
+
Markov-modulated Poisson process: Poisson rate rates[i] while the
|
|
257
|
+
modulating chain (generator q) is in state i.
|
|
258
|
+
"""
|
|
259
|
+
rates_arr = np.asarray(rates, dtype=float)
|
|
260
|
+
q_arr = np.asarray(q, dtype=float)
|
|
261
|
+
if not np.allclose(q_arr @ np.ones(q_arr.shape[0]), 0.0, atol=1e-9):
|
|
262
|
+
raise ValueError("q must be a generator (zero row sums)")
|
|
263
|
+
return MAPParams(D0=q_arr - np.diag(rates_arr), D1=np.diag(rates_arr))
|
|
264
|
+
|
|
265
|
+
# ------------------------------------------------------------ sampling
|
|
266
|
+
def generate(self) -> float:
|
|
267
|
+
"""Time until the next arrival (phase state persists between calls)."""
|
|
268
|
+
rng = self.generator
|
|
269
|
+
m = self.D0.shape[0]
|
|
270
|
+
elapsed = 0.0
|
|
271
|
+
while True:
|
|
272
|
+
rate = -self.D0[self._phase, self._phase]
|
|
273
|
+
elapsed += rng.exponential(1.0 / rate)
|
|
274
|
+
# competing transitions: D0 off-diagonal (no arrival) vs any D1 entry
|
|
275
|
+
probs = np.concatenate([self.D0[self._phase], self.D1[self._phase]])
|
|
276
|
+
probs[self._phase] = 0.0
|
|
277
|
+
probs = probs / rate
|
|
278
|
+
nxt = int(rng.choice(2 * m, p=probs))
|
|
279
|
+
if nxt >= m:
|
|
280
|
+
self._phase = nxt - m
|
|
281
|
+
return elapsed
|
|
282
|
+
self._phase = nxt
|
|
@@ -63,6 +63,15 @@ def create_distribution(params, kendall_notation: str, generator):
|
|
|
63
63
|
dist = NormalDistribution(params, generator=generator)
|
|
64
64
|
elif kendall_notation == "D":
|
|
65
65
|
dist = DeterministicDistribution(params)
|
|
66
|
+
elif kendall_notation == "PH":
|
|
67
|
+
# local import: map_ph pulls in scipy.linalg, keep base import light
|
|
68
|
+
from most_queue.random.map_ph import PHDistribution # pylint: disable=import-outside-toplevel
|
|
69
|
+
|
|
70
|
+
dist = PHDistribution(params, generator=generator)
|
|
71
|
+
elif kendall_notation == "MAP":
|
|
72
|
+
from most_queue.random.map_ph import MAP # pylint: disable=import-outside-toplevel
|
|
73
|
+
|
|
74
|
+
dist = MAP(params, generator=generator)
|
|
66
75
|
else:
|
|
67
76
|
raise ValueError(
|
|
68
77
|
"Incorrect distribution type specified. See most_queue.random.distributions.print_supported_distributions() for list of supported distributions"
|
most_queue/sim/__init__.py
CHANGED
|
@@ -6,5 +6,6 @@ various types of queueing systems and networks.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from most_queue.sim.base import QsSim
|
|
9
|
+
from most_queue.sim.size_based import PerfectSimPredictor, SizeBasedQsSim, SizePredictor
|
|
9
10
|
|
|
10
|
-
__all__ = ["QsSim"]
|
|
11
|
+
__all__ = ["QsSim", "SizeBasedQsSim", "SizePredictor", "PerfectSimPredictor"]
|