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.
Files changed (87) hide show
  1. most_queue/__init__.py +1 -1
  2. most_queue/io/plots.py +41 -5
  3. most_queue/misc/calc_v_pls.py +1 -1
  4. most_queue/random/distributions.py +62 -0
  5. most_queue/random/map_ph.py +282 -0
  6. most_queue/random/utils/create.py +9 -0
  7. most_queue/sim/__init__.py +2 -1
  8. most_queue/sim/base.py +194 -45
  9. most_queue/sim/base_core.py +87 -0
  10. most_queue/sim/batch.py +99 -99
  11. most_queue/sim/finite_source.py +246 -241
  12. most_queue/sim/flow_sum.py +240 -240
  13. most_queue/sim/fork_join.py +264 -266
  14. most_queue/sim/fork_join_delta.py +7 -0
  15. most_queue/sim/impatient.py +177 -184
  16. most_queue/sim/negative.py +248 -75
  17. most_queue/sim/networks/base_network_sim.py +201 -1
  18. most_queue/sim/networks/negative_network.py +622 -0
  19. most_queue/sim/networks/network.py +28 -27
  20. most_queue/sim/networks/priority_network.py +33 -35
  21. most_queue/sim/priority.py +71 -58
  22. most_queue/sim/retrial.py +79 -0
  23. most_queue/sim/single_server_disciplines.py +225 -0
  24. most_queue/sim/size_based.py +248 -0
  25. most_queue/sim/unreliable.py +95 -0
  26. most_queue/sim/utils/events.py +123 -0
  27. most_queue/sim/utils/predictor.py +44 -0
  28. most_queue/sim/utils/qs_queue.py +12 -0
  29. most_queue/sim/utils/queue_priority_size.py +112 -0
  30. most_queue/sim/utils/servers.py +45 -1
  31. most_queue/sim/utils/stats_update.py +2 -2
  32. most_queue/sim/utils/tasks.py +21 -0
  33. most_queue/sim/vacations.py +364 -339
  34. most_queue/structs.py +32 -1
  35. most_queue/theory/__init__.py +11 -7
  36. most_queue/theory/fifo/__init__.py +4 -0
  37. most_queue/theory/fifo/erlang.py +249 -0
  38. most_queue/theory/fifo/gi_g_approx.py +223 -0
  39. most_queue/theory/fifo/gi_m_n.py +257 -258
  40. most_queue/theory/fifo/gmc_takahasi.py +331 -0
  41. most_queue/theory/fifo/hkhk_takahasi.py +475 -0
  42. most_queue/theory/fifo/m_g_inf.py +114 -0
  43. most_queue/theory/fifo/mg1_lcfs_pr.py +106 -0
  44. most_queue/theory/fifo/mg1_ps.py +119 -0
  45. most_queue/theory/fifo/mgn_takahasi.py +250 -61
  46. most_queue/theory/fifo/takahasi_base.py +211 -0
  47. most_queue/theory/impatience/mmn.py +147 -0
  48. most_queue/theory/matrix/__init__.py +7 -0
  49. most_queue/theory/matrix/map_ph1.py +190 -0
  50. most_queue/theory/matrix/qbd.py +147 -0
  51. most_queue/theory/negative/mg1_disasters.py +112 -18
  52. most_queue/theory/negative/mgn_disaster.py +377 -143
  53. most_queue/theory/negative/mgn_rcs.py +530 -96
  54. most_queue/theory/negative/structs.py +0 -1
  55. most_queue/theory/networks/__init__.py +8 -0
  56. most_queue/theory/networks/negative_network.py +247 -0
  57. most_queue/theory/retrial/__init__.py +8 -0
  58. most_queue/theory/retrial/mg1.py +102 -0
  59. most_queue/theory/retrial/mm1.py +160 -0
  60. most_queue/theory/srpt/__init__.py +22 -0
  61. most_queue/theory/srpt/_base.py +136 -0
  62. most_queue/theory/srpt/mg1_fb.py +68 -0
  63. most_queue/theory/srpt/mg1_psjf.py +53 -0
  64. most_queue/theory/srpt/mg1_sjf.py +46 -0
  65. most_queue/theory/srpt/mg1_spjf.py +96 -0
  66. most_queue/theory/srpt/mg1_srpt.py +61 -0
  67. most_queue/theory/srpt/utils/__init__.py +12 -0
  68. most_queue/theory/srpt/utils/load_below.py +123 -0
  69. most_queue/theory/srpt/utils/predictor.py +153 -0
  70. most_queue/theory/utils/busy_periods.py +1 -2
  71. most_queue/theory/utils/conditional.py +1 -1
  72. most_queue/theory/utils/derivative.py +93 -0
  73. most_queue/theory/utils/passage_time.py +4 -2
  74. most_queue/theory/utils/restarts.py +241 -0
  75. most_queue/theory/utils/timing.py +2 -2
  76. most_queue/theory/vacations/m_h2_h2warm.py +1 -1
  77. most_queue/theory/vacations/mg1_unreliable.py +159 -0
  78. most_queue/theory/vacations/mg1_vacations.py +228 -0
  79. most_queue/theory/vacations/mg1_warm_calc.py +1 -1
  80. most_queue/theory/vacations/mgn_with_h2_delay_cold_warm.py +1 -1
  81. most_queue/theory/vacations/mmn_with_h2_cold_and_h2_warmup.py +1 -1
  82. most_queue-2.8.dist-info/METADATA +185 -0
  83. most_queue-2.8.dist-info/RECORD +154 -0
  84. {most_queue-2.6.dist-info → most_queue-2.8.dist-info}/WHEEL +1 -1
  85. most_queue-2.6.dist-info/METADATA +0 -247
  86. most_queue-2.6.dist-info/RECORD +0 -114
  87. {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.05"
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
- ax.plot(self.xs, errors, color="black")
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
- ax.plot(self.xs, self.sim_results, label="Sim", color="black", linestyle="--")
75
- ax.plot(self.xs, self.calc_results, label="Calc", color="black")
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
- ax.plot(self.xs, self.sim_results, label="Sim", color="black", linestyle="--")
92
- ax.plot(self.xs, self.calc_results, label="Calc", color="black")
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)
@@ -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"
@@ -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"]