LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
mdd_descriptor.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_DESCRIPTOR_H
6#define LINE_API_MDD_MDD_DESCRIPTOR_H
7
8/**
9 * @file
10 * @ingroup api_mdd
11 * Kronecker rate descriptor of a single-class closed queueing network.
12 *
13 * Port of matlab/src/api/mdd/mdd_descriptor.m, jline.api.mdd.Mdd_descriptor and
14 * python/line_solver/api/mdd/descriptor.py, for the Miner-Ciardo-Donatelli
15 * aggregation `mdd_mcd` (SIGMETRICS 2000).
16 *
17 * The transition rate matrix is expressed compositionally as
18 * R = sum_e (kron_k W_k^e) restricted to the reachable set, with
19 * W_k^e[i,j] = lambda_k^e[i] * Prob_k^e(i,j) (Eq. 1). Each level k is a station
20 * and each event e is a completion at station a routed to b.
21 *
22 * EXPONENTIAL STATIONS. The local state is the population alone:
23 * W_a^e[i,i-1] = mu[a]*min(i,servers[a])*P[a][b] for i >= 1 (departure),
24 * W_b^e[i,i+1] = 1 for i <= N-1 (arrival), and the identity elsewhere.
25 *
26 * PHASE-TYPE STATIONS. The local state is the PAIR (population, phase of the
27 * job in service), encoded in one level rather than two. Splitting them does not
28 * work: on a completion routed into station b the phase at b restarts only when
29 * b was empty, a joint condition on b's two components, which is not a product
30 * of per-level terms. Merging them keeps every event local:
31 *
32 * index 0 : station empty
33 * index 1 + (n-1)*h + (a-1) : n jobs present, job in service in phase a
34 * domain = 1 + N*h (h = 1 reproduces index = n)
35 *
36 * With exit vector t = D1*1 and entry law pie, departure (n,a)->(n-1,b) at
37 * t[a]*P[a][b]*pie[b] for n >= 2 and (1,a)->0 at t[a]*P[a][b]; arrival 0->(1,b)
38 * at pie[b] and (m,c)->(m+1,c) at 1 for m >= 1; internal (n,a)->(n,b) at
39 * D0[a][b] for n >= 1, a != b.
40 *
41 * RESTRICTIONS. A phase-type station must be single-server: with c > 1 or an
42 * infinite server the local state would have to count jobs per phase rather than
43 * name one phase, a different and much larger encoding. It must also be
44 * NON-preemptive, because the composite level names the phase of the one job in
45 * service and restarts it at pie when the next job starts; under preemptive
46 * resume an arrival suspends that job and its phase has to be remembered, so the
47 * local state would need a stack of phases. That matters for LCFSPR, which is
48 * BCMP type 2 and stays product-form under general service: the insensitivity is
49 * real but is NOT reachable through this encoding. Exponential service is
50 * unaffected, preemption being immaterial by memorylessness. Pass the
51 * disciplines to have the case rejected rather than silently modelled as
52 * non-preemptive.
53 */
54
55#include <cmath>
56#include <cstddef>
57#include <string>
58#include <vector>
59
61#include "line/num/number.h"
62#include "line/util/error.h"
63#include "line/util/matrix.h"
64
65namespace line {
66namespace mdd {
67
68namespace detail {
69
70/** Disciplines whose preemptive-resume semantics the composite encoding cannot carry. */
71inline bool desc_is_preemptive_resume(const std::string& nm) {
72 return nm == "LCFSPR" || nm == "FCFSPR" || nm == "LCFSPRPRIO" || nm == "FCFSPRPRIO";
73}
74
75/** Shared-server disciplines, where every job present holds its own phase. */
76inline bool desc_is_shared_server(const std::string& nm) {
77 return nm == "PS" || nm == "DPS" || nm == "GPS";
78}
79
80inline std::string desc_upper(const std::string& s) {
81 std::string out = s;
82 for (std::size_t i = 0; i < out.size(); ++i)
83 if (out[i] >= 'a' && out[i] <= 'z') out[i] = static_cast<char>(out[i] - 'a' + 'A');
84 return out;
85}
86
87/** Local index of (population n, service phase a); 0 when the station is empty. */
88inline int desc_idx(int n, int a, std::size_t h) {
89 if (n == 0) return 0;
90 return 1 + (n - 1) * static_cast<int>(h) + (a - 1);
91}
92
93/** Population of a local index; 0 when empty. */
94inline int desc_population(int index, std::size_t h) {
95 if (index == 0) return 0;
96 return (index - 1) / static_cast<int>(h) + 1;
97}
98
99/** Service phase (1-based) of a local index; 0 when empty. */
100inline int desc_phase(int index, std::size_t h) {
101 if (index == 0) return 0;
102 return (index - 1) % static_cast<int>(h) + 1;
103}
104
105/** Phase changes that do not complete a service, at any population n >= 1. */
106template <class T>
107MddLocalMatrix<T> desc_internal(const Matrix<T>& D0i, std::size_t h, int N, int d) {
108 const T zero = num_traits<T>::from_int(0);
109 typename MddLocalMatrix<T>::Builder bld(static_cast<std::size_t>(d));
110 for (int n = 1; n <= N; ++n)
111 for (std::size_t a = 1; a <= h; ++a)
112 for (std::size_t b = 1; b <= h; ++b) {
113 if (a == b || D0i(a - 1, b - 1) == zero) continue;
114 bld.add(static_cast<std::size_t>(desc_idx(n, static_cast<int>(a), h)),
115 static_cast<std::size_t>(desc_idx(n, static_cast<int>(b), h)),
116 D0i(a - 1, b - 1));
117 }
118 return bld.build();
119}
120
121/** Completion at this station, routed out with probability pr. */
122template <class T>
123MddLocalMatrix<T> desc_departure(const Matrix<T>& D1i, const std::vector<T>& piei, std::size_t h,
124 int N, int d, const T& mui, double srv, const T& pr) {
125 const T zero = num_traits<T>::from_int(0);
126 typename MddLocalMatrix<T>::Builder bld(static_cast<std::size_t>(d));
127 if (h == 1) {
128 // exponential: the multi-server and delay rate laws live here
129 for (int n = 1; n <= N; ++n) {
130 const double cap = static_cast<double>(n) < srv ? static_cast<double>(n) : srv;
131 bld.add(static_cast<std::size_t>(n), static_cast<std::size_t>(n - 1),
132 T(mui * num_traits<T>::from_double(cap) * pr));
133 }
134 } else {
135 std::vector<T> t(h, zero);
136 for (std::size_t a = 0; a < h; ++a) {
137 T s = zero;
138 for (std::size_t b = 0; b < h; ++b) s += D1i(a, b);
139 t[a] = s;
140 }
141 for (int n = 1; n <= N; ++n)
142 for (std::size_t a = 1; a <= h; ++a) {
143 if (t[a - 1] == zero) continue;
144 if (n == 1) {
145 bld.add(static_cast<std::size_t>(desc_idx(1, static_cast<int>(a), h)), 0,
146 T(t[a - 1] * pr));
147 } else {
148 for (std::size_t b = 1; b <= h; ++b) {
149 if (piei[b - 1] == zero) continue;
150 bld.add(static_cast<std::size_t>(desc_idx(n, static_cast<int>(a), h)),
151 static_cast<std::size_t>(
152 desc_idx(n - 1, static_cast<int>(b), h)),
153 T(t[a - 1] * pr * piei[b - 1]));
154 }
155 }
156 }
157 }
158 return bld.build();
159}
160
161/** An arrival starts service only when the station was empty. */
162template <class T>
163MddLocalMatrix<T> desc_arrival(const std::vector<T>& pieb, std::size_t h, int N, int d) {
164 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
165 typename MddLocalMatrix<T>::Builder bld(static_cast<std::size_t>(d));
166 if (h == 1) {
167 for (int m = 0; m < N; ++m)
168 bld.add(static_cast<std::size_t>(m), static_cast<std::size_t>(m + 1), one);
169 } else {
170 for (std::size_t b = 1; b <= h; ++b) {
171 if (pieb[b - 1] == zero) continue;
172 bld.add(0, static_cast<std::size_t>(desc_idx(1, static_cast<int>(b), h)), pieb[b - 1]);
173 }
174 for (int m = 1; m < N; ++m)
175 for (std::size_t c = 1; c <= h; ++c)
176 bld.add(static_cast<std::size_t>(desc_idx(m, static_cast<int>(c), h)),
177 static_cast<std::size_t>(desc_idx(m + 1, static_cast<int>(c), h)), one);
178 }
179 return bld.build();
180}
181
182/** Successor local-index vectors of a state, used to generate the reachable set. */
183template <class T>
184std::vector<std::vector<int>> desc_successors(const std::vector<int>& s,
185 const std::vector<Matrix<T>>& D0,
186 const std::vector<Matrix<T>>& D1,
187 const std::vector<std::vector<T>>& pie,
188 const std::vector<std::size_t>& h,
189 const Matrix<T>& P) {
190 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
191 const std::size_t K = s.size();
192 std::vector<std::vector<int>> out;
193 for (std::size_t i = 0; i < K; ++i) {
194 const int ni = desc_population(s[i], h[i]);
195 const int ai = desc_phase(s[i], h[i]);
196 if (ni == 0) continue;
197 // internal phase change
198 if (h[i] > 1) {
199 for (std::size_t b = 1; b <= h[i]; ++b) {
200 if (static_cast<int>(b) == ai || D0[i](ai - 1, b - 1) == zero) continue;
201 std::vector<int> t = s;
202 t[i] = desc_idx(ni, static_cast<int>(b), h[i]);
203 out.push_back(t);
204 }
205 }
206 // completion routed to j
207 T exits = one;
208 if (h[i] > 1) {
209 exits = zero;
210 for (std::size_t b = 0; b < h[i]; ++b) exits += D1[i](ai - 1, b);
211 }
212 if (exits == zero) continue;
213 for (std::size_t j = 0; j < K; ++j) {
214 if (j == i || !(P(i, j) > zero)) continue;
215 const int nj = desc_population(s[j], h[j]);
216 const int aj = desc_phase(s[j], h[j]);
217 int newi = (h[i] == 1) ? desc_idx(ni - 1, 1, 1) : 0;
218 for (std::size_t bi = 1; bi <= h[i]; ++bi) {
219 if (h[i] > 1) {
220 if (ni == 1) {
221 newi = 0;
222 } else if (pie[i][bi - 1] == zero) {
223 continue;
224 } else {
225 newi = desc_idx(ni - 1, static_cast<int>(bi), h[i]);
226 }
227 } else if (bi > 1) {
228 continue;
229 }
230 for (std::size_t bj = 1; bj <= h[j]; ++bj) {
231 int newj;
232 if (nj == 0) {
233 if (pie[j][bj - 1] == zero) continue;
234 newj = desc_idx(1, static_cast<int>(bj), h[j]);
235 } else if (bj > 1) {
236 continue;
237 } else {
238 newj = desc_idx(nj + 1, aj, h[j]);
239 }
240 std::vector<int> t = s;
241 t[i] = newi;
242 t[j] = newj;
243 out.push_back(t);
244 }
245 if (ni == 1 && h[i] > 1) break;
246 }
247 }
248 }
249 return out;
250}
251
252} // namespace detail
253
254/**
255 * Build the descriptor.
256 *
257 * @param mu station service rates, 1/E[S]; entry i is ignored when station i is
258 * given a phase-type law through proc
259 * @param P station-to-station routing matrix, row-stochastic
260 * @param servers servers per station, infinite for delay/IS
261 * @param N closed population
262 * @param proc per-station service law; an absent or `present == false` entry is
263 * an exponential station
264 * @param sched per-station discipline names, consulted only to REJECT a
265 * phase-type law at a preemptive-resume or shared-server station; may be
266 * empty when every station is non-preemptive
267 */
268template <class T>
269MddDescriptor<T> mdd_descriptor(const std::vector<T>& mu, const Matrix<T>& P,
270 const std::vector<double>& servers, int N,
271 const std::vector<MddServiceLaw<T>>& proc =
272 std::vector<MddServiceLaw<T>>(),
273 const std::vector<std::string>& sched =
274 std::vector<std::string>()) {
275 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
276 const std::size_t K = mu.size();
277 if (K == 0) throw InputError("mdd_descriptor: the network has no stations");
278 if (P.rows() != K || P.cols() != K)
279 throw InputError("mdd_descriptor: the routing matrix is not (K x K)");
280 if (servers.size() != K)
281 throw InputError("mdd_descriptor: one server count per station is required");
282 if (N < 0) throw InputError("mdd_descriptor: the population must be non-negative");
283
284 std::vector<Matrix<T>> D0(K), D1(K);
285 std::vector<std::vector<T>> entry(K);
286 std::vector<std::size_t> h(K, 1);
287
288 for (std::size_t i = 0; i < K; ++i) {
289 const bool has_law = i < proc.size() && proc[i].present;
290 if (!has_law) {
291 D0[i] = Matrix<T>(1, 1, T(-mu[i]));
292 D1[i] = Matrix<T>(1, 1, mu[i]);
293 entry[i] = std::vector<T>(1, one);
294 h[i] = 1;
295 continue;
296 }
297 D0[i] = proc[i].D0;
298 D1[i] = proc[i].D1;
299 h[i] = proc[i].phases();
300 entry[i] = mdd_entry_law(proc[i].pie, D1[i], h[i], i, "mdd_descriptor");
301 if (h[i] > 1 && servers[i] != 1)
302 throw InputError("mdd_descriptor: station " + std::to_string(i + 1) +
303 " has a phase-type service law and " + std::to_string(servers[i]) +
304 " servers; a multi-server or delay station would have to count jobs "
305 "per phase rather than name the phase of one job in service, which "
306 "this encoding does not carry");
307 if (h[i] > 1 && i < sched.size() && !sched[i].empty()) {
308 const std::string nm = detail::desc_upper(sched[i]);
309 if (detail::desc_is_preemptive_resume(nm))
310 throw InputError("mdd_descriptor: station " + std::to_string(i + 1) +
311 " combines a phase-type service law with a preemptive-resume "
312 "discipline; the suspended jobs' phases would have to be stacked "
313 "in the local state, which this encoding does not carry, and the "
314 "descriptor would silently model the non-preemptive chain "
315 "instead");
316 if (detail::desc_is_shared_server(nm))
317 throw InputError("mdd_descriptor: station " + std::to_string(i + 1) +
318 " combines a phase-type service law with a shared-server "
319 "discipline; every job present is in service and holds its own "
320 "phase, which this encoding does not carry. Use mdd_ps, whose "
321 "local state is the per-phase count vector.");
322 }
323 }
324
325 std::vector<int> d(K, 0);
326 for (std::size_t i = 0; i < K; ++i) d[i] = 1 + N * static_cast<int>(h[i]);
327
328 MddDescriptor<T> desc;
329 desc.K = K;
330 desc.N = N;
331 desc.domain = d;
332 desc.mu = mu;
333 desc.servers = servers;
334 desc.P = P;
335 desc.nphases = h;
336
337 // ---- index maps
338 desc.valuemap.assign(K, std::vector<double>());
339 for (std::size_t i = 0; i < K; ++i) {
340 desc.valuemap[i].assign(static_cast<std::size_t>(d[i]), 0.0);
341 for (int n = 1; n <= N; ++n)
342 for (std::size_t a = 0; a < h[i]; ++a)
343 desc.valuemap[i][static_cast<std::size_t>(1 + (n - 1) * static_cast<int>(h[i]) +
344 static_cast<int>(a))] =
345 static_cast<double>(n);
346 }
347
348 // ---- initial state: all jobs at station 1, in its entry phase
349 desc.init.assign(K, 0);
350 std::size_t first_phase = 0;
351 for (std::size_t a = 0; a < h[0]; ++a)
352 if (entry[0][a] > zero) {
353 first_phase = a;
354 break;
355 }
356 desc.init[0] = detail::desc_idx(N, static_cast<int>(first_phase) + 1, h[0]);
357
358 const std::vector<Matrix<T>> fD0 = D0, fD1 = D1;
359 const std::vector<std::vector<T>> fpie = entry;
360 const std::vector<std::size_t> fh = h;
361 const Matrix<T> fP = P;
362 desc.nextfun = [fD0, fD1, fpie, fh, fP](const std::vector<int>& state) {
363 return detail::desc_successors(state, fD0, fD1, fpie, fh, fP);
364 };
365
366 // ---- events: internal phase changes, one per phase-type station
367 for (std::size_t i = 0; i < K; ++i) {
368 if (h[i] == 1) continue;
369 const MddLocalMatrix<T> Wi = detail::desc_internal(D0[i], h[i], N, d[i]);
370 if (Wi.nnz == 0) continue;
371 MddEvent<T> ev;
372 ev.a = i;
373 ev.b = i;
374 ev.lev.push_back(i);
375 ev.W.push_back(Wi);
376 desc.events.push_back(ev);
377 }
378 // ---- events: completions routed a -> b
379 for (std::size_t a = 0; a < K; ++a)
380 for (std::size_t b = 0; b < K; ++b) {
381 if (a == b || !(P(a, b) > zero)) continue;
382 MddEvent<T> ev;
383 ev.a = a;
384 ev.b = b;
385 ev.lev.push_back(a);
386 ev.lev.push_back(b);
387 ev.W.push_back(detail::desc_departure(D1[a], entry[a], h[a], N, d[a], mu[a],
388 servers[a], P(a, b)));
389 ev.W.push_back(detail::desc_arrival(entry[b], h[b], N, d[b]));
390 desc.events.push_back(ev);
391 }
392 return desc;
393}
394
395} // namespace mdd
396} // namespace line
397
398#endif // LINE_API_MDD_MDD_DESCRIPTOR_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
The exception types the port throws.
Dense matrix and non-owning view.
The rate side of the decision-diagram domain: local matrices, events, the Kronecker descriptor,...
std::vector< T > mdd_entry_law(const std::vector< T > &given, const Matrix< T > &D1, std::size_t h, std::size_t i, const std::string &caller)
Entry law of a phase-type station, taken as given or derived from D1.
Definition mdd_types.h:268
MddDescriptor< T > mdd_descriptor(const std::vector< T > &mu, const Matrix< T > &P, const std::vector< double > &servers, int N, const std::vector< MddServiceLaw< T > > &proc=std::vector< MddServiceLaw< T > >(), const std::vector< std::string > &sched=std::vector< std::string >())
Build the descriptor.
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
MddNextState nextfun
Successor function over local indices.
Definition mdd_types.h:191
std::vector< int > domain
Local domain per level.
Definition mdd_types.h:171
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
std::vector< std::size_t > nphases
Phases per station, 1 when exponential.
Definition mdd_types.h:179
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< int > init
Initial local index per level.
Definition mdd_types.h:189
std::vector< MddEvent< T > > events
The events of the descriptor.
Definition mdd_types.h:193
std::size_t K
Number of levels, i.e.
Definition mdd_types.h:167
Matrix< T > P
Station-to-station routing matrix.
Definition mdd_types.h:177
One event of the Kronecker rate descriptor.
Definition mdd_types.h:124
std::size_t b
Station (or mode) the event arrives at, 0-based; equals a for an internal event.
Definition mdd_types.h:128
std::size_t a
Station (or transition node) the event departs from, 0-based.
Definition mdd_types.h:126
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
std::size_t nnz
Total number of stored nonzeros.
Definition mdd_types.h:56
Phase-type service law of one station, as a Markovian (D0,D1) pair.
Definition mdd_types.h:144