LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
npfqn_traffic_merge.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_NPFQN_TRAFFIC_MERGE_H
6#define LINE_API_NPFQN_TRAFFIC_MERGE_H
7
8/**
9 * @file
10 * @ingroup api_npfqn
11 * Superposition of several marked arrival flows into one.
12 *
13 * Templated port of matlab/src/api/npfqn/npfqn_traffic_merge.m and
14 * matlab/src/api/npfqn/npfqn_traffic_merge_cs.m.
15 *
16 * Given n MMAPs carrying the same R classes, the merge is the class-by-class
17 * superposition mmap_super(., ., 'match'), folded left to right, followed
18 * optionally by a compression back to a small representation. The class-
19 * switching variant first re-marks flow i with its own (R x R) switching
20 * matrix, prob((i-1)R + r, s) being the probability that a class-r arrival
21 * from flow i leaves as class s, and then superposes.
22 *
23 * WHAT MUST HOLD. Superposition of independent flows adds the rates: the
24 * merged per-class rate is the sum of the per-class rates of the operands, as
25 * an identity and not as an approximation. The class-switching variant carries
26 * the rates through the switching matrix, so the merged class-s rate is
27 * sum_i sum_r lambda_{i,r} P_i(r, s), and the TOTAL rate is preserved whenever
28 * every P_i is stochastic. Both are asserted exactly in the rational
29 * instantiation by the tests. Compression does NOT preserve them exactly: it
30 * preserves the class probabilities p_c and the aggregate mean, hence the
31 * per-class rates, only up to the APH(2) moment fit.
32 *
33 * REFERENCE DEFECTS in matlab/src/api/npfqn/npfqn_traffic_merge.m:
34 *
35 * 1. A ONE-ARGUMENT CALL WITH MORE THAN ONE NON-EMPTY FLOW ALWAYS ERRORS.
36 * Line 9 builds the default configuration as struct('merge','default'),
37 * with no compress field, and line 46 then reaches `switch config.compress`
38 * unconditionally. MATLAB raises "Reference to non-existent field
39 * 'compress'". The single-flow case returns early at line 13 and is
40 * unaffected, which is why the defect survives: the n == 1 shortcut is by
41 * far the most common call. Reproduction, from matlab/:
42 * M = map_exponential(1); M{3} = M{2};
43 * npfqn_traffic_merge({M, M})
44 * -> Reference to non-existent field 'compress'.
45 * while npfqn_traffic_merge({M}) returns M.
46 * The INTENDED behaviour is the documented default of the compress switch,
47 * i.e. compression by mmap_compress, and that is what this port does with
48 * its default-constructed configuration. The defect is NOT propagated:
49 * there is no way to spell "merge configured but compression unset" here,
50 * since MergeConfig::compress is a value with a default.
51 *
52 * 2. The 'default'/'super' branch guards each operand with ~isempty(MMAP{j})
53 * although the empty operands were already deleted at line 5, so the guard
54 * is dead. Reproduced as written (the port drops empty flows up front and
55 * the loop then has nothing to skip), and harmless.
56 *
57 * ARITHMETIC. The merge itself is a Kronecker sum and stays exact at
58 * T = Rational, so npfqn_traffic_merge_cs and the no-compression merge are
59 * offered at every arithmetic. Compression pulls in aph2_fit, so
60 * npfqn_traffic_merge with Compress::Default is gated on
61 * num_traits<T>::has_transcendental through mmap_compress.
62 *
63 * Merge::Mixture refits each superposition with mmap_mixture_fit_mmap
64 * (mmap_modulate.h) whenever it is a feasible MMAP. Merge::Interpos is served, through
65 * m3pp2m_fitc_theoretical and m3pp2m_interleave; it is gated on transcendental
66 * arithmetic like the rest of the counting-process fitters, so neither is
67 * offered at T = Rational.
68 */
69
70#include <cstddef>
71#include <vector>
72
76#include "line/num/number.h"
77#include "line/util/error.h"
78#include "line/util/matrix.h"
79
80namespace line {
81namespace npfqn {
82
83/**
84 * Superposition matching classes one to one (mmap_super.m, option 'match').
85 *
86 * Every component of the MATLAB cell, D0 and D1 included, is combined with the
87 * same Kronecker sum, so the result carries the same class list as its
88 * operands. This is NOT line::mam::mmap_super, which is the 'default' option
89 * and CONCATENATES the two class lists; the merge needs the matching form,
90 * because merging n flows of R classes must yield R classes and not n R.
91 * MATLAB errors when the class counts differ; so does this.
92 *
93 * It lives in the npfqn namespace, next to its only caller, rather than in the
94 * mam domain: the mam MMAP algebra is owned elsewhere in this tree and this
95 * option is not part of it.
96 */
97template <class T>
99 if (a.classes() != b.classes())
100 throw InputError("mmap_super_match: class matching failed, the MMAPs have different "
101 "numbers of classes");
102 mam::Mmap<T> s;
103 s.D0 = mam::krons(a.D0, b.D0);
104 s.D1 = mam::krons(a.D1, b.D1);
105 s.Dc.reserve(a.classes());
106 for (std::size_t c = 0; c < a.classes(); ++c) s.Dc.push_back(mam::krons(a.Dc[c], b.Dc[c]));
107 return mam::mmap_normalize(s);
108}
109
110/**
111 * Re-mark an MMAP's K types into R classes (mmap_mark.m).
112 *
113 * @param m an MMAP with K marked types
114 * @param prob (K x R); prob(k, r) is the probability that a type-k arrival is
115 * marked as class r
116 *
117 * D0 and D1 are untouched and D1^(r) = sum_k D1^(k) prob(k, r). MATLAB does not
118 * normalize here, so neither does this: the caller's mmap_super does it.
119 *
120 * Distinct from line::mam::mmap_mark, which marks a plain MAP with
121 * phase-dependent weights; MATLAB's mmap_mark is this one.
122 */
123template <class T>
125 const std::size_t K = prob.rows(), R = prob.cols();
126 if (K != m.classes())
127 throw InputError("mmap_mark_types: prob needs one row per marked type of the MMAP");
128 const T zero = num_traits<T>::from_int(0);
129 mam::Mmap<T> out;
130 out.D0 = m.D0;
131 out.D1 = m.D1;
132 const std::size_t n = m.order();
133 for (std::size_t r = 0; r < R; ++r) {
134 Matrix<T> Dr(n, n, zero);
135 for (std::size_t k = 0; k < K; ++k) {
136 const T& p = prob(k, r);
137 if (p == zero) continue;
138 for (std::size_t i = 0; i < n; ++i)
139 for (std::size_t j = 0; j < n; ++j) Dr(i, j) += m.Dc[k](i, j) * p;
140 }
141 out.Dc.push_back(Dr);
142 }
143 return out;
144}
145
146/** Merge rule, MATLAB's config.merge. */
147enum class Merge {
148 Default, ///< 'default', identical to 'super'
149 Super, ///< 'super'
150 Mixture, ///< 'mixture', superpose then refit with mmap_mixture_fit_mmap
151 Interpos ///< 'interpos', lumped interleaving of per-flow M3PP(2, m) fits
152};
153
154/** Post-merge compression, MATLAB's config.compress. */
155enum class Compress {
156 Default, ///< 'default', mmap_compress with its own default method
157 None ///< 'none'
158};
159
160/** MATLAB's config struct. The defaults are the documented intended ones. */
165
166namespace detail {
167
168/**
169 * Compression, if the arithmetic can carry it. The exact instantiation cannot:
170 * mmap_compress fits an APH(2) and needs square roots. Rather than making
171 * npfqn_traffic_merge uninstantiable at T = Rational -- which would remove the
172 * exact merge too, and that one is a pure Kronecker sum -- the branch is
173 * selected at compile time and the exact build refuses only at run time, and
174 * only when compression is actually requested.
175 */
176template <class T>
177mam::Mmap<T> compress_or_refuse(const mam::Mmap<T>& s) {
178 if constexpr (num_traits<T>::has_transcendental) {
180 } else {
181 throw UnsupportedError("npfqn_traffic_merge: compression needs transcendental arithmetic "
182 "(aph2_fit); merge at exact arithmetic with Compress::None");
183 }
184}
185
186} // namespace detail
187
188/**
189 * Merge a list of MMAPs carrying the same classes.
190 *
191 * @param flows the MMAPs to superpose; empty ones are dropped, as in MATLAB
192 * @param config merge rule and compression
193 * @return the merged MMAP, normalized
194 */
195template <class T>
197 const MergeConfig& config = MergeConfig()) {
198 std::vector<const mam::Mmap<T>*> nonEmpty;
199 for (const mam::Mmap<T>& f : flows)
200 if (f.order() != 0) nonEmpty.push_back(&f);
201 if (nonEmpty.empty()) throw InputError("npfqn_traffic_merge: no non-empty flow to merge");
202 if (nonEmpty.size() == 1) return *nonEmpty.front();
203
204 switch (config.merge) {
205 case Merge::Default:
206 case Merge::Super:
207 break;
208 case Merge::Mixture: {
209 // Each superposition is re-fitted as an order-2 mixture of APH(2)s
210 // (mmap_mixture_fit_mmap) when it is a feasible MMAP, as in the
211 // reference; the fit is transcendental.
212 if constexpr (!num_traits<T>::has_transcendental) {
213 throw UnsupportedError("npfqn_traffic_merge: merge 'mixture' fits APH(2) "
214 "components, which is transcendental; use double or real");
215 } else {
216 mam::Mmap<T> s = *nonEmpty.front();
217 for (std::size_t j = 1; j < nonEmpty.size(); ++j) {
218 s = mmap_super_match(s, *nonEmpty[j]);
219 // MATLAB's mmap_isfeasible, whose tolerance defaults to 1e-8, not the
220 // exact predicate: a superposition is feasible only to round-off
223 }
224 if (config.compress == Compress::Default) s = detail::compress_or_refuse(s);
225 return mam::mmap_normalize(s);
226 }
227 }
228 case Merge::Interpos: {
229 // REFUSED AT RUNTIME, NOT AT COMPILE TIME. `m3pp2m_fitc_theoretical`
230 // static_asserts on transcendental arithmetic, and a template
231 // instantiates every branch of this switch whatever the runtime
232 // `config.merge` is -- so calling it unguarded made the whole of
233 // `npfqn_traffic_merge` uninstantiable at exact arithmetic, including
234 // the Super and single-flow paths that never reach here.
235 if constexpr (!num_traits<T>::has_transcendental) {
236 throw UnsupportedError("npfqn_traffic_merge: merge 'interpos' fits an "
237 "M3PP(2,m), which is transcendental; use double or real");
238 } else {
239 // Every flow is first reduced to an M3PP(2, m) on its exact counting
240 // characteristics, then the L of them are lumped onto one
241 // birth-death phase process of order L + 1.
242 std::vector<mam::Mmap<T>> flowFits;
243 for (std::size_t j = 0; j < nonEmpty.size(); ++j)
244 flowFits.push_back(mam::m3pp2m_fitc_theoretical(
245 *nonEmpty[j], std::string("exact_delta"), num_traits<T>::from_int(1),
247 mam::Mmap<T> lumped = mam::m3pp2m_interleave(flowFits);
248 if (config.compress == Compress::Default)
249 lumped = detail::compress_or_refuse(lumped);
250 return mam::mmap_normalize(lumped);
251 }
252 }
253 }
254
255 mam::Mmap<T> s = *nonEmpty.front();
256 for (std::size_t j = 1; j < nonEmpty.size(); ++j) s = mmap_super_match(s, *nonEmpty[j]);
257
258 if (config.compress == Compress::Default) s = detail::compress_or_refuse(s);
259 return mam::mmap_normalize(s);
260}
261
262} // namespace npfqn
263} // namespace line
264
265#endif // LINE_API_NPFQN_TRAFFIC_MERGE_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.
LUMPED interleaving of several M3PP(2, m), and the two fitters built on it (matlab/lib/m3a/m3a/m3pp/m...
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...
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 ...
@ MixtureOrder1
'default', 'mixture', 'mixture.order1'
Mmap< T > m3pp2m_interleave(const std::vector< Mmap< T > > &parts)
Interleave L M3PP(2, m_i) into one M3PP of order L + 1 whose class list is the concatenation of their...
Mmap< T > mmap_mixture_fit_mmap(const Mmap< T > &mm)
mmap_mixture_fit driven from an MMAP (mmap_mixture_fit_mmap.m): the triple sigma and the cross moment...
bool mmap_isfeasible_tol(const Mmap< T > &m, const T &tol)
Feasibility of a marked MAP WITHIN A TOLERANCE, the semantics of matlab/lib/m3a/m3a/mmap/mmap_isfeasi...
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_compress(const Mmap< T > &in, MmapCompressMethod method)
Compress an MMAP (mmap_compress.m).
Mmap< T > m3pp2m_fitc_theoretical(const Mmap< T > &mm, const std::string &method, const T &t, const T &tinf)
Fit the counting characteristics of a GIVEN MMAP with an M3PP(2, m).
Merge
Merge rule, MATLAB's config.merge.
@ Interpos
'interpos', lumped interleaving of per-flow M3PP(2, m) fits
@ Default
'default', identical to 'super'
@ Mixture
'mixture', superpose then refit with mmap_mixture_fit_mmap
mam::Mmap< T > mmap_super_match(const mam::Mmap< T > &a, const mam::Mmap< T > &b)
Superposition matching classes one to one (mmap_super.m, option 'match').
mam::Mmap< T > mmap_mark_types(const mam::Mmap< T > &m, const Matrix< T > &prob)
Re-mark an MMAP's K types into R classes (mmap_mark.m).
Compress
Post-merge compression, MATLAB's config.compress.
@ Default
'default', mmap_compress with its own default method
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.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
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
MATLAB's config struct.