LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
mmap_assemble.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_ASSEMBLE_H
6#define LINE_API_MAM_MMAP_ASSEMBLE_H
7
8/**
9 * @file
10 * @ingroup api_mam
11 * The MMAP assembly primitives `solver_mam_basic.m` builds its per-station
12 * arrival stream from: `mmap_exponential`, the probabilistic `mmap_mark`, the
13 * per-class `mmap_scale`, and `mmap_super_safe`.
14 *
15 * They are separate from `mmap_lambda.h` because each of them is a DIFFERENT
16 * function from the same-named one already there: `mmap_lambda.h`'s
17 * `mmap_mark` splits a MAP by per-PHASE weights and its `mmap_scale` takes a
18 * single target mean, which are the M3A signatures; the MAM solver calls the
19 * kpctoolbox ones, which split an MMAP by per-CLASS probabilities and target
20 * one mean per class. Naming them apart is deliberate: two functions with one
21 * name and two meanings is exactly the failure mode the parity notes record for
22 * `map_normalize`.
23 */
24
25#include <algorithm>
26#include <cmath>
27#include <cstddef>
28#include <limits>
29#include <numeric>
30#include <string>
31#include <vector>
32
35#include "line/num/number.h"
36#include "line/util/error.h"
37#include "line/util/linalg.h"
38#include "line/util/matrix.h"
39
40namespace line {
41namespace mam {
42
43/**
44 * Order-n MMAP with the given per-class arrival rates (mmap_exponential.m).
45 *
46 * The per-class matrix is `flip(eye(n)) * lambda_c`, so at n = 1 this is the
47 * ordinary marked Poisson stream and at n > 1 it is the n-phase cycle the
48 * reference uses as a neutral element of the superposition.
49 */
50template <class T>
51Mmap<T> mmap_exponential_vec(const std::vector<T>& lambda, std::size_t n = 1) {
52 const T zero = num_traits<T>::from_int(0);
53 if (n == 0) throw InputError("mmap_exponential_vec: order must be positive");
54 Mmap<T> m;
55 m.D0 = Matrix<T>(n, n, zero);
56 m.D1 = Matrix<T>(n, n, zero);
57 for (std::size_t c = 0; c < lambda.size(); ++c) {
58 Matrix<T> Dc(n, n, zero);
59 for (std::size_t i = 0; i < n; ++i) Dc(i, n - 1 - i) = lambda[c];
60 for (std::size_t i = 0; i < n; ++i)
61 for (std::size_t j = 0; j < n; ++j) m.D1(i, j) += Dc(i, j);
62 m.Dc.push_back(Dc);
63 }
64 return mmap_normalize(m);
65}
66
67/**
68 * Re-mark an MMAP by a (K x R) probability matrix (mmap_mark.m): a type-k
69 * arrival is reported as class r with probability prob(k,r).
70 */
71template <class T>
72Mmap<T> mmap_mark_probs(const Mmap<T>& in, const Matrix<T>& prob) {
73 const T zero = num_traits<T>::from_int(0);
74 const std::size_t K = prob.rows(), R = prob.cols();
75 if (K > in.classes())
76 throw InputError("mmap_mark_probs: the probability matrix has more input types than the "
77 "MMAP has classes");
78 Mmap<T> m;
79 m.D0 = in.D0;
80 m.D1 = in.D1;
81 const std::size_t n = in.order();
82 for (std::size_t r = 0; r < R; ++r) {
83 Matrix<T> Dr(n, n, zero);
84 for (std::size_t k = 0; k < K; ++k)
85 for (std::size_t i = 0; i < n; ++i)
86 for (std::size_t j = 0; j < n; ++j) Dr(i, j) += in.Dc[k](i, j) * prob(k, r);
87 m.Dc.push_back(Dr);
88 }
89 return m;
90}
91
92/**
93 * Retarget the per-class MEAN inter-arrival times (mmap_scale.m, vector form).
94 *
95 * Each class matrix is rescaled by (1/M_c)/lambda_c, then the MMAP is
96 * renormalized. The reference calls this "heuristic because it also affects the
97 * other classes"; the refinement loop that follows it in MATLAB is dead code
98 * behind an unconditional `return`, so the heuristic IS the function and is
99 * what is reproduced here. A class with zero rate is zeroed rather than divided.
100 */
101template <class T>
102Mmap<T> mmap_scale_perclass(const Mmap<T>& in, const std::vector<T>& M) {
103 const T zero = num_traits<T>::from_int(0);
104 const std::size_t C = in.classes();
105 if (M.size() != C) throw InputError("mmap_scale_perclass: one target mean per class is needed");
106 const std::vector<T> l = mmap_count_lambda(in);
107 Mmap<T> s;
108 s.D0 = in.D0;
109 s.D1 = Matrix<T>(in.order(), in.order(), zero);
110 for (std::size_t c = 0; c < C; ++c) {
111 Matrix<T> Dc = in.Dc[c];
112 const T f = (l[c] > zero) ? T(T(num_traits<T>::from_int(1) / M[c]) / l[c]) : zero;
113 for (std::size_t i = 0; i < Dc.rows(); ++i)
114 for (std::size_t j = 0; j < Dc.cols(); ++j) {
115 Dc(i, j) *= f;
116 s.D1(i, j) += Dc(i, j);
117 }
118 s.Dc.push_back(Dc);
119 }
120 return mmap_normalize(s);
121}
122
123namespace mmap_super_detail {
124
125/** 1-norm of D1, used to detect a component that carries no arrivals at all. */
126template <class T>
127double arrival_norm(const Mmap<T>& m) {
128 double best = 0.0;
129 for (std::size_t j = 0; j < m.D1.cols(); ++j) {
130 double col = 0.0;
131 for (std::size_t i = 0; i < m.D1.rows(); ++i)
132 col += std::fabs(num_traits<T>::to_double(m.D1(i, j)));
133 if (col > best) best = col;
134 }
135 return best;
136}
137
138/** The order-1 marked Poisson stream carrying this MMAP's per-class rates. */
139template <class T>
140Mmap<T> to_poisson(const Mmap<T>& m) {
142}
143
144} // namespace mmap_super_detail
145
146/**
147 * Order-bounded superposition of several MMAPs (mmap_super_safe.m).
148 *
149 * Components are superposed low-SCV first, and the product order is held at or
150 * below `maxorder` by replacing a component with its marked Poisson equivalent.
151 * The marks are permuted back into INPUT order afterwards, because `mmap_super`
152 * concatenates them in fold order while every caller reads mark k as its own
153 * k-th class; without that the SCV sort renames the classes.
154 *
155 * ONE REFERENCE BRANCH IS REFUSED BY NAME rather than substituted. When the
156 * order budget still allows an order-2 component, MATLAB compresses with
157 * `mamap2m_fit_gamma_fb_mmap`, an acyclic MAP(2) fit that `mmap_compress.h`
158 * records as not ported. Substituting the Poisson fallback there would silently
159 * discard the component's variability, so this refuses instead. With the
160 * solver's default `space_max = 128` the branch needs an arrival stream of
161 * order above 128 (or a product above it with room for a 2-phase factor) to be
162 * reachable at all.
163 */
164template <class T>
165Mmap<T> mmap_super_safe(const std::vector<Mmap<T>>& in, std::size_t maxorder) {
166 if (maxorder == 0) throw InputError("mmap_super_safe: maxorder must be positive");
167 std::vector<Mmap<T>> parts;
168 for (const Mmap<T>& m : in) {
169 if (m.order() == 0) continue;
170 // A component with an all-zero D1 has zero rate and an absorbing phase
171 // generator, so map_scv would fail on it; canonicalize to the equivalent
172 // order-1 null, whose superposition is the identity.
173 if (m.order() > 1 && mmap_super_detail::arrival_norm(m) < 1e-13)
174 parts.push_back(mmap_exponential_vec(
175 std::vector<T>(m.classes(), num_traits<T>::from_int(0)), 1));
176 else
177 parts.push_back(m);
178 }
179 if (parts.empty()) throw InputError("mmap_super_safe: no components to superpose");
180
181 std::vector<std::size_t> order(parts.size());
182 std::iota(order.begin(), order.end(), 0u);
183 // MATLAB sorts by map_scv, and a ZERO-RATE component (the neutral element
184 // the MAM analyzer superposes to reshape a marking) has an infinite mean, so
185 // its SCV is NaN there and MATLAB's ascending sort puts NaN LAST. Calling
186 // map_scv on it here would divide by a zero rate, so the case is answered
187 // with +infinity, which sorts last for the same reason.
188 std::vector<double> scv(parts.size());
189 for (std::size_t i = 0; i < parts.size(); ++i)
190 scv[i] = mmap_super_detail::arrival_norm(parts[i]) > 0.0
191 ? num_traits<T>::to_double(map_scv(parts[i].map()))
192 : std::numeric_limits<double>::infinity();
193 std::stable_sort(order.begin(), order.end(),
194 [&scv](std::size_t a, std::size_t b) { return scv[a] < scv[b]; });
195
196 // Mark provenance: a zero-rate component sorts last (SCV +inf above), so a
197 // chain that never visits the station used to push its marks ahead of one
198 // that does, renaming both chains' classes.
199 std::vector<std::size_t> markbase(parts.size() + 1, 0);
200 for (std::size_t i = 0; i < parts.size(); ++i)
201 markbase[i + 1] = markbase[i] + parts[i].classes();
202 std::vector<std::size_t> outorder;
203
204 bool first = true;
205 Mmap<T> sup;
206 for (std::size_t idx : order) {
207 for (std::size_t j = 0; j < parts[idx].classes(); ++j)
208 outorder.push_back(markbase[idx] + j);
209 Mmap<T> cur = parts[idx];
210 if (cur.order() > maxorder) {
211 if (maxorder >= 2)
212 throw UnsupportedError(
213 "mmap_super_safe: a component of order " + std::to_string(cur.order()) +
214 " exceeds the order budget and the reference compresses it with "
215 "mamap2m_fit_gamma_fb_mmap, which is not ported to C++");
216 cur = mmap_super_detail::to_poisson(cur);
217 }
218 if (first) {
219 sup = (maxorder == 1) ? mmap_super_detail::to_poisson(cur) : cur;
220 first = false;
221 continue;
222 }
223 if (sup.order() * cur.order() > maxorder) {
224 if (sup.order() * 2 <= maxorder)
225 throw UnsupportedError(
226 "mmap_super_safe: the superposition exceeds the order budget and the "
227 "reference compresses the next component with mamap2m_fit_gamma_fb_mmap, "
228 "which is not ported to C++");
229 sup = mmap_super(sup, mmap_super_detail::to_poisson(cur));
230 } else {
231 sup = mmap_super(sup, cur);
232 }
233 }
234 // Restore the caller's mark order.
235 if (sup.Dc.size() == outorder.size() &&
236 !std::is_sorted(outorder.begin(), outorder.end())) {
237 std::vector<Matrix<T>> reordered(outorder.size());
238 for (std::size_t j = 0; j < outorder.size(); ++j) reordered[outorder[j]] = sup.Dc[j];
239 sup.Dc.swap(reordered);
240 }
241 return sup;
242}
243
244} // namespace mam
245} // namespace line
246
247#endif // LINE_API_MAM_MMAP_ASSEMBLE_H
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
UnsupportedError(const std::string &what)
Definition error.h:51
The exception types the port throws.
Dense linear algebra over the templated number type: products, identity, inverse, and powers.
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...
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_mark_probs(const Mmap< T > &in, const Matrix< T > &prob)
Re-mark an MMAP by a (K x R) probability matrix (mmap_mark.m): a type-k arrival is reported as class ...
Mmap< T > mmap_scale_perclass(const Mmap< T > &in, const std::vector< T > &M)
Retarget the per-class MEAN inter-arrival times (mmap_scale.m, vector form).
Mmap< T > mmap_exponential_vec(const std::vector< T > &lambda, std::size_t n=1)
Order-n MMAP with the given per-class arrival rates (mmap_exponential.m).
T map_scv(const Map< T > &m)
Squared coefficient of variation.
Definition map_moment.h:140
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< T > mmap_count_lambda(const Mmap< T > &m)
Per-class arrival rates, lambda_c = theta D1^(c) e.
Mmap< T > mmap_super_safe(const std::vector< Mmap< T > > &in, std::size_t maxorder)
Order-bounded superposition of several MMAPs (mmap_super_safe.m).
Number-type abstraction for the templated API port.
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