LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
mdd_mcd.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_MDD_MDD_MCD_H
6#define LINE_API_MDD_MDD_MCD_H
7
8/**
9 * @file
10 * @ingroup api_mdd
11 * Miner-Ciardo-Donatelli approximate stationary analysis.
12 *
13 * Port of matlab/src/api/mdd/mdd_mcd.m, jline.api.mdd.Mdd_mcd and
14 * python/line_solver/api/mdd/mcd.py, after A.S. Miner, G. Ciardo, S. Donatelli,
15 * "Using the exact state space of a Markov model to compute approximate
16 * stationary measures", ACM SIGMETRICS 2000, pp.207-216.
17 *
18 * Solve a structured CTMC whose EXACT reachable state space is stored in a
19 * decision diagram, by building and iterating K level-CTMCs. The method never
20 * forms the |S|-state generator or probability vector. It keeps one CTMC per
21 * level k, over states M_k = {(p,i_k)} with p a level-k node and i_k a local
22 * state on a non-null arc, and iterates the coupled system to a fixed point.
23 * The single approximation (Eq. 5) is Pr{i_k | alpha} = Pr{i_k | p}: the
24 * local-state law at level k depends only on the node p, not the full path
25 * above it, which the exact reachability the diagram encodes justifies. For
26 * product-form models the method is EXACT (paper Sec. 5), so on a single-class
27 * closed QN it reproduces SolverCTMC.
28 *
29 * ORIENTATION. The paper indexes levels K (top/root) down to 1
30 * (bottom/terminal); `MDD` uses level 0 as the root. This function works in the
31 * paper's orientation with 0-based indices, so paper level k (0 = bottom) maps
32 * to MDD level K-1-k and to station K-1-k. Getting this backwards silently
33 * mislabels every per-station metric.
34 */
35
36#include <algorithm>
37#include <cmath>
38#include <cstddef>
39#include <string>
40#include <utility>
41#include <vector>
42
43#include "line/api/mdd/mdd.h"
45#include "line/num/number.h"
46#include "line/util/error.h"
47#include "line/util/lstsq.h"
48#include "line/util/matrix.h"
49
50namespace line {
51namespace mdd {
52
53namespace detail {
54
55/** Node marginal of a level vector: Pr{p} = sum over the arcs of p. */
56template <class T>
57std::vector<T> mcd_node_marginal(const std::vector<std::pair<int, int>>& rows,
58 const std::vector<T>& pk, int nnodes) {
59 std::vector<T> pr(static_cast<std::size_t>(nnodes), num_traits<T>::from_int(0));
60 for (std::size_t r = 0; r < rows.size(); ++r) pr[rows[r].first - 1] += pk[r];
61 return pr;
62}
63
64template <class T>
65std::vector<std::vector<T>> mcd_identity(std::size_t n) {
66 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
67 std::vector<std::vector<T>> I(n, std::vector<T>(n, zero));
68 for (std::size_t i = 0; i < n; ++i) I[i][i] = one;
69 return I;
70}
71
72/** Infinitesimal generator of a rate matrix: the diagonal absorbs the row sum. */
73template <class T>
74std::vector<std::vector<T>> mcd_generator(const std::vector<std::vector<T>>& R) {
75 const std::size_t n = R.size();
76 const T zero = num_traits<T>::from_int(0);
77 std::vector<std::vector<T>> Q = R;
78 for (std::size_t i = 0; i < n; ++i) {
79 T s = zero;
80 for (std::size_t j = 0; j < n; ++j) s += R[i][j];
81 Q[i][i] -= s;
82 }
83 return Q;
84}
85
86/**
87 * Least-squares solution of an overdetermined system by Householder QR.
88 *
89 * QR is used rather than the normal equations because A'A squares the condition
90 * number, which is exactly the failure the appended-normalisation form of
91 * `mcd_solve_stat` exists to avoid. This is why the shared `line::lstsq` is not
92 * called here: it forms the normal equations on the full-rank branch.
93 */
94template <class T>
95std::vector<T> mcd_lstsq(const std::vector<std::vector<T>>& A, const std::vector<T>& b) {
96 using std::sqrt;
97 const std::size_t m = A.size(), n = A[0].size();
98 const T zero = num_traits<T>::from_int(0), two = num_traits<T>::from_int(2);
99 std::vector<std::vector<T>> R = A;
100 std::vector<T> y = b;
101 for (std::size_t k = 0; k < n; ++k) {
102 T norm2 = zero;
103 for (std::size_t i = k; i < m; ++i) norm2 += T(R[i][k] * R[i][k]);
104 if (norm2 == zero) continue;
105 T norm = T(sqrt(norm2));
106 if (R[k][k] > zero) norm = T(-norm);
107 std::vector<T> v(m, zero);
108 for (std::size_t i = k; i < m; ++i) v[i] = R[i][k];
109 v[k] -= norm;
110 T vtv = zero;
111 for (std::size_t i = k; i < m; ++i) vtv += T(v[i] * v[i]);
112 if (vtv == zero) continue;
113 for (std::size_t j = k; j < n; ++j) {
114 T dot = zero;
115 for (std::size_t i = k; i < m; ++i) dot += T(v[i] * R[i][j]);
116 const T f = T(two * dot / vtv);
117 for (std::size_t i = k; i < m; ++i) R[i][j] -= T(f * v[i]);
118 }
119 T dot = zero;
120 for (std::size_t i = k; i < m; ++i) dot += T(v[i] * y[i]);
121 const T f = T(two * dot / vtv);
122 for (std::size_t i = k; i < m; ++i) y[i] -= T(f * v[i]);
123 }
124 std::vector<T> x(n, zero);
125 for (std::size_t ii = n; ii > 0; --ii) {
126 const std::size_t i = ii - 1;
127 T s = y[i];
128 for (std::size_t j = i + 1; j < n; ++j) s -= T(R[i][j] * x[j]);
129 x[i] = R[i][i] == zero ? zero : T(s / R[i][i]);
130 }
131 return x;
132}
133
134/**
135 * Stationary distribution of a small irreducible generator: p Q = 0, sum p = 1.
136 *
137 * The normalisation is APPENDED rather than substituted for the last balance
138 * equation: overwriting a row discards a constraint and leaves Q' singular to
139 * working precision from about |M_k| = 325 upwards, so the solve returns NaN.
140 * The overdetermined system has full column rank whenever the level chain is
141 * irreducible, and least squares solves it stably.
142 *
143 * TWO BACKENDS, chosen by the arithmetic rather than by an option. In floating
144 * point the Householder QR above is used, because the shared `line::lstsq`
145 * forms the normal equations on its full-rank branch and A'A squares the
146 * condition number -- exactly the failure the appended normalisation exists to
147 * avoid. Under EXACT arithmetic there is no condition number to square and no
148 * square root to take, so `line::lstsq` is called directly: that is what makes
149 * the level aggregation available at `Rational`, where it returns the fixed
150 * point of the level system with no rounding at all.
151 */
152template <class T>
153std::vector<T> mcd_solve_stat(const std::vector<std::vector<T>>& Q) {
154 const std::size_t n = Q.size();
155 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
156 if (n == 1) return std::vector<T>(1, one);
157 std::vector<std::vector<T>> A(n + 1, std::vector<T>(n, zero));
158 for (std::size_t i = 0; i < n; ++i)
159 for (std::size_t j = 0; j < n; ++j) A[j][i] = Q[i][j];
160 for (std::size_t j = 0; j < n; ++j) A[n][j] = one;
161 std::vector<T> rhs(n + 1, zero);
162 rhs[n] = one;
163 std::vector<T> p;
164 if constexpr (num_traits<T>::is_exact) {
165 Matrix<T> Am(n + 1, n, zero);
166 for (std::size_t i = 0; i <= n; ++i)
167 for (std::size_t j = 0; j < n; ++j) Am(i, j) = A[i][j];
168 p = line::lstsq(Am, rhs).x;
169 } else {
170 p = mcd_lstsq(A, rhs);
171 }
172 T s = zero;
173 for (std::size_t i = 0; i < n; ++i) {
174 if (p[i] < zero) p[i] = zero;
175 s += p[i];
176 }
177 const double sd = num_traits<T>::to_double(s);
178 if (!(sd > 0) || std::isnan(sd) || std::isinf(sd))
179 throw NumericError("mdd_mcd: level CTMC of order " + std::to_string(n) +
180 " admits no proper stationary distribution (the level generator is "
181 "reducible or numerically degenerate).");
182 for (std::size_t i = 0; i < n; ++i) p[i] = T(p[i] / s);
183 return p;
184}
185
186/**
187 * Path counts per MDD level: above[oL][p] is the number of distinct root-to-p
188 * paths, |A(p)| in the paper's notation, and below[oL][p] the number of
189 * accepted states under p.
190 *
191 * Both are O(#nodes) and serve two purposes: the uniform initialisation, and
192 * the exactness certificate. The single approximation is
193 * Pr{i_k | alpha} = Pr{i_k | p}, so when a node is reached by exactly one path,
194 * conditioning on the node IS conditioning on the path and the identity is
195 * exact. That test is SUFFICIENT, not necessary: a product-form model is exact
196 * too however much its diagram shares. Note also that max |A(p)| = 1 means no
197 * node is shared, i.e. the diagram compresses nothing, so exactness by this
198 * route and a useful saving are mutually exclusive.
199 */
200inline void mcd_path_counts(const MddStruct& mdds, std::size_t K,
201 std::vector<std::vector<double>>& above,
202 std::vector<std::vector<double>>& below) {
203 below.assign(K, std::vector<double>());
204 above.assign(K, std::vector<double>());
205 for (std::size_t oo = K; oo > 0; --oo) {
206 const std::size_t oL = oo - 1;
207 std::vector<double> nb(static_cast<std::size_t>(mdds.nnodes[oL]), 0.0);
208 for (std::size_t p = 0; p < nb.size(); ++p) {
209 double s = 0;
210 for (int v = 0; v < mdds.domain[oL]; ++v) {
211 const int ch = mdds.node[oL][p][v];
212 if (oL + 1 == K) {
213 if (ch == TERM_TRUE) s += 1.0;
214 } else if (ch > 0) {
215 s += below[oL + 1][ch - 1];
216 }
217 }
218 nb[p] = s;
219 }
220 below[oL] = nb;
221 }
222 for (std::size_t oL = 0; oL < K; ++oL)
223 above[oL].assign(static_cast<std::size_t>(mdds.nnodes[oL]), 0.0);
224 above[0][mdds.root - 1] = 1.0;
225 for (std::size_t oL = 0; oL + 1 < K; ++oL) {
226 for (std::size_t p = 0; p < above[oL].size(); ++p) {
227 const double w = above[oL][p];
228 if (w == 0) continue;
229 for (int v = 0; v < mdds.domain[oL]; ++v) {
230 const int ch = mdds.node[oL][p][v];
231 if (ch > 0) above[oL + 1][ch - 1] += w;
232 }
233 }
234 }
235}
236
237/**
238 * Uniform law over the EXACT reachable set, projected onto each level.
239 *
240 * Pr{(p,v)} = (paths root->p) * (states below arc p[v]) / |S|. A flat law over
241 * M_k instead treats level states as equiprobable irrespective of how many
242 * global states they stand for, which breaks the population invariant the
243 * diagram encodes; from about K=8 the coupled iteration then descends into the
244 * basin of the DEGENERATE empty-population fixed point (all mass on local state
245 * 0 at every level, a true fixed point since no station can then emit) and
246 * converges to it with zero residual. The projection below is consistent across
247 * levels by construction, so the iteration starts inside the physical simplex.
248 */
249template <class T>
250std::vector<std::vector<T>> mcd_uniform_init(
251 const MddStruct& mdds, const std::vector<std::vector<std::pair<int, int>>>& Mrows,
252 std::size_t K, const std::vector<std::vector<double>>& above,
253 const std::vector<std::vector<double>>& below) {
254 std::vector<std::vector<T>> pik(K);
255 for (std::size_t k = 0; k < K; ++k) {
256 const std::size_t oL = K - 1 - k; // paper level k is MDD level K-1-k
257 const std::vector<std::pair<int, int>>& rows = Mrows[k];
258 std::vector<double> w(rows.size(), 0.0);
259 double sum = 0;
260 for (std::size_t r = 0; r < rows.size(); ++r) {
261 const int p = rows[r].first, v = rows[r].second;
262 double val;
263 if (oL + 1 == K) {
264 val = above[oL][p - 1]; // a TRUE arc stands for one state
265 } else {
266 const int ch = mdds.node[oL][p - 1][v];
267 val = above[oL][p - 1] * below[oL + 1][ch - 1];
268 }
269 w[r] = val;
270 sum += val;
271 }
272 pik[k].assign(rows.size(), num_traits<T>::from_int(0));
273 for (std::size_t r = 0; r < rows.size(); ++r)
274 pik[k][r] = num_traits<T>::from_double(w[r] / sum);
275 }
276 return pik;
277}
278
279/**
280 * ComputeAs(k): A_k^e from A_{k+1}^e, the "from above" contribution (Fig. 3).
281 *
282 * The adjust denominator Pr{p[v]} is the FROM-ABOVE marginal of the child node,
283 * Pr{p} = sum over parents of pi_{k+1}. Using it (rather than the level-k CTMC
284 * marginal, which only equals it at convergence) makes adjust a proper
285 * conditional Pr{(parent,arc)|child} and pins the inter-level node marginals,
286 * removing the spurious fixed points.
287 */
288template <class T>
289std::vector<std::vector<std::vector<T>>> mcd_compute_as(
290 std::size_t k, const std::vector<std::vector<std::vector<T>>>& Aup,
291 const std::vector<std::vector<std::vector<int>>>& Pnode,
292 const std::vector<std::vector<T>>& pik, const std::vector<std::vector<MddLocalMatrix<T>>>& W,
293 const std::vector<std::vector<std::pair<int, int>>>& Mrows, const std::vector<int>& nn,
294 std::size_t E) {
295 const T zero = num_traits<T>::from_int(0);
296 const std::vector<std::pair<int, int>>& rows1 = Mrows[k + 1];
297 std::vector<T> pr_above(static_cast<std::size_t>(nn[k]), zero);
298 for (std::size_t r = 0; r < rows1.size(); ++r) {
299 const int child = Pnode[k + 1][rows1[r].first - 1][rows1[r].second];
300 if (child > 0) pr_above[child - 1] += pik[k + 1][r];
301 }
302 std::vector<std::vector<std::vector<T>>> Ak(
303 E, std::vector<std::vector<T>>(static_cast<std::size_t>(nn[k]),
304 std::vector<T>(static_cast<std::size_t>(nn[k]), zero)));
305 for (std::size_t r = 0; r < rows1.size(); ++r) {
306 const int p = rows1[r].first;
307 const int v = rows1[r].second;
308 const int childp = Pnode[k + 1][p - 1][v]; // p[v]: node at level k
309 if (childp <= 0 || !(pr_above[childp - 1] > zero)) continue;
310 const T adjust = T(pik[k + 1][r] / pr_above[childp - 1]);
311 for (std::size_t e = 0; e < E; ++e) {
312 const std::vector<std::size_t>& wcols = W[e][k + 1].cols[v];
313 if (wcols.empty()) continue;
314 const std::vector<T>& wvals = W[e][k + 1].vals[v];
315 const std::vector<T>& arow = Aup[e][p - 1];
316 for (std::size_t wi = 0; wi < wcols.size(); ++wi) {
317 const std::size_t w = wcols[wi];
318 const T wv = wvals[wi];
319 for (std::size_t q = 0; q < arow.size(); ++q) {
320 if (arow[q] == zero) continue;
321 const int childq = Pnode[k + 1][q][w];
322 if (childq <= 0) continue; // q[w] null
323 Ak[e][childp - 1][childq - 1] += T(arow[q] * wv * adjust);
324 }
325 }
326 }
327 }
328 return Ak;
329}
330
331/**
332 * ComputeMC(k): level-k rate matrix (Eq. 6),
333 * R_k^e[(p,i),(q,j)] = A_k^e[p,q] * W_k^e[i,j] * b_{k-1}^e[p[i]].
334 */
335template <class T>
336std::vector<std::vector<T>> mcd_compute_mc(
337 std::size_t k, const std::vector<std::vector<std::vector<T>>>& Ak,
338 const std::vector<std::vector<std::vector<T>>>& bcell,
339 const std::vector<std::vector<std::vector<int>>>& Pnode,
340 const std::vector<std::vector<MddLocalMatrix<T>>>& W,
341 const std::vector<std::vector<std::pair<int, int>>>& Mrows,
342 const std::vector<std::vector<int>>& Midx, const std::vector<std::size_t>& level_sizes,
343 const std::vector<int>& dom, std::size_t E) {
344 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
345 const std::size_t nm = level_sizes[k];
346 const std::vector<std::pair<int, int>>& rows = Mrows[k];
347 std::vector<std::vector<T>> Rk(nm, std::vector<T>(nm, zero));
348 for (std::size_t r = 0; r < nm; ++r) {
349 const int p = rows[r].first;
350 const int v = rows[r].second;
351 for (std::size_t e = 0; e < E; ++e) {
352 const std::vector<std::size_t>& wcols = W[e][k].cols[v];
353 if (wcols.empty()) continue;
354 T bfac = one; // terminal ONE
355 if (k > 0) bfac = bcell[k - 1][Pnode[k][p - 1][v] - 1][e];
356 if (bfac == zero) continue;
357 const std::vector<T>& wvals = W[e][k].vals[v];
358 const std::vector<T>& arow = Ak[e][p - 1];
359 for (std::size_t wi = 0; wi < wcols.size(); ++wi) {
360 const std::size_t w = wcols[wi];
361 const T wv = wvals[wi];
362 for (std::size_t q = 0; q < arow.size(); ++q) {
363 if (arow[q] == zero) continue;
364 const int di = Midx[k][q * static_cast<std::size_t>(dom[k]) + w];
365 if (di == 0) continue; // (q,w) not in M_k
366 Rk[r][di - 1] += T(arow[q] * wv * bfac);
367 }
368 }
369 }
370 }
371 return Rk;
372}
373
374} // namespace detail
375
376/**
377 * Approximate stationary measures by decision-diagram-guided aggregation.
378 *
379 * @param mdds the reachable set, in MDD orientation
380 * @param desc the Kronecker rate descriptor
381 * @param options level-iteration knobs
382 */
383template <class T>
385 const MddMcdOptions& options = MddMcdOptions()) {
386 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
387 const std::size_t K = mdds.K;
388 if (K == 0) throw InputError("mdd_mcd: the diagram has no levels");
389
390 // ---- paper orientation: paper level k <-> MDD level K-1-k = station K-1-k
391 std::vector<std::vector<std::vector<int>>> Pnode(K);
392 std::vector<int> nn(K, 0), dom(K, 0);
393 for (std::size_t k = 0; k < K; ++k) {
394 const std::size_t oL = K - 1 - k;
395 Pnode[k] = mdds.node[oL];
396 nn[k] = mdds.nnodes[oL];
397 dom[k] = mdds.domain[oL];
398 }
399
400 // ---- per (event, paper level) local matrices W_k^e; an untouched level
401 // carries the identity, which is supplied here rather than stored
402 const std::size_t E = desc.events.size();
403 std::vector<std::vector<MddLocalMatrix<T>>> W(E, std::vector<MddLocalMatrix<T>>(K));
404 std::vector<std::vector<bool>> touched(E, std::vector<bool>(K, false));
405 for (std::size_t e = 0; e < E; ++e) {
406 const MddEvent<T>& ev = desc.events[e];
407 for (std::size_t t = 0; t < ev.lev.size(); ++t) {
408 const std::size_t pk = K - 1 - ev.lev[t]; // station level -> paper level
409 W[e][pk] = ev.W[t];
410 touched[e][pk] = true;
411 }
412 for (std::size_t k = 0; k < K; ++k)
413 if (!touched[e][k])
414 W[e][k] = MddLocalMatrix<T>::identity(static_cast<std::size_t>(dom[k]));
415 }
416
417 // ---- level-k CTMC state sets M_k = {(p, v) : arc p[v] non-null}
418 std::vector<std::vector<std::pair<int, int>>> Mrows(K);
419 std::vector<std::vector<int>> Midx(K);
420 std::vector<std::size_t> level_sizes(K, 0);
421 for (std::size_t k = 0; k < K; ++k) {
422 // column-major order, matching the MATLAB find() the reference uses
423 for (int v = 0; v < dom[k]; ++v) {
424 for (int p = 0; p < nn[k]; ++p) {
425 const int ch = Pnode[k][p][v];
426 const bool live = (k == 0) ? (ch == TERM_TRUE) : (ch > 0);
427 if (live) Mrows[k].push_back(std::make_pair(p + 1, v));
428 }
429 }
430 level_sizes[k] = Mrows[k].size();
431 Midx[k].assign(static_cast<std::size_t>(nn[k]) * static_cast<std::size_t>(dom[k]), 0);
432 for (std::size_t r = 0; r < Mrows[k].size(); ++r)
433 Midx[k][static_cast<std::size_t>(Mrows[k][r].first - 1) *
434 static_cast<std::size_t>(dom[k]) +
435 static_cast<std::size_t>(Mrows[k][r].second)] = static_cast<int>(r + 1);
436 }
437
438 // ---- initialise level stationary vectors and node marginals
439 std::vector<std::vector<double>> above, below;
440 detail::mcd_path_counts(mdds, K, above, below);
441 std::vector<std::vector<T>> pik;
442 if (!options.initpik.empty()) {
443 pik.assign(K, std::vector<T>());
444 for (std::size_t k = 0; k < K; ++k) {
445 pik[k].assign(options.initpik[k].size(), zero);
446 for (std::size_t r = 0; r < options.initpik[k].size(); ++r)
447 pik[k][r] = num_traits<T>::from_double(options.initpik[k][r]);
448 }
449 } else {
450 pik = detail::mcd_uniform_init<T>(mdds, Mrows, K, above, below);
451 }
452 std::vector<std::vector<T>> Prp(K);
453 for (std::size_t k = 0; k < K; ++k) Prp[k] = detail::mcd_node_marginal(Mrows[k], pik[k], nn[k]);
454
455 // ---- fixed-point iteration (Fig. 3, procedure Solve)
456 int iters = 0;
457 bool converged = false;
458 double delta = 0;
459 for (int it = 1; it <= options.maxiter; ++it) {
460 iters = it;
461 const std::vector<std::vector<T>> piold = pik;
462
463 // ComputeBs, bottom-up:
464 // b_k^e[p] = sum_v Pr{v|p} * b_{k-1}^e[p[v]] * lambda
465 std::vector<std::vector<std::vector<T>>> bcell(K);
466 for (std::size_t k = 0; k < K; ++k) {
467 std::vector<std::vector<T>> bk(static_cast<std::size_t>(nn[k]),
468 std::vector<T>(E, zero));
469 const std::vector<std::pair<int, int>>& rows = Mrows[k];
470 for (std::size_t r = 0; r < rows.size(); ++r) {
471 const int p = rows[r].first;
472 const int v = rows[r].second;
473 if (!(Prp[k][p - 1] > zero)) continue;
474 const T adjust = T(pik[k][r] / Prp[k][p - 1]); // Pr{v|p}
475 for (std::size_t e = 0; e < E; ++e) {
476 const T le = W[e][k].row_sum[v];
477 if (le == zero) continue; // not locally enabled
478 T down = one; // terminal ONE
479 if (k > 0) down = bcell[k - 1][Pnode[k][p - 1][v] - 1][e];
480 bk[p - 1][e] += T(adjust * down * le);
481 }
482 }
483 bcell[k] = bk;
484 }
485
486 // top-down: ComputeAs(k) then SolveLevel(k)
487 std::vector<std::vector<std::vector<std::vector<T>>>> Acell(K);
488 Acell[K - 1].assign(E, std::vector<std::vector<T>>());
489 for (std::size_t e = 0; e < E; ++e)
490 Acell[K - 1][e] = detail::mcd_identity<T>(static_cast<std::size_t>(nn[K - 1]));
491 for (std::size_t kk = K; kk > 0; --kk) {
492 const std::size_t k = kk - 1;
493 if (k + 1 < K)
494 Acell[k] = detail::mcd_compute_as(k, Acell[k + 1], Pnode, pik, W, Mrows, nn, E);
495 // SolveLevel(k): assemble R_k (Eq. 6), solve pi_k Q_k = 0
496 const std::vector<std::vector<T>> Rk =
497 detail::mcd_compute_mc(k, Acell[k], bcell, Pnode, W, Mrows, Midx, level_sizes,
498 dom, E);
499 pik[k] = detail::mcd_solve_stat(detail::mcd_generator(Rk));
500 Prp[k] = detail::mcd_node_marginal(Mrows[k], pik[k], nn[k]);
501 }
502
503 // A diverged iterate must not be read as converged, which is what a
504 // NaN-skipping maximum would do.
505 delta = 0;
506 for (std::size_t k = 0; k < K; ++k) {
507 double dk = 0;
508 for (std::size_t r = 0; r < pik[k].size(); ++r) {
509 const double d = num_traits<T>::to_double(T(pik[k][r] - piold[k][r]));
510 dk = std::max(dk, d < 0 ? -d : d);
511 }
512 if (std::isnan(dk) || std::isinf(dk))
513 throw NumericError("mdd_mcd: level " + std::to_string(k + 1) +
514 " iterate is not finite at iteration " + std::to_string(it) +
515 "; the level CTMC did not yield a proper stationary vector.");
516 delta = std::max(delta, dk);
517 }
518 if (delta < options.tol) {
519 converged = true;
520 break;
521 }
522 }
523 if (!converged)
524 throw NumericError("mdd_mcd: the coupled level iteration did not converge in " +
525 std::to_string(options.maxiter) + " sweeps (last change " +
526 std::to_string(delta) + " against tol " + std::to_string(options.tol) +
527 "); the level marginals returned would not be a fixed point. Raise "
528 "maxiter or relax tol.");
529
530 // ---- performance measures from the per-level marginals. QLen is the mean
531 // local value and is defined for any descriptor (jobs at a station, tokens
532 // in a place); X and U need the queueing parameters.
533 const bool is_qn = !desc.mu.empty() && !desc.servers.empty();
534 MddMcdResult<T> out;
535 out.QLen.assign(K, zero);
536 if (is_qn) {
537 out.X.assign(K, zero);
538 out.U.assign(K, zero);
539 }
540 for (std::size_t s = 0; s < K; ++s) {
541 const std::size_t k = K - 1 - s; // paper level of station s
542 const std::vector<std::pair<int, int>>& rows = Mrows[k];
543 T q = zero, busy = zero;
544 for (std::size_t r = 0; r < rows.size(); ++r) {
545 const double vd = desc.valuemap.empty()
546 ? static_cast<double>(rows[r].second)
547 : desc.valuemap[s][static_cast<std::size_t>(rows[r].second)];
548 const T v = num_traits<T>::from_double(vd);
549 q += T(v * pik[k][r]);
550 if (is_qn) {
551 const double srv = desc.servers[s];
552 const T cap = num_traits<T>::from_double(vd < srv ? vd : srv);
553 busy += T(cap * pik[k][r]);
554 }
555 }
556 out.QLen[s] = q;
557 if (is_qn) {
558 out.X[s] = T(desc.mu[s] * busy);
559 out.U[s] = std::isinf(desc.servers[s])
560 ? q
561 : T(busy / num_traits<T>::from_double(desc.servers[s]));
562 }
563 }
564
565 // The level chains are coupled only through rates, so nothing in the
566 // iteration forces the marginals to describe the same population; a fixed
567 // point that does not is a wrong answer, not an approximation, and must not
568 // be returned. The test is a conservation law of the model: the closed
569 // population for a QN, a place invariant w'*m = const for a net.
570 std::vector<double> winv = desc.invariant_weights;
571 double vinv = desc.invariant_value;
572 if (winv.empty() && desc.N > 0) {
573 winv.assign(K, 1.0); // closed QN: total population
574 vinv = desc.N;
575 }
576 if (!winv.empty()) {
577 double got = 0;
578 for (std::size_t s = 0; s < K; ++s)
579 got += winv[s] * num_traits<T>::to_double(out.QLen[s]);
580 const double scale = std::max(1.0, std::fabs(vinv));
581 if (std::fabs(got - vinv) > 1e-6 * scale)
582 throw NumericError("mdd_mcd: the level marginals converged to an invariant value of " +
583 std::to_string(got) + " against the model value " +
584 std::to_string(vinv) + ", so the fixed point reached is degenerate "
585 "(the level chains are mutually inconsistent). Supply initpik with "
586 "a consistent starting law.");
587 }
588
589 out.pik = pik;
590 out.Mrows = Mrows;
591 out.level_sizes = level_sizes;
592 out.iters = iters;
593 out.paths_per_level.assign(K, 1.0);
594 bool no_agg = true;
595 for (std::size_t k = 0; k < K; ++k) {
596 const std::size_t oL = K - 1 - k;
597 double mx = 1.0;
598 for (std::size_t p = 0; p < above[oL].size(); ++p) mx = std::max(mx, above[oL][p]);
599 out.paths_per_level[k] = mx;
600 if (mx > 1.0 + 1e-12) no_agg = false;
601 }
602 out.no_aggregation = no_agg;
603 return out;
604}
605
606} // namespace mdd
607} // namespace line
608
609#endif // LINE_API_MDD_MDD_MCD_H
InputError(const std::string &what)
Definition error.h:39
NumericError(const std::string &what)
Definition error.h:45
The exception types the port throws.
Least squares for a rectangular system, exact-capable.
Dense matrix and non-owning view.
Quasi-reduced ordered Multi-valued Decision Diagram.
The rate side of the decision-diagram domain: local matrices, events, the Kronecker descriptor,...
MddMcdResult< T > mdd_mcd(const MddStruct &mdds, const MddDescriptor< T > &desc, const MddMcdOptions &options=MddMcdOptions())
Approximate stationary measures by decision-diagram-guided aggregation.
Definition mdd_mcd.h:384
const int TERM_TRUE
Terminal node "1": a completed path is accepted.
Definition mdd.h:48
double dot(const std::vector< double > &a, const std::vector< double > &b)
The inner product of a row vector with a column held as a vector.
Definition mg1.h:236
LstsqResult< T > lstsq(const Matrix< T > &A, const std::vector< T > &b, const T &tol)
Least-squares solution of A x = b, minimum-norm when A is rank deficient.
Definition lstsq.h:152
Number-type abstraction for the templated API port.
Kronecker rate descriptor of a structured model, the input of mdd_mcd.
Definition mdd_types.h:165
std::vector< std::vector< double > > valuemap
valuemap[i][idx] is the physical occupancy of level i in local state idx.
Definition mdd_types.h:187
std::vector< double > servers
Servers per station; infinite for a delay station.
Definition mdd_types.h:175
int N
Closed population; the conservation law the level marginals must satisfy.
Definition mdd_types.h:169
std::vector< T > mu
Station service rates, 1/E[S]; empty for a descriptor with no queueing parameters.
Definition mdd_types.h:173
std::vector< double > invariant_weights
Optional conservation law as weights' * QLen = value, overriding the closed-population test.
Definition mdd_types.h:198
double invariant_value
Value of the invariant when invariant_weights is set.
Definition mdd_types.h:200
std::vector< MddEvent< T > > events
The events of the descriptor.
Definition mdd_types.h:193
One event of the Kronecker rate descriptor.
Definition mdd_types.h:124
std::vector< MddLocalMatrix< T > > W
Local matrices at the levels named by lev.
Definition mdd_types.h:132
std::vector< std::size_t > lev
Levels the event touches, as 0-based level indices, aligned with W.
Definition mdd_types.h:130
A local rate matrix W_k^e of the Kronecker descriptor, held row-compressed.
Definition mdd_types.h:46
static MddLocalMatrix< T > identity(std::size_t d)
The identity of the given order, used for a level an event does not touch.
Definition mdd_types.h:59
Knobs of the level iteration in mdd_mcd.
Definition mdd_types.h:212
Result of the Miner-Ciardo-Donatelli level aggregation.
Definition mdd_types.h:229
std::vector< std::size_t > level_sizes
|M_k| per paper level.
Definition mdd_types.h:241
bool no_aggregation
True certifies the result is EXACT with no reference solve needed; false means "not certified by this...
Definition mdd_types.h:255
std::vector< std::vector< T > > pik
pik[k] is the level-k stationary vector over M_k, in paper orientation.
Definition mdd_types.h:237
std::vector< std::vector< std::pair< int, int > > > Mrows
Mrows[k][r] = {node id, local value} of row r of M_k.
Definition mdd_types.h:239
int iters
Fixed-point iterations performed.
Definition mdd_types.h:243
std::vector< T > QLen
Mean occupancy per station (or place), in station order.
Definition mdd_types.h:231
std::vector< double > paths_per_level
max |A(p)| per paper level: the largest number of distinct root-to-node paths at that level.
Definition mdd_types.h:249
std::vector< T > X
Per-station throughput; empty when the descriptor carries no queueing parameters.
Definition mdd_types.h:233
std::vector< T > U
Per-station utilization; empty when the descriptor carries no queueing parameters.
Definition mdd_types.h:235
Plain-array export of an MDD, the input contract of mdd_mcd.
Definition mdd.h:57
std::vector< std::vector< std::vector< int > > > node
node[k][p][v] is the child of arc v of level-k node id p+1: a level-(k+1) node id when k < K-1,...
Definition mdd.h:70
std::vector< int > domain
domain[k] is the number of local states at level k.
Definition mdd.h:61
std::vector< int > nnodes
nnodes[k] is the live node count at level k.
Definition mdd.h:65
std::size_t K
Number of variable levels.
Definition mdd.h:59