LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_mam_traffic.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_SOLVERS_MAM_SOLVER_MAM_TRAFFIC_H
6#define LINE_SOLVERS_MAM_SOLVER_MAM_TRAFFIC_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `solver_mam_traffic.m` and `solver_mam_traffic_mmap.m`: the traffic
12 * step of the `dec.mmap` decomposition.
13 *
14 * WHAT IT COMPUTES. Given the DEPARTURE process of every class at every station
15 * as a (D0,D1) pair, it produces the ARRIVAL process seen by every node. Each
16 * station's per-class departures are superposed into one marked MAP, that MAP
17 * is split along the routing (`npfqn_traffic_split_cs`, which also carries the
18 * class switching), and the per-link flows arriving at a node are superposed
19 * back (`npfqn_traffic_merge`). The result is the per-link traffic descriptor
20 * table the outer fixed point of `solver_mam.m` iterates on.
21 *
22 * IT IS AN APPROXIMATION, and the approximation is not incidental. The exact
23 * superposition of n marked MAPs is the Kronecker sum, whose order is the
24 * PRODUCT of the operand orders, so a network of any size exhausts memory after
25 * a handful of merges. Both the per-station class superposition here and
26 * `npfqn_traffic_merge` therefore compress back to a bounded representation
27 * (`mmap_compress`, an APH(2) mixture fit) whenever the order passes
28 * `config.space_max`. Compression preserves the class probabilities and the
29 * first three backward moments per class; it does NOT preserve the correlation
30 * structure, and it does not preserve the per-class rates exactly. Nothing here
31 * may be compared against an exact solver for equality: the descriptors are
32 * moment-matched surrogates of the true superposed processes. What survives the
33 * compression exactly is the SPLIT: `npfqn_traffic_split_cs` is linear in the
34 * routing probabilities, so first-moment conservation across a split, and the
35 * whole traffic table of a network that never trips `space_max` and never
36 * merges more than one flow into a node, are identities.
37 *
38 * INDEXING. The reference works in an indexing over the NON-CLASS-SWITCH nodes
39 * ("NCS"): the class-switch nodes are eliminated by taking the stochastic
40 * complement of `sn.rtnodes` over the rows of the surviving nodes, so a switch
41 * shows up only as a class change on the edges around it. `ARV` is returned
42 * NODE-indexed and of length `nnodes` -- the reference preallocates it as
43 * `cell(Inc,1)` and then assigns `ARV{ind}` at node indices, so MATLAB grows it
44 * to `I`; the preallocation size is dead. An entry of order 0 is MATLAB's `[]`:
45 * a class-switch node, or a Source, which has no arrivals to describe.
46 *
47 * REFERENCE DEFECT in `solver_mam_traffic.m`, line 104. When a node has no
48 * incoming flow above `FineTol` the fallback reads `ARV{ind} = LINKS{jnd,1}`.
49 * `jnd` is the loop variable left behind by the `for jnd=1:Inc` above it, so it
50 * is `Inc`, and the column is the literal 1 rather than `inc`: the node is
51 * handed the link from the LAST non-class-switch node to the FIRST one, which
52 * is a flow between two other nodes entirely, or `[]` when that link was never
53 * built. The FJ variant of the same file (`solver_mam_traffic_mmap.m`, lines
54 * 179-188) writes the intended form -- any non-empty link INTO this node, and a
55 * zero-rate MMAP when there is none -- and that is what this port does for both
56 * entry points. Propagating the defect would attribute one node's traffic to
57 * another, which no downstream consumer could detect.
58 *
59 * WHAT IS REFUSED BY NAME. The reference's node switch has a branch only for
60 * Source, Delay and Queue (plus Fork and Join in the FJ variant). Any other
61 * node that a flow passes THROUGH -- a Router, a Logger, a Cache, a Place, a
62 * Transition, a Region -- silently emits no outgoing link there, so everything
63 * downstream of it is described as receiving no traffic at all. That is not a
64 * conservative approximation, it is a wrong answer with no symptom, so such a
65 * model is refused by name rather than reproduced. A Sink is not refused: it
66 * genuinely has no departures.
67 *
68 * WHAT IS NOT PORTED. `sn_build_fj_sync_map` and `mmap_max` have no home in
69 * this tree yet -- the first belongs in `line/api/fj/`, the second in
70 * `line/api/mam/` next to the rest of the M3A MMAP algebra -- and the FJ
71 * variant is unusable without both. They are transcribed here under their
72 * MATLAB names, exactly as `npfqn_traffic_split_cs.h` inlines `mmap_normalize`
73 * for the same reason, and should be lifted into their own headers when one is
74 * added. Neither is re-derived: both are line-by-line transcriptions.
75 *
76 * ARITHMETIC. The merge, the split and the synchronization are Kronecker
77 * algebra and are exact in any field. Compression is not: `mmap_compress` fits
78 * an APH(2) and needs square roots, so every call to it is behind the same
79 * compile-time gate `npfqn_traffic_merge` uses, and the exact instantiation
80 * refuses at run time only when compression is actually reached.
81 */
82
83#include <cstddef>
84#include <map>
85#include <string>
86#include <utility>
87#include <vector>
88
97#include "line/util/error.h"
98#include "line/util/linalg.h"
99#include "line/util/matrix.h"
100
101namespace line {
102namespace mam {
103
104/** `DEP{i,r}`, the departure process of class r from i in (D0,D1) form. */
105template <class T>
106using DepTable = std::vector<std::vector<Map<T>>>;
107
108/**
109 * The fields of `options.config` the traffic step reads.
110 *
111 * `merge` carries MATLAB's `config.merge` and `config.compress` together, since
112 * `npfqn_traffic_merge` already owns both.
113 */
115 std::size_t space_max = 128;
116 /** `config.fj_sync_q_len`, the join's synchronization queue; MATLAB's default. */
117 std::size_t fj_sync_q_len = 2;
119};
120
121/** The traffic step's view of `SolverOptions('MAM')`. */
124 c.space_max = opt.space_max;
125 return c;
126}
127
128/**
129 * `sn_build_fj_sync_map`: which incoming flows at a Join must be synchronized.
130 *
131 * `node_sync[j][i]` is the sync group of the flow from node i into node j, with
132 * 0 meaning an independent flow. Both indices are 0-based over nodes; group ids
133 * are 1-based, as in the reference.
134 */
135struct FjSyncMap {
136 std::vector<std::vector<std::size_t>> node_sync;
137 std::vector<std::size_t> fork_of_group; ///< 1-based node index, by group
138 std::vector<std::size_t> join_of_group; ///< 1-based node index, by group
139 std::size_t ngroups = 0;
140};
141
142namespace traffic_detail {
143
144/**
145 * `npfqn_traffic_split_cs` takes the MMAP as MATLAB's flat cell and the rest of
146 * the port takes `mam::Mmap`, so the two representations have to meet here.
147 */
148template <class T>
149npfqn::Mmap<T> to_flat(const Mmap<T>& m) {
151 v.reserve(2 + m.classes());
152 v.push_back(m.D0);
153 v.push_back(m.D1);
154 for (const Matrix<T>& Dc : m.Dc) v.push_back(Dc);
155 return v;
156}
157
158template <class T>
159Mmap<T> from_flat(const npfqn::Mmap<T>& v) {
160 if (v.size() < 3) throw InputError("solver_mam_traffic: a split flow carries no marking");
161 Mmap<T> m;
162 m.D0 = v[0];
163 m.D1 = v[1];
164 m.Dc.assign(v.begin() + 2, v.end());
165 return m;
166}
167
168/** See `npfqn_traffic_merge`'s compress_or_refuse: the same gate, named here. */
169template <class T>
170Mmap<T> compress_or_refuse(const Mmap<T>& m) {
171 if constexpr (num_traits<T>::has_transcendental) {
173 } else {
174 throw UnsupportedError(
175 "solver_mam_traffic: the superposition passed config.space_max and must be compressed, "
176 "which needs transcendental arithmetic (aph2_fit); raise space_max or run at double");
177 }
178}
179
180/** An MMAP of `nclasses` classes that never fires, MATLAB's `{[0],[0],[0]}`. */
181template <class T>
182Mmap<T> silent_mmap(std::size_t nclasses) {
183 const T zero = num_traits<T>::from_int(0);
184 Mmap<T> m;
185 m.D0 = Matrix<T>(1, 1, zero);
186 m.D1 = Matrix<T>(1, 1, zero);
187 m.Dc.assign(nclasses, Matrix<T>(1, 1, zero));
188 return m;
189}
190
191/**
192 * One cell of DEP as a single-class MMAP.
193 *
194 * MATLAB replaces an empty or NaN-carrying entry by the silent MMAP and
195 * otherwise sets `D1^(1) = D1`, i.e. reads the renewal departure process as an
196 * MMAP with one class. The NaN test is the reference's `any(any(isnan(D0)))`.
197 */
198template <class T>
199Mmap<T> dep_cell(const DepTable<T>& DEP, std::size_t i, std::size_t r) {
200 if (i >= DEP.size() || r >= DEP[i].size()) return silent_mmap<T>(1);
201 const Map<T>& d = DEP[i][r];
202 if (d.order() == 0) return silent_mmap<T>(1);
203 for (std::size_t a = 0; a < d.D0.rows(); ++a)
204 for (std::size_t b = 0; b < d.D0.cols(); ++b)
205 if (npfqn::detail::num_isnan(d.D0(a, b))) return silent_mmap<T>(1);
206 Mmap<T> m;
207 m.D0 = d.D0;
208 m.D1 = d.D1;
209 m.Dc.assign(1, d.D1);
210 return m;
211}
212
213/**
214 * `sum(mmap_lambda(.))`, MATLAB's test for a link that carries traffic.
215 *
216 * A link whose D1 is identically zero is short-circuited to zero. Its rate is
217 * zero by inspection -- lambda_c = theta D1^(c) e -- and computing theta would
218 * mean solving a singular 0 x = 0, which is exactly the shape the silent MMAP
219 * has. This is not a deviation: it is the same number, obtained without the
220 * degenerate solve.
221 */
222template <class T>
223T total_rate(const Mmap<T>& m) {
224 const T zero = num_traits<T>::from_int(0);
225 bool silent = true;
226 for (std::size_t i = 0; i < m.D1.rows() && silent; ++i)
227 for (std::size_t j = 0; j < m.D1.cols(); ++j)
228 if (!(m.D1(i, j) == zero)) {
229 silent = false;
230 break;
231 }
232 if (silent) return zero;
233 T s = zero;
234 for (const T& v : mmap_lambda(m)) s += v;
235 return s;
236}
237
238/** The node types the reference's switch has no branch for; see the header note. */
239template <class T>
240void reject_opaque_node(const qn::NetworkStruct<T>& sn, std::size_t a, bool fj_aware) {
241 const qn::NodeType ty = sn.nodes[a].nodetype;
242 const char* what = nullptr;
243 switch (ty) {
244 case qn::NodeType::Router: what = "Router"; break;
245 case qn::NodeType::Logger: what = "Logger"; break;
246 case qn::NodeType::Cache: what = "Cache"; break;
247 case qn::NodeType::Place: what = "Place"; break;
248 case qn::NodeType::Transition: what = "Transition"; break;
249 case qn::NodeType::Region: what = "Region"; break;
250 case qn::NodeType::Fork:
251 case qn::NodeType::Join:
252 if (fj_aware) return;
253 throw UnsupportedError(
254 std::string("solver_mam_traffic: node '") + sn.nodes[a].name +
255 "' is a Fork or a Join, which the plain traffic step has no branch for; the "
256 "fork-join variant is solver_mam_traffic_mmap, which synchronizes the join with "
257 "mmap_max");
258 default: return;
259 }
260 throw UnsupportedError(std::string("solver_mam_traffic: node '") + sn.nodes[a].name + "' is a " +
261 what +
262 " node. The reference's node switch has no branch for it, so it emits "
263 "no outgoing link and every node downstream of it is described as "
264 "receiving no traffic at all; that is a wrong traffic table with no "
265 "symptom, not a conservative approximation");
266}
267
268} // namespace traffic_detail
269
270/**
271 * `mmap_max(MMAPa, MMAPb, k)`: the synchronization of two flows through a join
272 * with a queue of length k on each side.
273 *
274 * The phase space is (phase of a) x (phase of b) x (lead), with `lead` running
275 * over 2k+1 blocks: block 0 is "both streams matched", the ODD blocks 1..2k-1
276 * are "a is ahead by 1..k" and the EVEN blocks 2..2k are "b is ahead by 1..k".
277 * A stream that is k ahead is BLOCKED, which is why the two extreme diagonal
278 * blocks carry only the other stream's hidden generator. An arrival is emitted
279 * exactly when the lagging stream catches up, i.e. on every transition that
280 * moves the lead towards 0, so the join's throughput is bounded by the slower
281 * of the two inputs and falls short of it by the blocking probability.
282 *
283 * Transcription of matlab/lib/m3a/m3a/mmap/mmap_max.m. It belongs in
284 * `line/api/mam/`; see the header note on why it is here.
285 */
286template <class T>
287Mmap<T> mmap_max(const Mmap<T>& a, const Mmap<T>& b, std::size_t k) {
288 if (k == 0) throw InputError("mmap_max: the synchronization queue must hold at least one job");
289 if (a.classes() != b.classes())
290 throw InputError("mmap_max: the two flows carry different numbers of classes");
291 const T zero = num_traits<T>::from_int(0);
292 const std::size_t na = a.order(), nbp = b.order(), n = na * nbp, nblk = 1 + 2 * k;
293 const Matrix<T> Ia = eye<T>(na), Ib = eye<T>(nbp);
294 const Matrix<T> A0B0 = krons(a.D0, b.D0);
295 const Matrix<T> A1IB = kron(a.D1, Ib);
296 const Matrix<T> IAB1 = kron(Ia, b.D1);
297 const Matrix<T> IAB0 = kron(Ia, b.D0);
298 const Matrix<T> A0IB = kron(a.D0, Ib);
299
300 auto put = [n](Matrix<T>& M, std::size_t rb, std::size_t cb, const Matrix<T>& B) {
301 for (std::size_t i = 0; i < B.rows(); ++i)
302 for (std::size_t j = 0; j < B.cols(); ++j) M(rb * n + i, cb * n + j) = B(i, j);
303 };
304
305 Matrix<T> M0(n * nblk, n * nblk, zero);
306 put(M0, 0, 0, A0B0);
307 put(M0, 0, 1, A1IB);
308 put(M0, 0, 2, IAB1);
309 for (std::size_t blk = 1; blk + 2 <= 2 * k; ++blk) put(M0, blk, blk, A0B0);
310 put(M0, 2 * k - 1, 2 * k - 1, IAB0);
311 put(M0, 2 * k, 2 * k, A0IB);
312 for (std::size_t i = 2; i <= k; ++i) {
313 const std::size_t rb = 1 + 2 * (i - 2), cb = 3 + 2 * (i - 2);
314 put(M0, rb, cb, A1IB);
315 put(M0, rb + 1, cb + 1, IAB1);
316 }
317
318 // The emission matrices all share one pattern; only the two operand blocks
319 // change, which is what makes sum_c Dc = D1 hold by linearity of kron.
320 auto emissions = [&](const Matrix<T>& bArr, const Matrix<T>& aArr) {
321 Matrix<T> M(n * nblk, n * nblk, zero);
322 put(M, 1, 0, bArr);
323 put(M, 2, 0, aArr);
324 for (std::size_t i = 2; i <= k; ++i) {
325 const std::size_t rb = 1 + 2 * (i - 1), cb = 1 + 2 * (i - 2);
326 put(M, rb, cb, bArr);
327 put(M, rb + 1, cb + 1, aArr);
328 }
329 return M;
330 };
331
332 Mmap<T> out;
333 out.D0 = M0;
334 out.D1 = emissions(IAB1, A1IB);
335 out.Dc.reserve(a.classes());
336 for (std::size_t c = 0; c < a.classes(); ++c)
337 out.Dc.push_back(emissions(kron(Ia, b.Dc[c]), kron(a.Dc[c], Ib)));
338 return out;
339}
340
341namespace traffic_detail {
342
343/** The NCS bookkeeping both entry points open with. */
344struct NcsIndex {
345 std::vector<bool> is_ncs; ///< by 0-based node
346 std::vector<std::size_t> to_ncs; ///< 1-based NCS index, 0 for a class switch
347 std::vector<std::size_t> keep; ///< 0-based rtnodes rows of the surviving nodes
348 std::size_t inc_count = 0;
349};
350
351template <class T>
352NcsIndex build_ncs_index(const qn::NetworkStruct<T>& sn) {
353 const std::size_t I = sn.nof_nodes(), R = sn.nclasses;
354 NcsIndex x;
355 x.is_ncs.assign(I, false);
356 x.to_ncs.assign(I, 0);
357 for (std::size_t a = 0; a < I; ++a) {
358 if (sn.nodes[a].nodetype == qn::NodeType::ClassSwitch) continue;
359 x.is_ncs[a] = true;
360 x.to_ncs[a] = ++x.inc_count;
361 for (std::size_t r = 0; r < R; ++r) x.keep.push_back(a * R + r);
362 }
363 return x;
364}
365
366/** The per-station (or per-node) class superposition, bounded by space_max. */
367template <class T>
368Mmap<T> superpose_classes(const DepTable<T>& DEP, std::size_t row, std::size_t R,
369 const TrafficConfig& config) {
370 Mmap<T> d = dep_cell(DEP, row, 0);
371 for (std::size_t r = 1; r < R; ++r) {
372 d = mmap_super(d, dep_cell(DEP, row, r));
373 if (d.order() > config.space_max) d = compress_or_refuse(d);
374 }
375 return d;
376}
377
378/** Row `inc` of the complemented routing, reshaped into split_cs's (R x Inc R). */
379template <class T>
380Matrix<T> split_probabilities(const Matrix<T>& rtncs, std::size_t inc, std::size_t Inc,
381 std::size_t R) {
382 Matrix<T> P(R, Inc * R, num_traits<T>::from_int(0));
383 for (std::size_t r = 0; r < R; ++r)
384 for (std::size_t jnc = 0; jnc < Inc; ++jnc)
385 for (std::size_t s = 0; s < R; ++s)
386 P(r, jnc * R + s) = rtncs(inc * R + r, jnc * R + s);
387 return P;
388}
389
390/**
391 * The reference's fallback for a node with no incoming flow, in the corrected
392 * form the FJ variant writes; see the header note on the line-104 defect.
393 */
394template <class T>
395Mmap<T> no_flow_arrival(const std::vector<std::vector<Mmap<T>>>& LINKS, std::size_t inc,
396 std::size_t R) {
397 for (std::size_t jnc = 0; jnc < LINKS.size(); ++jnc)
398 if (LINKS[jnc][inc].order() != 0) return LINKS[jnc][inc];
399 return silent_mmap<T>(R);
400}
401
402} // namespace traffic_detail
403
404/**
405 * `sn_build_fj_sync_map`.
406 *
407 * A node sits on the parallel path of a (Fork, Join) pair when the fork routes
408 * to it and it routes to the join, both for at least one class; every such node
409 * feeds one sync group at that join. Transcription of
410 * matlab/src/api/fj/sn_build_fj_sync_map.m; it belongs in `line/api/fj/`.
411 */
412template <class T>
414 const std::size_t I = sn.nof_nodes(), K = sn.nclasses;
415 const T zero = num_traits<T>::from_int(0);
416 FjSyncMap m;
417 m.node_sync.assign(I, std::vector<std::size_t>(I, 0));
418 for (const std::pair<std::size_t, std::size_t>& fjp : sn.fj) {
419 const std::size_t f = fjp.first - 1, j = fjp.second - 1;
420 const std::size_t gid = ++m.ngroups;
421 m.fork_of_group.push_back(f + 1);
422 m.join_of_group.push_back(j + 1);
423 for (std::size_t a = 0; a < I; ++a) {
424 if (a == f || a == j) continue;
425 bool from_fork = false, to_join = false;
426 for (std::size_t k = 0; k < K; ++k) {
427 if (sn.rtnodes(f * K + k, a * K + k) > zero) from_fork = true;
428 if (sn.rtnodes(a * K + k, j * K + k) > zero) to_join = true;
429 }
430 if (from_fork && to_join) m.node_sync[j][a] = gid;
431 }
432 }
433 return m;
434}
435
436/**
437 * Port of `solver_mam_traffic.m`.
438 *
439 * @param DEP station-indexed: `DEP[ist][r]` is the class-r departure process
440 * of station `ist` (0-based), an empty Map standing for MATLAB's
441 * `[]`, "this class does not depart from here"
442 * @param sn the refreshed network struct
443 * @param config the `options.config` fields the traffic analyzer reads
444 * @return node-indexed arrival descriptors of length `nnodes`; an entry
445 * of order 0 is MATLAB's `[]`
446 */
447template <class T>
448std::vector<Mmap<T>> solver_mam_traffic(const qn::NetworkStruct<T>& sn, const DepTable<T>& DEP,
449 const TrafficConfig& config) {
450 namespace td = traffic_detail;
451 const std::size_t I = sn.nof_nodes(), R = sn.nclasses;
452 if (R == 0) throw InputError("solver_mam_traffic: the model has no classes");
453
454 const td::NcsIndex x = td::build_ncs_index(sn);
455 const std::size_t Inc = x.inc_count;
456 const Matrix<T> rtncs = mc::dtmc_stochcomp(sn.rtnodes, x.keep);
457
458 // Outgoing flows: superpose each station's classes, then split along the
459 // routing. Only the branches of the reference's switch produce links.
460 std::vector<std::vector<Mmap<T>>> LINKS(Inc, std::vector<Mmap<T>>(Inc));
461 for (std::size_t a = 0; a < I; ++a) {
462 if (!x.is_ncs[a]) continue;
463 const qn::NodeType ty = sn.nodes[a].nodetype;
464 if (ty != qn::NodeType::Source && ty != qn::NodeType::Delay && ty != qn::NodeType::Queue) {
465 td::reject_opaque_node(sn, a, false);
466 continue;
467 }
468 const std::size_t inc = x.to_ncs[a] - 1;
469 const std::size_t ist = sn.nodes[a].station;
470 if (ist == 0)
471 throw InputError("solver_mam_traffic: node '" + sn.nodes[a].name +
472 "' serves jobs but carries no station index");
473 const Mmap<T> dep = td::superpose_classes(DEP, ist - 1, R, config);
474 const Matrix<T> Psplit = td::split_probabilities(rtncs, inc, Inc, R);
475 const std::vector<npfqn::Mmap<T>> F =
476 npfqn::npfqn_traffic_split_cs(td::to_flat(dep), Psplit);
477 for (std::size_t jnc = 0; jnc < Inc; ++jnc)
478 LINKS[inc][jnc] = mmap_normalize(td::from_flat(F[jnc]));
479 }
480
481 // Incoming flows: superpose the links that carry traffic into each node.
482 std::vector<Mmap<T>> ARV(I);
483 for (std::size_t a = 0; a < I; ++a) {
484 if (!x.is_ncs[a] || sn.nodes[a].nodetype == qn::NodeType::Source) continue;
485 const std::size_t inc = x.to_ncs[a] - 1;
486 std::vector<Mmap<T>> flows;
487 for (std::size_t jnc = 0; jnc < Inc; ++jnc) {
488 const Mmap<T>& lk = LINKS[jnc][inc];
489 if (lk.order() == 0) continue;
490 if (!(num_traits<T>::to_double(td::total_rate(lk)) > lang::GlobalConstants::FineTol))
491 continue;
492 flows.push_back(lk);
493 }
494 if (flows.size() > 1) ARV[a] = npfqn::npfqn_traffic_merge(flows, config.merge);
495 else if (flows.size() == 1) ARV[a] = flows[0];
496 else ARV[a] = td::no_flow_arrival(LINKS, inc, R);
497 }
498 return ARV;
499}
500
501/**
502 * Port of `solver_mam_traffic_mmap.m`, the fork-join aware traffic step.
503 *
504 * It differs from `solver_mam_traffic` in three places: DEP is NODE-indexed,
505 * Fork and Join nodes produce outgoing links, and the flows arriving at a join
506 * along one sync group are combined with `mmap_max` rather than superposed --
507 * a join fires when its slowest branch delivers, which superposition, being the
508 * union of the two point processes, would get badly wrong.
509 *
510 * @param DEP node-indexed: `DEP[ind][r]`, 0-based over nodes
511 * @param fjSyncMap from `sn_build_fj_sync_map`
512 * @param sn the refreshed network struct
513 * @param config the `options.config` fields the traffic analyzer reads
514 */
515template <class T>
516std::vector<Mmap<T>> solver_mam_traffic_mmap(const qn::NetworkStruct<T>& sn, const DepTable<T>& DEP,
517 const TrafficConfig& config,
518 const FjSyncMap& fjSyncMap) {
519 namespace td = traffic_detail;
520 const std::size_t I = sn.nof_nodes(), R = sn.nclasses;
521 if (R == 0) throw InputError("solver_mam_traffic_mmap: the model has no classes");
522 if (fjSyncMap.node_sync.size() != I)
523 throw InputError("solver_mam_traffic_mmap: the sync map is not indexed over the nodes");
524
525 const td::NcsIndex x = td::build_ncs_index(sn);
526 const std::size_t Inc = x.inc_count;
527 const Matrix<T> rtncs = mc::dtmc_stochcomp(sn.rtnodes, x.keep);
528
529 std::vector<std::vector<std::size_t>> syncNCS(Inc, std::vector<std::size_t>(Inc, 0));
530 for (std::size_t a = 0; a < I; ++a) {
531 if (!x.is_ncs[a]) continue;
532 for (std::size_t b = 0; b < I; ++b)
533 if (x.is_ncs[b] && fjSyncMap.node_sync[a][b] > 0)
534 syncNCS[x.to_ncs[a] - 1][x.to_ncs[b] - 1] = fjSyncMap.node_sync[a][b];
535 }
536
537 std::vector<std::vector<Mmap<T>>> LINKS(Inc, std::vector<Mmap<T>>(Inc));
538 for (std::size_t a = 0; a < I; ++a) {
539 if (!x.is_ncs[a]) continue;
540 const qn::NodeType ty = sn.nodes[a].nodetype;
541 if (ty != qn::NodeType::Source && ty != qn::NodeType::Delay &&
542 ty != qn::NodeType::Queue && ty != qn::NodeType::Fork && ty != qn::NodeType::Join) {
543 td::reject_opaque_node(sn, a, true);
544 continue;
545 }
546 const std::size_t inc = x.to_ncs[a] - 1;
547 const Mmap<T> dep = td::superpose_classes(DEP, a, R, config);
548 const Matrix<T> Psplit = td::split_probabilities(rtncs, inc, Inc, R);
549 const std::vector<npfqn::Mmap<T>> F =
550 npfqn::npfqn_traffic_split_cs(td::to_flat(dep), Psplit);
551 for (std::size_t jnc = 0; jnc < Inc; ++jnc)
552 LINKS[inc][jnc] = mmap_normalize(td::from_flat(F[jnc]));
553 }
554
555 std::vector<Mmap<T>> ARV(I);
556 for (std::size_t a = 0; a < I; ++a) {
557 if (!x.is_ncs[a] || sn.nodes[a].nodetype == qn::NodeType::Source) continue;
558 const std::size_t inc = x.to_ncs[a] - 1;
559
560 // Ordered by group id, which is what MATLAB's unique() delivers.
561 std::map<std::size_t, std::vector<Mmap<T>>> groups;
562 std::vector<Mmap<T>> independent;
563 for (std::size_t jnc = 0; jnc < Inc; ++jnc) {
564 const Mmap<T>& lk = LINKS[jnc][inc];
565 if (lk.order() == 0) continue;
566 if (!(num_traits<T>::to_double(td::total_rate(lk)) > lang::GlobalConstants::FineTol))
567 continue;
568 const std::size_t gid = syncNCS[inc][jnc];
569 if (gid == 0) independent.push_back(lk);
570 else groups[gid].push_back(lk);
571 }
572
573 std::vector<Mmap<T>> flows;
574 for (const auto& g : groups) {
575 Mmap<T> synced = g.second.front();
576 for (std::size_t f = 1; f < g.second.size(); ++f) {
577 synced = mmap_normalize(mmap_max(synced, g.second[f], config.fj_sync_q_len));
578 if (synced.order() > config.space_max) synced = td::compress_or_refuse(synced);
579 }
580 flows.push_back(synced);
581 }
582 flows.insert(flows.end(), independent.begin(), independent.end());
583
584 if (flows.size() > 1) ARV[a] = npfqn::npfqn_traffic_merge(flows, config.merge);
585 else if (flows.size() == 1) ARV[a] = flows[0];
586 else ARV[a] = td::no_flow_arrival(LINKS, inc, R);
587 }
588 return ARV;
589}
590
591} // namespace mam
592} // namespace line
593
594#endif // LINE_SOLVERS_MAM_SOLVER_MAM_TRAFFIC_H
InputError(const std::string &what)
Definition error.h:39
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
Stochastic complement of a DTMC partition, a port of matlab/lib/kpctoolbox/mc/dtmc_stochcomp....
The exception types the port throws.
Dense linear algebra over the templated number type: products, identity, inverse, and powers.
The option and result types SolverMAM shares with its analyzers.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
Dense matrix and non-owning view.
Compression of a marked MAP into a smaller representation, and the two M3A primitives it is built fro...
Marked MAP (MMAP) algebra: per-class rates, class probabilities, superposition, normalization and sca...
@ DEP
a job departs
Definition lang_types.h:115
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:324
FjSyncMap sn_build_fj_sync_map(const qn::NetworkStruct< T > &sn)
sn_build_fj_sync_map.
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 ...
Mmap< T > mmap_max(const Mmap< T > &a, const Mmap< T > &b, unsigned k)
MMAP of the maximum over k synchronization rounds of two independent MMAPs.
Definition map_max.h:88
@ MixtureOrder1
'default', 'mixture', 'mixture.order1'
std::vector< std::vector< Map< T > > > DepTable
DEP{i,r}, the departure process of class r from i in (D0,D1) form.
TrafficConfig traffic_config(const MamOptions &opt)
The traffic step's view of SolverOptions('MAM').
std::vector< T > mmap_lambda(const Mmap< T > &m)
Alias kept for parity with the MATLAB name.
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
std::vector< Mmap< T > > solver_mam_traffic_mmap(const qn::NetworkStruct< T > &sn, const DepTable< T > &DEP, const TrafficConfig &config, const FjSyncMap &fjSyncMap)
Port of solver_mam_traffic_mmap.m, the fork-join aware traffic step.
Matrix< T > kron(const Matrix< T > &A, const Matrix< T > &B)
Kronecker product.
Definition mmap_lambda.h:57
Mmap< T > mmap_compress(const Mmap< T > &in, MmapCompressMethod method)
Compress an MMAP (mmap_compress.m).
Mmap< T > mmap_super(const Mmap< T > &a, const Mmap< T > &b)
Superposition of two MMAPs: the phase process is the product chain, and the class list of the result ...
Definition mmap_lambda.h:88
std::vector< Mmap< T > > solver_mam_traffic(const qn::NetworkStruct< T > &sn, const DepTable< T > &DEP, const TrafficConfig &config)
Port of solver_mam_traffic.m.
Matrix< T > dtmc_stochcomp(const Matrix< T > &P, const std::vector< std::size_t > &keep)
Stochastic complement of a DTMC partition, a port of matlab/lib/kpctoolbox/mc/dtmc_stochcomp....
std::vector< Mmap< T > > npfqn_traffic_split_cs(const Mmap< T > &MMAP, const Matrix< T > &P)
Splitting of a marked MAP departure flow with class switching.
std::vector< Matrix< T > > Mmap
An MMAP as the MATLAB cell {D0, D1, D1^(1), ..., D1^(R)}.
mam::Mmap< T > npfqn_traffic_merge(const std::vector< mam::Mmap< T > > &flows, const MergeConfig &config=MergeConfig())
Merge a list of MMAPs carrying the same classes.
Matrix< T > eye(std::size_t n)
Identity of order n.
Definition linalg.h:28
A queueing network and its refreshed NetworkStruct.
Superposition of several marked arrival flows into one.
Splitting of a marked MAP departure flow with class switching.
static constexpr double FineTol
Definition lang_types.h:668
sn_build_fj_sync_map: which incoming flows at a Join must be synchronized.
std::vector< std::size_t > join_of_group
1-based node index, by group
std::vector< std::size_t > fork_of_group
1-based node index, by group
std::vector< std::vector< std::size_t > > node_sync
The options SolverMAM reads.
Definition mam_types.h:29
An MMAP: the underlying MAP plus the per-class arrival matrices.
Definition mmap_lambda.h:45
std::size_t classes() const
Definition mmap_lambda.h:51
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
std::size_t order() const
Definition mmap_lambda.h:50
The fields of options.config the traffic step reads.
std::size_t fj_sync_q_len
config.fj_sync_q_len, the join's synchronization queue; MATLAB's default.
MATLAB's config struct.