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 * NOT PORTED. Merge::Mixture needs mmap_mixture_fit_mmap, which is not in this
64 * tree; it raises UnsupportedError naming the missing MATLAB function rather
65 * than falling back to another merge. Merge::Interpos is served, through
66 * m3pp2m_fitc_theoretical and m3pp2m_interleave; it is gated on transcendental
67 * arithmetic like the rest of the counting-process fitters, so it is not
68 * offered at T = Rational.
69 */
70
71#include <cstddef>
72#include <vector>
73
77#include "line/num/number.h"
78#include "line/util/error.h"
79#include "line/util/matrix.h"
80
81namespace line {
82namespace npfqn {
83
84/**
85 * Superposition matching classes one to one (mmap_super.m, option 'match').
86 *
87 * Every component of the MATLAB cell, D0 and D1 included, is combined with the
88 * same Kronecker sum, so the result carries the same class list as its
89 * operands. This is NOT line::mam::mmap_super, which is the 'default' option
90 * and CONCATENATES the two class lists; the merge needs the matching form,
91 * because merging n flows of R classes must yield R classes and not n R.
92 * MATLAB errors when the class counts differ; so does this.
93 *
94 * It lives in the npfqn namespace, next to its only caller, rather than in the
95 * mam domain: the mam MMAP algebra is owned elsewhere in this tree and this
96 * option is not part of it.
97 */
98template <class T>
100 if (a.classes() != b.classes())
101 throw InputError("mmap_super_match: class matching failed, the MMAPs have different "
102 "numbers of classes");
103 mam::Mmap<T> s;
104 s.D0 = mam::krons(a.D0, b.D0);
105 s.D1 = mam::krons(a.D1, b.D1);
106 s.Dc.reserve(a.classes());
107 for (std::size_t c = 0; c < a.classes(); ++c) s.Dc.push_back(mam::krons(a.Dc[c], b.Dc[c]));
108 return mam::mmap_normalize(s);
109}
110
111/**
112 * Re-mark an MMAP's K types into R classes (mmap_mark.m).
113 *
114 * @param m an MMAP with K marked types
115 * @param prob (K x R); prob(k, r) is the probability that a type-k arrival is
116 * marked as class r
117 *
118 * D0 and D1 are untouched and D1^(r) = sum_k D1^(k) prob(k, r). MATLAB does not
119 * normalize here, so neither does this: the caller's mmap_super does it.
120 *
121 * Distinct from line::mam::mmap_mark, which marks a plain MAP with
122 * phase-dependent weights; MATLAB's mmap_mark is this one.
123 */
124template <class T>
126 const std::size_t K = prob.rows(), R = prob.cols();
127 if (K != m.classes())
128 throw InputError("mmap_mark_types: prob needs one row per marked type of the MMAP");
129 const T zero = num_traits<T>::from_int(0);
130 mam::Mmap<T> out;
131 out.D0 = m.D0;
132 out.D1 = m.D1;
133 const std::size_t n = m.order();
134 for (std::size_t r = 0; r < R; ++r) {
135 Matrix<T> Dr(n, n, zero);
136 for (std::size_t k = 0; k < K; ++k) {
137 const T& p = prob(k, r);
138 if (p == zero) continue;
139 for (std::size_t i = 0; i < n; ++i)
140 for (std::size_t j = 0; j < n; ++j) Dr(i, j) += m.Dc[k](i, j) * p;
141 }
142 out.Dc.push_back(Dr);
143 }
144 return out;
145}
146
147/** Merge rule, MATLAB's config.merge. */
148enum class Merge {
149 Default, ///< 'default', identical to 'super'
150 Super, ///< 'super'
151 Mixture, ///< 'mixture', not ported
152 Interpos ///< 'interpos', lumped interleaving of per-flow M3PP(2, m) fits
153};
154
155/** Post-merge compression, MATLAB's config.compress. */
156enum class Compress {
157 Default, ///< 'default', mmap_compress with its own default method
158 None ///< 'none'
159};
160
161/** MATLAB's config struct. The defaults are the documented intended ones. */
166
167namespace detail {
168
169/**
170 * Compression, if the arithmetic can carry it. The exact instantiation cannot:
171 * mmap_compress fits an APH(2) and needs square roots. Rather than making
172 * npfqn_traffic_merge uninstantiable at T = Rational -- which would remove the
173 * exact merge too, and that one is a pure Kronecker sum -- the branch is
174 * selected at compile time and the exact build refuses only at run time, and
175 * only when compression is actually requested.
176 */
177template <class T>
178mam::Mmap<T> compress_or_refuse(const mam::Mmap<T>& s) {
179 if constexpr (num_traits<T>::has_transcendental) {
181 } else {
182 throw UnsupportedError("npfqn_traffic_merge: compression needs transcendental arithmetic "
183 "(aph2_fit); merge at exact arithmetic with Compress::None");
184 }
185}
186
187} // namespace detail
188
189/**
190 * Merge a list of MMAPs carrying the same classes.
191 *
192 * @param flows the MMAPs to superpose; empty ones are dropped, as in MATLAB
193 * @param config merge rule and compression
194 * @return the merged MMAP, normalized
195 */
196template <class T>
198 const MergeConfig& config = MergeConfig()) {
199 std::vector<const mam::Mmap<T>*> nonEmpty;
200 for (const mam::Mmap<T>& f : flows)
201 if (f.order() != 0) nonEmpty.push_back(&f);
202 if (nonEmpty.empty()) throw InputError("npfqn_traffic_merge: no non-empty flow to merge");
203 if (nonEmpty.size() == 1) return *nonEmpty.front();
204
205 switch (config.merge) {
206 case Merge::Default:
207 case Merge::Super:
208 break;
209 case Merge::Mixture:
210 throw UnsupportedError("npfqn_traffic_merge: merge 'mixture' needs "
211 "mmap_mixture_fit_mmap, which is not ported");
212 case Merge::Interpos: {
213 // REFUSED AT RUNTIME, NOT AT COMPILE TIME. `m3pp2m_fitc_theoretical`
214 // static_asserts on transcendental arithmetic, and a template
215 // instantiates every branch of this switch whatever the runtime
216 // `config.merge` is -- so calling it unguarded made the whole of
217 // `npfqn_traffic_merge` uninstantiable at exact arithmetic, including
218 // the Super and single-flow paths that never reach here.
219 if constexpr (!num_traits<T>::has_transcendental) {
220 throw UnsupportedError("npfqn_traffic_merge: merge 'interpos' fits an "
221 "M3PP(2,m), which is transcendental; use double or real");
222 } else {
223 // Every flow is first reduced to an M3PP(2, m) on its exact counting
224 // characteristics, then the L of them are lumped onto one
225 // birth-death phase process of order L + 1.
226 std::vector<mam::Mmap<T>> flowFits;
227 for (std::size_t j = 0; j < nonEmpty.size(); ++j)
228 flowFits.push_back(mam::m3pp2m_fitc_theoretical(
229 *nonEmpty[j], std::string("exact_delta"), num_traits<T>::from_int(1),
231 mam::Mmap<T> lumped = mam::m3pp2m_interleave(flowFits);
232 if (config.compress == Compress::Default)
233 lumped = detail::compress_or_refuse(lumped);
234 return mam::mmap_normalize(lumped);
235 }
236 }
237 }
238
239 mam::Mmap<T> s = *nonEmpty.front();
240 for (std::size_t j = 1; j < nonEmpty.size(); ++j) s = mmap_super_match(s, *nonEmpty[j]);
241
242 if (config.compress == Compress::Default) s = detail::compress_or_refuse(s);
243 return mam::mmap_normalize(s);
244}
245
246} // namespace npfqn
247} // namespace line
248
249#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...
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', not ported
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.
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.