LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
mmap_modulate.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_API_MAM_MMAP_MODULATE_H
6#define LINE_API_MAM_MMAP_MODULATE_H
7
8/**
9 * @file
10 * @ingroup api_mam
11 * Modulate a family of marked MAPs by an environment chain.
12 *
13 * Templated port of matlab/lib/m3a/m3a/mmap/mmap_modulate.m and
14 * mmap_mixture_order2.m.
15 *
16 * `mmap_modulate(P, HT, MMAPs)` builds the marked arrival process of a system
17 * that sits in environment j for a phase-type holding time HT[j], arriving
18 * according to MMAPs[j] while it does, and then jumps to environment i with
19 * probability P(j,i). The state is the pair (holding-time phase, arrival phase),
20 * so environment j contributes a block of order nh(j) nm(j) and the result has
21 * order sum_j nh(j) nm(j):
22 *
23 * diagonal block j: krons(HT0[j], A0[j]), the two clocks running together;
24 * off-diagonal (j,i): P(j,i) (HT1[j] (x) I) 1 (pie(HT[i]) (x) pie(MMAP[i])),
25 * the holding time expiring and the new environment being entered in its
26 * own initial phase.
27 *
28 * THE ENVIRONMENT SWITCH EMITS NO ARRIVAL. The off-diagonal blocks appear in D0
29 * and in NO Dc, which is what makes the jump a hidden transition. The reference
30 * writes the per-class off-diagonal blocks explicitly as `0*P(j,i)*...`, i.e. a
31 * zero of the right shape; that is reproduced by simply leaving them zero.
32 *
33 * A PLAIN MAP IS AUTO-CONVERTED to a one-class MMAP, as the reference does, so a
34 * caller may pass either. All the components must then agree on the class count.
35 *
36 * `mmap_mixture_order2` is the second-order companion: given m^2 two-phase
37 * components PHs(i,j) and a second-order transition tensor P2, it builds the
38 * marked MAP whose state records the PREVIOUS and the current class, which is
39 * what lets a mixture reproduce a lag-1 class correlation that an order-1
40 * mixture cannot.
41 *
42 * ARITHMETIC: field. Kronecker products and block assembly only.
43 */
44
45#include <cstddef>
46#include <string>
47#include <vector>
48
55#include "line/num/number.h"
56#include "line/util/error.h"
57#include "line/util/matrix.h"
58
59namespace line {
60namespace mam {
61
62/**
63 * @brief Modulate a family of marked MAPs by an environment chain.
64 *
65 * @param P (J x J) environment transition probabilities
66 * @param HT the J phase-type holding times, as (D0, D1) pairs
67 * @param comps the J marked arrival processes, one per environment
68 */
69template <class T>
70Mmap<T> mmap_modulate(const Matrix<T>& P, const std::vector<Map<T>>& HT,
71 const std::vector<Mmap<T>>& comps) {
72 const std::size_t J = HT.size();
73 if (comps.size() != J)
74 throw InputError("mmap_modulate: the holding-time and MMAP lists must have equal length");
75 if (J == 0) throw InputError("mmap_modulate: no environments given");
76 if (P.rows() != J || P.cols() != J)
77 throw InputError("mmap_modulate: P must be square of the environment count");
78 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
79
80 // A plain MAP arrives here as an Mmap with no per-class blocks; the
81 // reference promotes it to a one-class MMAP by copying D1.
82 std::vector<Mmap<T>> M = comps;
83 for (std::size_t j = 0; j < J; ++j)
84 if (M[j].Dc.empty()) M[j].Dc.push_back(M[j].D1);
85 const std::size_t K = M[0].classes();
86 for (std::size_t j = 1; j < J; ++j)
87 if (M[j].classes() != K)
88 throw InputError("mmap_modulate: the MMAPs must have the same number of types");
89
90 std::vector<std::size_t> nh(J), nm(J), nq(J), off(J + 1, 0);
91 for (std::size_t j = 0; j < J; ++j) {
92 nh[j] = HT[j].D0.rows();
93 nm[j] = M[j].order();
94 nq[j] = nh[j] * nm[j];
95 off[j + 1] = off[j] + nq[j];
96 }
97 const std::size_t N = off[J];
98
99 Mmap<T> out;
100 out.D0 = Matrix<T>(N, N, zero);
101 out.D1 = Matrix<T>(N, N, zero);
102 out.Dc.assign(K, Matrix<T>(N, N, zero));
103
104 // The entry law of each environment: pie(HT) (x) pie(MMAP).
105 std::vector<std::vector<T>> entry(J);
106 for (std::size_t i = 0; i < J; ++i) {
107 const std::vector<T> a = map_pie(HT[i]);
108 const std::vector<T> b = map_pie(M[i].map());
109 entry[i].assign(nq[i], zero);
110 for (std::size_t p = 0; p < nh[i]; ++p)
111 for (std::size_t q = 0; q < nm[i]; ++q) entry[i][p * nm[i] + q] = T(a[p] * b[q]);
112 }
113
114 for (std::size_t j = 0; j < J; ++j) {
115 // Diagonal block: the two clocks run together, krons(HT0, A0).
116 const Matrix<T> diag = krons(HT[j].D0, M[j].D0);
117 for (std::size_t r = 0; r < nq[j]; ++r)
118 for (std::size_t c = 0; c < nq[j]; ++c) out.D0(off[j] + r, off[j] + c) = diag(r, c);
119 // Per-class blocks: an arrival of class k leaves the holding-time phase
120 // untouched, I (x) Ak.
121 for (std::size_t k = 0; k < K; ++k) {
122 const Matrix<T> blk = kron(eye<T>(nh[j]), M[j].Dc[k]);
123 for (std::size_t r = 0; r < nq[j]; ++r)
124 for (std::size_t c = 0; c < nq[j]; ++c)
125 out.Dc[k](off[j] + r, off[j] + c) = blk(r, c);
126 }
127
128 // Off-diagonal: the holding time expires and environment i is entered.
129 // (HT1 (x) I) 1 is the exit rate out of each (phase, phase) pair.
130 const Matrix<T> exit = kron(HT[j].D1, eye<T>(nm[j]));
131 std::vector<T> exitRow(nq[j], zero);
132 for (std::size_t r = 0; r < nq[j]; ++r) {
133 T s = zero;
134 for (std::size_t c = 0; c < nq[j]; ++c) s += exit(r, c);
135 exitRow[r] = s;
136 }
137 for (std::size_t i = 0; i < J; ++i) {
138 if (i == j) continue;
139 for (std::size_t r = 0; r < nq[j]; ++r)
140 for (std::size_t c = 0; c < nq[i]; ++c)
141 out.D0(off[j] + r, off[i] + c) = T(P(j, i) * exitRow[r] * entry[i][c]);
142 }
143 }
144 (void)one;
145 return mmap_normalize(out);
146}
147
148/**
149 * Second-order mixture: the state records the previous and the current class.
150 *
151 * @param PHs (m x m) two-phase components, PHs[i][j] being the sojourn in class
152 * j reached from class i
153 * @param P2 (m x m) second-order class transition probabilities
154 */
155template <class T>
156Mmap<T> mmap_mixture_order2(const std::vector<std::vector<Map<T>>>& PHs, const Matrix<T>& P2) {
157 const std::size_t m = PHs.size();
158 if (m == 0) throw InputError("mmap_mixture_order2: no components given");
159 for (std::size_t i = 0; i < m; ++i)
160 if (PHs[i].size() != m)
161 throw InputError("mmap_mixture_order2: the component table must be square");
162 if (P2.rows() != m || P2.cols() != m)
163 throw InputError("mmap_mixture_order2: P2 must be square of the component count");
164 // The reference indexes in blocks of two, so every component is order two.
165 for (std::size_t i = 0; i < m; ++i)
166 for (std::size_t j = 0; j < m; ++j)
167 if (PHs[i][j].D0.rows() != 2)
168 throw InputError("mmap_mixture_order2: every component must be of order two");
169
170 const T zero = num_traits<T>::from_int(0);
171 const std::size_t N = 2 * m * m;
172 Mmap<T> out;
173 out.D0 = Matrix<T>(N, N, zero);
174 out.D1 = Matrix<T>(N, N, zero);
175 out.Dc.assign(m, Matrix<T>(N, N, zero));
176
177 // Diagonal: the sojourn generator of each (previous, current) pair.
178 for (std::size_t i = 0; i < m; ++i)
179 for (std::size_t j = 0; j < m; ++j) {
180 const std::size_t k = i * m + j, base = 2 * k;
181 for (std::size_t r = 0; r < 2; ++r)
182 for (std::size_t c = 0; c < 2; ++c) out.D0(base + r, base + c) = PHs[i][j].D0(r, c);
183 }
184
185 // Off-diagonal: a completion in pair (i1,j1) marks class j1 and moves to a
186 // pair whose PREVIOUS class is j1, with probability P2(i1, i2).
187 for (std::size_t i1 = 0; i1 < m; ++i1)
188 for (std::size_t j1 = 0; j1 < m; ++j1) {
189 const std::size_t k1 = i1 * m + j1;
190 for (std::size_t i2 = 0; i2 < m; ++i2)
191 for (std::size_t j2 = 0; j2 < m; ++j2) {
192 if (j1 != i2) continue;
193 const std::size_t k2 = i2 * m + j2;
194 const std::vector<T> pie = map_pie(PHs[i2][j2]);
195 for (std::size_t r = 0; r < 2; ++r) {
196 T ex = zero;
197 for (std::size_t c = 0; c < 2; ++c) ex += -PHs[i1][j1].D0(r, c);
198 for (std::size_t c = 0; c < 2; ++c) {
199 const T v = T(P2(i1, i2) * ex * pie[c]);
200 out.Dc[j1](2 * k1 + r, 2 * k2 + c) = v;
201 }
202 }
203 }
204 }
205 return mmap_normalize(out);
206}
207
208
209/**
210 * Second-order mixture FITTED from cross moments and a triple sigma, the
211 * reference's `mmap_mixture_fit`.
212 *
213 * Port of matlab/lib/m3a/m3a/mmap/mmap_mixture_fit.m and
214 * mmap_mixture_fit_trace.m. Each ordered class pair (i,j) gets its own APH(2)
215 * fitted to the cross moments M1(i,j), M2(i,j), M3(i,j) -- the sojourn in class
216 * j when it followed class i -- and those are assembled exactly as
217 * `mmap_mixture_order2` assembles them, except that the transition weight is
218 * the CONDITIONAL second-order probability
219 *
220 * p = P2(i1, i2, j2) / sum_h P2(i1, i2, h),
221 *
222 * so the chain over (previous, current) pairs is stochastic by construction.
223 * That normalization is the whole difference from `mmap_mixture_order2`, which
224 * takes an already-conditioned two-index weight.
225 *
226 * @param P2 the triple sigma, (C x C*C) with entry (i, j*C + h) = P2(i,j,h)
227 * @param M1,M2,M3 the (C x C) cross moments
228 */
229template <class T>
230Mmap<T> mmap_mixture_fit(const Matrix<T>& P2, const Matrix<T>& M1, const Matrix<T>& M2,
231 const Matrix<T>& M3) {
233 "mmap_mixture_fit fits an APH(2) per class pair");
234 const std::size_t m = M1.rows();
235 if (m == 0) throw InputError("mmap_mixture_fit: no classes given");
236 if (M1.cols() != m || M2.rows() != m || M2.cols() != m || M3.rows() != m || M3.cols() != m)
237 throw InputError("mmap_mixture_fit: the cross-moment tables must be square and equal");
238 if (P2.rows() != m || P2.cols() != m * m)
239 throw InputError("mmap_mixture_fit: P2 must be (C x C*C), the flattened triple sigma");
240 const T zero = num_traits<T>::from_int(0);
241
242 // A pair that was never observed has no cross moment to fit, and fitting a
243 // zero first moment would silently produce a degenerate component. The
244 // reference does not guard it; refusing by name says which pair is missing.
245 std::vector<std::vector<Map<T>>> PHs(m, std::vector<Map<T>>(m));
246 for (std::size_t i = 0; i < m; ++i)
247 for (std::size_t j = 0; j < m; ++j) {
248 if (!(num_traits<T>::to_double(M1(i, j)) > 0.0))
249 throw InputError(
250 "mmap_mixture_fit: the cross moment of class pair (" + std::to_string(i + 1) +
251 ", " + std::to_string(j + 1) +
252 ") is not positive, so that pair was never observed and its component cannot "
253 "be fitted. Supply a trace in which every ordered class pair occurs, or fit "
254 "fewer classes");
255 PHs[i][j] = aph2_fit(M1(i, j), M2(i, j), M3(i, j)).aph;
256 }
257
258 const std::size_t N = 2 * m * m;
259 Mmap<T> out;
260 out.D0 = Matrix<T>(N, N, zero);
261 out.D1 = Matrix<T>(N, N, zero);
262 out.Dc.assign(m, Matrix<T>(N, N, zero));
263
264 for (std::size_t i = 0; i < m; ++i)
265 for (std::size_t j = 0; j < m; ++j) {
266 const std::size_t base = 2 * (i * m + j);
267 for (std::size_t r = 0; r < 2; ++r)
268 for (std::size_t c = 0; c < 2; ++c) out.D0(base + r, base + c) = PHs[i][j].D0(r, c);
269 }
270
271 for (std::size_t i1 = 0; i1 < m; ++i1)
272 for (std::size_t j1 = 0; j1 < m; ++j1) {
273 const std::size_t k1 = i1 * m + j1;
274 // The row of the conditional law: P2(i1, j1, .) normalized.
275 T rowtot = zero;
276 for (std::size_t h = 0; h < m; ++h) rowtot += P2(i1, j1 * m + h);
277 for (std::size_t i2 = 0; i2 < m; ++i2)
278 for (std::size_t j2 = 0; j2 < m; ++j2) {
279 if (j1 != i2) continue;
280 if (!(num_traits<T>::to_double(rowtot) > 0.0)) continue;
281 const T pr = T(P2(i1, i2 * m + j2) / rowtot);
282 const std::size_t k2 = i2 * m + j2;
283 const std::vector<T> pie = map_pie(PHs[i2][j2]);
284 for (std::size_t r = 0; r < 2; ++r) {
285 T ex = zero;
286 for (std::size_t c = 0; c < 2; ++c) ex += -PHs[i1][j1].D0(r, c);
287 for (std::size_t c = 0; c < 2; ++c)
288 out.Dc[j1](2 * k1 + r, 2 * k2 + c) = T(pr * ex * pie[c]);
289 }
290 }
291 }
292 return mmap_normalize(out);
293}
294
295/**
296 * `mmap_mixture_fit` driven from a marked trace: the triple sigma and the cross
297 * moments are measured on the trace itself.
298 *
299 * @param Tv the inter-arrival times
300 * @param A the class of each arrival
301 */
302template <class T>
303Mmap<T> mmap_mixture_fit_trace(const std::vector<T>& Tv, const std::vector<int>& A) {
304 if (Tv.empty() || Tv.size() != A.size())
305 throw InputError("mmap_mixture_fit_trace: the trace and its labels must agree in length");
307 const Matrix<T> M1 = trace::mtrace_cross_moment(Tv, A, 1u).mc;
308 const Matrix<T> M2 = trace::mtrace_cross_moment(Tv, A, 2u).mc;
309 const Matrix<T> M3 = trace::mtrace_cross_moment(Tv, A, 3u).mc;
310 return mmap_mixture_fit(P2, M1, M2, M3);
311}
312
313} // namespace mam
314} // namespace line
315
316#endif // LINE_API_MAM_MMAP_MODULATE_H
APH(2) fit of three moments, with a fallback to adjusted moments (matlab/lib/m3a/m3a/aph2/aph2_fit....
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
The exception types the port throws.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
Dense matrix and non-owning view.
Marked MAP (MMAP) algebra: per-class rates, class probabilities, superposition, normalization and sca...
Marked MAP statistics: embedded chains, class-transition probabilities, forward and cross moments,...
Class-pair cross moments of a marked trace: the k-th moment of the interval that separates an event o...
Two-step class transition frequencies of a marked trace,.
Mmap< T > mmap_normalize(const Mmap< T > &in)
Clamp negative off-diagonal and per-class entries to zero and rebuild D1 and the diagonal of D0 from ...
Aph2FitResult< T > aph2_fit(const T &M1, const T &M2, const T &M3)
Fit an APH(2) to (M1, M2, M3), relaxing the moments if necessary.
Definition aph2_fit.h:42
Mmap< T > mmap_mixture_fit_trace(const std::vector< T > &Tv, const std::vector< int > &A)
mmap_mixture_fit driven from a marked trace: the triple sigma and the cross moments are measured on t...
Mmap< T > mmap_mixture_fit(const Matrix< T > &P2, const Matrix< T > &M1, const Matrix< T > &M2, const Matrix< T > &M3)
Second-order mixture FITTED from cross moments and a triple sigma, the reference's mmap_mixture_fit.
Matrix< T > krons(const Matrix< T > &A, const Matrix< T > &B)
Kronecker sum, MATLAB's krons: kron(A, I_nb) + kron(I_na, B).
Definition mmap_lambda.h:71
Mmap< T > mmap_mixture_order2(const std::vector< std::vector< Map< T > > > &PHs, const Matrix< T > &P2)
Second-order mixture: the state records the previous and the current class.
Matrix< T > kron(const Matrix< T > &A, const Matrix< T > &B)
Kronecker product.
Definition mmap_lambda.h:57
Mmap< T > mmap_modulate(const Matrix< T > &P, const std::vector< Map< T > > &HT, const std::vector< Mmap< T > > &comps)
Modulate a family of marked MAPs by an environment chain.
std::vector< T > map_pie(const Map< T > &m)
Phase distribution seen by an arriving job, pie = pi D1 / (pi D1 e).
Definition map_moment.h:89
MtraceCrossMomentResult< T > mtrace_cross_moment(const std::vector< T > &Tv, const std::vector< int > &L, unsigned k)
Class-pair cross moments of a marked trace: the k-th moment of the interval that separates an event o...
Matrix< T > mtrace_sigma2(const std::vector< int > &L)
Two-step class transition frequencies of a marked trace, sigma(i,j,h) = #{t : A_t = i,...
Matrix< T > eye(std::size_t n)
Identity of order n.
Definition linalg.h:28
Number-type abstraction for the templated API port.
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
An MMAP: the underlying MAP plus the per-class arrival matrices.
Definition mmap_lambda.h:45
Matrix< T > D0
Definition mmap_lambda.h:46
Matrix< T > D1
Definition mmap_lambda.h:47
std::vector< Matrix< T > > Dc
per-class matrices, sum_c Dc = D1
Definition mmap_lambda.h:48