LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ctmc_state_space_logsize.h
Go to the documentation of this file.
1#ifndef LINE_API_MC_CTMC_STATE_SPACE_LOGSIZE_H
2#define LINE_API_MC_CTMC_STATE_SPACE_LOGSIZE_H
3
4/**
5 * @file
6 * @ingroup api_mc
7 * Worst-case log-size of the CTMC state space induced by a NetworkStruct.
8 *
9 * Port of `matlab/src/api/mc/ctmc_state_space_logsize.m`. The estimate is the
10 * product of five factors, summed in log space:
11 *
12 * 1. job placements: stars-and-bars C(n_k+M-1, M-1) per class over the
13 * stations that keep no ordered buffer, with an open class holding AT MOST
14 * cutoff jobs over the non-Source stations;
15 * 2. buffer orderings: a station outside the share family keeps the CLASS
16 * SEQUENCE of the jobs it holds, so with K>1 classes one occupancy vector
17 * is as many states as its sequences;
18 * 3. service phases: the phase count raised to the number of jobs that can be
19 * in service concurrently at the station;
20 * 4. routing state: one pointer over the outgoing links per (node, class)
21 * routed RROBIN or WRROBIN;
22 * 5. cache content: the ordered items held by each Cache node, times its
23 * retrieval-system subsets; its in-transit job slot is summed with the
24 * placements, taking a closed job out of its chain.
25 *
26 * It is computed in LOG space throughout because the quantity it exists to
27 * detect overflows a double: the `intractableCTMC` fixture (8 PS stations,
28 * N=400, Erlang-5) sits at exp(200), and a linear-space estimator would report
29 * `inf` for everything above exp(709) and lose the ability to rank one
30 * intractable model against another.
31 *
32 * This is the quantity fed to `ctmc_memory_gate`. It is separate from the gate
33 * so a caller such as SolverAUTO can screen CTMC out of a ranking without
34 * building the chain.
35 */
36
37#include <algorithm>
38#include <cmath>
39#include <cstddef>
40#include <string>
41#include <limits>
42#include <utility>
43#include <vector>
44
47
48namespace line {
49namespace mc {
50
51
52
53/** Largest (m_1..m_K) box the exact ordered-buffer DP will walk. */
54inline constexpr double kOrderGridMax = 1.0e6;
55
56/**
57 * Log count of (placement, ordering) configurations over ALL order-preserving
58 * stations at once, POPULATION CONSERVED. `caps_per[a][k]` bounds buffered class
59 * k at ordered station a, `cap_tot[a]` bounds the buffer TOTAL there (a finite
60 * station capacity is a slot count, not a per-class bound).
61 *
62 * The DP state is the population still to place per POPULATION UNIT, not per
63 * class: `nj_u[u]` is unit u's population, `b_unit_of[k]` the unit of buffered
64 * class k, and unit u spreads what the ordered stations leave over `m_rem_u[u]`
65 * share (station, class) slots. A unit is one class, or a closed chain whose
66 * jobs switch class, whose N jobs may then take any class mix at a station.
67 *
68 * Cutoff truncates an OPEN class's population in the network exactly as the
69 * plain stars-and-bars term treats it, so open classes are conserved too.
70 */
71inline double log_ordered_joint(const std::vector<std::vector<int>>& caps_per,
72 const std::vector<double>& cap_tot,
73 const std::vector<int>& nj_u,
74 const std::vector<std::size_t>& b_unit_of,
75 const std::vector<double>& m_rem_u,
76 const std::vector<std::vector<double>>& log_ph_per = {},
77 const std::vector<std::vector<int>>& lev_per = {}) {
78 const std::size_t Kb = b_unit_of.size();
79 const std::size_t U = nj_u.size();
80 std::vector<std::size_t> dims(U);
81 std::size_t nstate = 1;
82 for (std::size_t u = 0; u < U; ++u) { dims[u] = static_cast<std::size_t>(nj_u[u]) + 1; nstate *= dims[u]; }
83 const double NEG = -std::numeric_limits<double>::infinity();
84 auto idx_of = [&](const std::vector<int>& v) {
85 std::size_t ix = 0, mult = 1;
86 for (std::size_t u = 0; u < U; ++u) { ix += static_cast<std::size_t>(v[u]) * mult; mult *= dims[u]; }
87 return ix;
88 };
89 auto sub_of = [&](std::size_t ix) {
90 std::vector<int> v(U);
91 for (std::size_t u = 0; u < U; ++u) { v[u] = static_cast<int>(ix % dims[u]); ix /= dims[u]; }
92 return v;
93 };
94 std::vector<double> L(nstate, NEG);
95 L[idx_of(nj_u)] = 0.0;
96 for (std::size_t a = 0; a < caps_per.size(); ++a) {
97 std::vector<double> Ln(nstate, NEG);
98 // lev_per[a][k] is the priority level of buffered class k at a station that orders jobs only WITHIN a level
99 // (LCFSPRIO, State priority-canonical buffers); an empty row marks a station that orders all its jobs.
100 const bool by_level = a < lev_per.size() && lev_per[a].size() == Kb;
101 std::vector<int> ulev;
102 if (by_level) {
103 ulev = lev_per[a];
104 std::sort(ulev.begin(), ulev.end());
105 ulev.erase(std::unique(ulev.begin(), ulev.end()), ulev.end());
106 }
107 for (std::size_t si = 0; si < nstate; ++si) {
108 if (!std::isfinite(L[si])) continue;
109 const std::vector<int> rem = sub_of(si);
110 std::vector<int> av(Kb);
111 for (std::size_t k = 0; k < Kb; ++k) av[k] = std::min(caps_per[a][k], rem[b_unit_of[k]]);
112 std::vector<int> m(Kb, 0);
113 for (;;) {
114 int t = 0;
115 for (std::size_t k = 0; k < Kb; ++k) t += m[k];
116 if (!(std::isfinite(cap_tot[a]) && t > cap_tot[a])) {
117 double v = L[si];
118 if (!by_level) {
119 v += std::lgamma(static_cast<double>(t) + 1.0);
120 for (std::size_t k = 0; k < Kb; ++k) v -= std::lgamma(static_cast<double>(m[k]) + 1.0);
121 } else if (t > 0) {
122 // One job of SOME present class c is in service and the other t-1 wait in the canonical
123 // layout, ordered only within a level: sum over c of prod_l (t_l(m-e_c))! / prod_k (m-e_c)_k!.
124 // Counting the waiting line alone (no in-service choice) gave 49 against 85 enumerated on a
125 // two-class open M/M/1 at cutoff 6 (MATLAB ctmc_logOrderedJoint).
126 std::vector<double> lc;
127 for (std::size_t c = 0; c < Kb; ++c) {
128 if (m[c] <= 0) continue;
129 std::vector<int> mc = m;
130 mc[c] -= 1;
131 double x = 0.0;
132 for (std::size_t k = 0; k < Kb; ++k) x -= std::lgamma(static_cast<double>(mc[k]) + 1.0);
133 for (int l : ulev) {
134 int tl = 0;
135 for (std::size_t k = 0; k < Kb; ++k)
136 if (lev_per[a][k] == l) tl += mc[k];
137 x += std::lgamma(static_cast<double>(tl) + 1.0);
138 }
139 lc.push_back(x);
140 }
141 double top = lc[0];
142 for (double x : lc) top = std::max(top, x);
143 double acc = 0.0;
144 for (double x : lc) acc += std::exp(x - top);
145 v += top + std::log(acc);
146 }
147 if (a < log_ph_per.size())
148 for (std::size_t k = 0; k < Kb && k < log_ph_per[a].size(); ++k)
149 v += m[k] * log_ph_per[a][k];
150 // a unit's classes draw on ONE remaining population, so a class mix may overdraw it
151 std::vector<int> nx = rem;
152 bool fits = true;
153 for (std::size_t k = 0; k < Kb; ++k) nx[b_unit_of[k]] -= m[k];
154 for (std::size_t u = 0; u < U; ++u) if (nx[u] < 0) fits = false;
155 if (fits) {
156 const std::size_t di = idx_of(nx);
157 if (!std::isfinite(Ln[di])) Ln[di] = v;
158 else { const double mx = std::max(Ln[di], v);
159 Ln[di] = mx + std::log(std::exp(Ln[di] - mx) + std::exp(v - mx)); }
160 }
161 }
162 std::size_t pos = 0;
163 while (pos < Kb && m[pos] == av[pos]) { m[pos] = 0; ++pos; }
164 if (pos == Kb) break;
165 ++m[pos];
166 }
167 }
168 L.swap(Ln);
169 }
170 std::vector<double> terms;
171 for (std::size_t si = 0; si < nstate; ++si) {
172 if (!std::isfinite(L[si])) continue;
173 const std::vector<int> rem = sub_of(si);
174 double v = L[si];
175 bool stranded = false;
176 for (std::size_t u = 0; u < U; ++u) {
177 const double r = rem[u];
178 if (m_rem_u[u] >= 1.0)
179 v += std::lgamma(r + m_rem_u[u]) - std::lgamma(r + 1.0) - std::lgamma(m_rem_u[u]);
180 else if (rem[u] > 0)
181 stranded = true; // leftovers with no share slot to go to
182 }
183 if (stranded) continue;
184 terms.push_back(v);
185 }
186 if (terms.empty()) return NEG;
187 double top = terms[0];
188 for (double v : terms) top = std::max(top, v);
189 double acc = 0.0;
190 for (double v : terms) acc += std::exp(v - top);
191 return top + std::log(acc);
192}
193
194/** Options the estimator reads; only the cutoff matters. */
196 double cutoff = -1.0; ///< < 0 or non-finite = not given, take the solver default
197};
198
199/**
200 * Worst-case log state-space size of `sn`.
201 *
202 * @param sn the struct, ideally after `sn_nonmarkov_toph`, since a
203 * non-Markovian service becomes phases the raw struct does not carry
204 * @return natural log of the worst-case state count
205 */
206template <class T>
211
212 const std::size_t M = sn.nstations;
213 const std::size_t K = sn.nclasses;
214 if (M == 0 || K == 0) return 0.0;
215
216 // The analyzer's own default for an open or mixed model, so the estimate is
217 // taken against the space that would actually be built.
218 double cutoff = opt.cutoff;
219 if (!(cutoff > 0.0) || !std::isfinite(cutoff))
220 cutoff = std::ceil(std::pow(6000.0, 1.0 / static_cast<double>(M * K)));
221
222 // ORDERED BUFFERS, computed EXACTLY -- see the python twin for the measured
223 // numbers. Omitting it under-priced gallery_mmap1_multiclass 311x; BOUNDING
224 // it instead of computing it double counts and refused a working model.
225 const auto is_share = [](SchedStrategy s) {
226 return s == SchedStrategy::INF || s == SchedStrategy::PS || s == SchedStrategy::DPS ||
227 s == SchedStrategy::GPS || s == SchedStrategy::PSPRIO ||
228 s == SchedStrategy::DPSPRIO || s == SchedStrategy::GPSPRIO ||
229 s == SchedStrategy::LPS;
230 };
231 std::vector<bool> is_buffered(K, true);
232 std::size_t Kb = 0;
233 for (std::size_t k = 0; k < K; ++k) {
234 if (k < sn.issignal.size() && sn.issignal[k]) is_buffered[k] = false;
235 if (is_buffered[k]) ++Kb;
236 }
237 // PREEMPT FAMILY. A preempt-family station stores the phase of EVERY job it
238 // holds, the buffered ones as [class, phase] pairs, so a class-k job there is
239 // phasessz(i,k) states on its own. Pricing only the jobs in service read a
240 // FCFSPRPRIO queue with an Erlang-2 class as 6862 states at cutoff 6 against
241 // 148863 enumerated. Such a station enters the ordered DP even with a single
242 // buffered class, carrying the per-job phase weight there.
243 const auto is_preempt = [](SchedStrategy s) {
244 return s == SchedStrategy::FCFSPR || s == SchedStrategy::FCFSPI ||
245 s == SchedStrategy::LCFSPR || s == SchedStrategy::LCFSPI ||
246 s == SchedStrategy::FCFSPRPRIO || s == SchedStrategy::FCFSPIPRIO ||
247 s == SchedStrategy::LCFSPRPRIO || s == SchedStrategy::LCFSPIPRIO;
248 };
249 const auto is_ordered = [&](std::size_t i) {
250 const SchedStrategy s = sn.stations[i].sched;
251 return is_preempt(s) || (Kb > 1 && !(s == SchedStrategy::EXT || is_share(s)));
252 };
253 const auto log_phases = [&](std::size_t i, std::size_t k) {
254 const double p = static_cast<double>(sn.phasessz_of(i + 1, k + 1));
255 return (std::isfinite(p) && p > 1.0) ? std::log(p) : 0.0;
256 };
257 std::size_t n_ord = 0;
258 for (std::size_t i = 0; i < M; ++i)
259 if (is_ordered(i)) ++n_ord;
260
261 double log_nstates = 0.0;
262 std::vector<double> nk_eff(K, 0.0);
263 const std::vector<double>& njobs = sn.njobs();
264 for (std::size_t k = 0; k < K; ++k)
265 nk_eff[k] = (k < njobs.size() && std::isfinite(njobs[k])) ? njobs[k] : cutoff;
266
267 const auto place = [&](double nk, double ms) {
268 return std::lgamma(1.0 + nk + ms - 1.0) - std::lgamma(1.0 + ms - 1.0) -
269 std::lgamma(1.0 + nk);
270 };
271 // A ZERO per-class capacity means the class is DISABLED at that station, so it
272 // never occupies a slot and the placement term must spread it over the
273 // stations that admit it, not over all M. ld_whittle_bandwidth disables each
274 // of its three PS routes for the other two classes; counting all M=4 priced it
275 // at C(9,6)^3 = 592704 states, 7852 GB under the quadratic byte model, and the
276 // gate refused a model whose true space is 7^3 = 343 and solves at once.
277 const auto disabled_at = [&](std::size_t i, std::size_t k) {
278 return i < sn.classcap.size() && k < sn.classcap[i].size() && sn.classcap[i][k] == 0.0;
279 };
280 // ZERO VISITS IS THE SECOND WAY IN, and it is the state space generator's own
281 // rule: a (station, class) pair whose chain visit ratio is zero gets capacity
282 // 0 there, because a class the routing never delivers to a station cannot be
283 // there. Only an SPN PLACE escapes, keeping its declared initial marking, so a
284 // Place is left alone here too. Reading only classcap missed all of it, since
285 // that zeroes a pair whose SERVICE RATE is NaN -- the class is disabled at the
286 // station, not unrouted to it. A class that exists only as a switch target on
287 // some departure arc has a good service rate everywhere and a visit ratio of
288 // zero everywhere: a Source -> FCFS Queue -> Sink model with such a class was
289 // priced at 184755 states against the 11 the generator enumerates, predicted
290 // 17.62 GB, and had the gate refuse a model that solves in 0.05 s.
291 // Visit rows are STATEFUL-node indexed, not station indexed. A pair the chain
292 // never enters carries floating-point noise (~1e-17) rather than an exact 0,
293 // which is the tolerance below, shared with the generator.
294 const double kVisitZeroTol = 1.0e-12;
295 std::vector<std::vector<bool>> never(M, std::vector<bool>(K, false));
296 if (!sn.visits.empty() && !sn.chains.empty()) {
297 for (std::size_t i = 0; i < M; ++i) {
298 if (sn.stations[i].nodetype == lang::NodeType::Place) continue;
299 const std::size_t nd = sn.node_of_station(i + 1);
300 if (nd == 0) continue;
301 const std::size_t isf1 = sn.stateful_index(nd);
302 if (isf1 == 0) continue;
303 const std::size_t isf = isf1 - 1;
304 for (std::size_t k = 0; k < K; ++k) {
305 std::size_t c = sn.nchains;
306 for (std::size_t ci = 0; ci < sn.chains.size(); ++ci)
307 if (k < sn.chains[ci].size() && sn.chains[ci][k]) { c = ci; break; }
308 if (c >= sn.visits.size()) continue;
309 const auto& V = sn.visits[c];
310 if (isf >= V.rows() || k >= V.cols()) continue;
311 const double v = num_traits<T>::to_double(V(isf, k));
312 if (std::isfinite(v) && std::fabs(v) < kVisitZeroTol) never[i][k] = true;
313 }
314 }
315 }
316 const auto admits = [&](std::size_t i, std::size_t k) {
317 return !disabled_at(i, k) && !never[i][k];
318 };
319 const auto admitting_count = [&](std::size_t k) {
320 std::size_t n = 0;
321 for (std::size_t i = 0; i < M; ++i)
322 if (admits(i, k)) ++n;
323 return n;
324 };
325 const auto admitting_all = [&](std::size_t k) {
326 const std::size_t n = admitting_count(k);
327 return n < 1 ? std::size_t(1) : n;
328 };
329 const auto admitting_rem = [&](std::size_t k) {
330 std::size_t n = 0;
331 for (std::size_t i = 0; i < M; ++i) {
332 if (is_ordered(i)) continue;
333 if (admits(i, k)) ++n;
334 }
335 return n;
336 };
337 // A class no station admits holds no job anywhere, so its population is zero
338 // rather than the cutoff; leaving it at the cutoff made the class occupy
339 // ordered buffers it can never enter.
340 for (std::size_t k = 0; k < K; ++k)
341 if (nk_eff[k] > 0.0 && admitting_count(k) == 0) nk_eff[k] = 0.0;
342 // AN OPEN CLASS HOLDS AT MOST nk_eff JOBS, not exactly nk_eff: the generator enumerates 0..cutoff of them. The
343 // Source played that slack bin only for a class it emits; a class that exists only as a switch target downstream
344 // (a cache's hit and miss classes) has zero visits at the Source and was priced at ONE placement: an open LRU
345 // cache feeding a Delay and a PS queue read 24 states against 11616. So an open class takes C(n+M', M') over the
346 // M' non-Source stations admitting it, a closed one C(n+M-1, M-1) over all of them (MATLAB ctmc_placement_term).
347 const auto placement_term = [&](std::size_t k, double n) {
348 const bool is_open = !(k < njobs.size() && std::isfinite(njobs[k]));
349 if (!is_open) return place(n, static_cast<double>(admitting_all(k)));
350 std::size_t mopen = 0;
351 for (std::size_t i = 0; i < M; ++i)
352 if (sn.stations[i].sched != SchedStrategy::EXT && admits(i, k)) ++mopen;
353 const double mo = static_cast<double>(mopen);
354 return std::lgamma(1.0 + n + mo) - std::lgamma(1.0 + mo) - std::lgamma(1.0 + n);
355 };
356
357 // CLOSED CHAINS THAT SWITCH CLASS conserve their population per CHAIN, not per class: LINE gives the reference
358 // class all N jobs and the switch targets none, so per-class terms placed N jobs over the reference class's
359 // stations and NONE over the targets'. A closed cache loop (Delay -> Cache -> Delay, hit/miss classes back, 10
360 // items capacity 3) read 2880 states against 4320, a closed switching tandem 4 against 10. Such a chain is ONE
361 // population unit: its N jobs spread over every admitted (station, class) pair, C(N+P-1, P-1), and at an ordered
362 // station the DP sums over the chain's class mixes there. Each class of the chain may hold up to N jobs, which
363 // also bounds its phase and capacity terms (MATLAB ctmc_state_space_logsize.m, _kb/06).
364 std::vector<std::size_t> unit_of(K, 0);
365 std::vector<std::vector<std::size_t>> unit_cls;
366 for (std::size_t ci = 0; ci < sn.chains.size(); ++ci) {
367 std::vector<std::size_t> cls;
368 for (std::size_t k = 0; k < K && k < sn.chains[ci].size(); ++k)
369 if (sn.chains[ci][k] && k < njobs.size() && std::isfinite(njobs[k]) && admitting_count(k) > 0)
370 cls.push_back(k);
371 if (cls.size() > 1) {
372 double ntot = 0.0;
373 for (std::size_t k : cls) ntot += njobs[k];
374 unit_cls.push_back(cls);
375 for (std::size_t k : cls) { nk_eff[k] = ntot; unit_of[k] = unit_cls.size(); }
376 }
377 }
378 for (std::size_t k = 0; k < K; ++k)
379 if (unit_of[k] == 0) { unit_cls.push_back(std::vector<std::size_t>{k}); unit_of[k] = unit_cls.size(); }
380 // unit_of is 1-based from here on; unit_cls[unit_of[k] - 1] holds class k
381 const auto unit_closed = [&](std::size_t u) {
382 const std::size_t k = unit_cls[u][0];
383 return k < njobs.size() && std::isfinite(njobs[k]);
384 };
385
386 // Placements and buffer orderings with per-class populations nk.
387 const auto placement_block = [&](const std::vector<double>& nk) -> double {
388 double lp = 0.0;
389 if (n_ord == 0) {
390 for (std::size_t u = 0; u < unit_cls.size(); ++u) {
391 const std::vector<std::size_t>& cls = unit_cls[u];
392 if (cls.size() == 1) { lp += placement_term(cls[0], nk[cls[0]]); continue; }
393 const double n = nk[cls[0]];
394 double P = 0.0;
395 for (std::size_t k : cls) P += static_cast<double>(admitting_count(k));
396 P = std::max(P, 1.0);
397 lp += std::lgamma(n + P) - std::lgamma(P) - std::lgamma(n + 1.0);
398 }
399 return lp;
400 }
401 for (std::size_t k = 0; k < K; ++k)
402 if (!is_buffered[k]) lp += placement_term(k, nk[k]);
403 std::vector<int> njb;
404 std::vector<std::size_t> bunits; // 0-based unit ids, first-seen order over the buffered classes
405 std::vector<std::size_t> b_unit_of;
406 for (std::size_t k = 0; k < K; ++k) {
407 if (!is_buffered[k]) continue;
408 njb.push_back(static_cast<int>(std::floor(nk[k])));
409 const std::size_t u = unit_of[k] - 1;
410 std::size_t bu = 0;
411 while (bu < bunits.size() && bunits[bu] != u) ++bu;
412 if (bu == bunits.size()) bunits.push_back(u);
413 b_unit_of.push_back(bu);
414 }
415 std::vector<int> nj_u;
416 std::vector<double> m_rem_u;
417 double grid = 1.0;
418 for (std::size_t u : bunits) {
419 const int n = static_cast<int>(std::floor(nk[unit_cls[u][0]]));
420 nj_u.push_back(n);
421 grid *= static_cast<double>(n) + 1.0;
422 double mr = 0.0;
423 for (std::size_t k : unit_cls[u]) mr += static_cast<double>(admitting_rem(k));
424 m_rem_u.push_back(mr);
425 }
426 if (grid <= kOrderGridMax) {
427 std::vector<std::vector<int>> caps_per;
428 std::vector<std::vector<double>> log_ph_per;
429 std::vector<std::vector<int>> lev_per;
430 std::vector<double> cap_tot;
431 for (std::size_t i = 0; i < M; ++i) {
432 if (!is_ordered(i)) continue;
433 std::vector<double> lph;
434 for (std::size_t k = 0; k < K; ++k)
435 if (is_buffered[k]) lph.push_back(is_preempt(sn.stations[i].sched) ? log_phases(i, k) : 0.0);
436 log_ph_per.push_back(lph);
437 std::vector<int> lev;
438 if (sn.stations[i].sched == SchedStrategy::LCFSPRIO)
439 for (std::size_t k = 0; k < K; ++k)
440 if (is_buffered[k]) lev.push_back(k < sn.classes.size() ? sn.classes[k].prio : 0);
441 lev_per.push_back(lev);
442 std::vector<int> per;
443 std::size_t bi = 0;
444 for (std::size_t k = 0; k < K; ++k)
445 if (is_buffered[k]) {
446 int c = njb[bi];
447 if (i < sn.classcap.size() && k < sn.classcap[i].size() &&
448 std::isfinite(sn.classcap[i][k]))
449 c = std::min(c, static_cast<int>(std::floor(sn.classcap[i][k])));
450 if (!admits(i, k)) c = 0;
451 per.push_back(c);
452 ++bi;
453 }
454 caps_per.push_back(per);
455 const double c = sn.stations[i].cap;
456 cap_tot.push_back(std::isfinite(c) && c >= 0
457 ? std::floor(c) : std::numeric_limits<double>::infinity());
458 }
459 return lp + log_ordered_joint(caps_per, cap_tot, nj_u, b_unit_of, m_rem_u, log_ph_per, lev_per);
460 }
461 double total = 0.0;
462 for (int c : njb) total += c;
463 const std::size_t nb = njb.size();
464 if (nb > 1) { // one class has a single sequence; only a preempt station gets here with it
465 const double lkb = std::log(static_cast<double>(nb));
466 lp += static_cast<double>(n_ord) *
467 ((total + 1.0) * lkb - std::log(static_cast<double>(nb) - 1.0) +
468 std::log1p(-std::exp(-(total + 1.0) * lkb)));
469 }
470 for (std::size_t i = 0; i < M; ++i)
471 if (is_preempt(sn.stations[i].sched))
472 for (std::size_t k = 0; k < K; ++k)
473 if (is_buffered[k]) lp += nk[k] * log_phases(i, k);
474 for (std::size_t k = 0; k < K; ++k)
475 if (is_buffered[k]) {
476 const std::size_t mk = admitting_rem(k);
477 if (mk >= 1) lp += place(nk[k], static_cast<double>(mk));
478 }
479 return lp;
480 };
481
482 // A CACHE'S IN-TRANSIT SLOT holds no job or one job of a class visiting the cache. A closed job held there is
483 // NOT at a station, so it is taken out of its chain's population and the placements are SUMMED over the slot
484 // states; multiplying them by (1 + nin) counted that job twice and read the closed cache loop at 2x truth. An
485 // open job's truncated population is not reduced, as the generator enumerates it.
486 std::vector<std::vector<std::size_t>> transit; // per Cache node, the visiting classes
487 for (const auto& kv : sn.nodeparam) {
488 const std::size_t ind = kv.first;
489 if (ind == 0 || ind > sn.nodes.size() || sn.nodes[ind - 1].nodetype != lang::NodeType::Cache) continue;
490 if (kv.second.nitems == 0) continue;
491 std::vector<std::size_t> cls;
492 const std::size_t isf1 = sn.stateful_index(ind);
493 if (isf1 > 0 && !sn.visits.empty()) {
494 for (std::size_t k = 0; k < K; ++k) {
495 std::size_t c = sn.chains.size();
496 for (std::size_t ci = 0; ci < sn.chains.size(); ++ci)
497 if (k < sn.chains[ci].size() && sn.chains[ci][k]) { c = ci; break; }
498 if (c >= sn.visits.size()) continue;
499 const auto& V = sn.visits[c];
500 if (isf1 - 1 >= V.rows() || k >= V.cols()) continue;
501 if (num_traits<T>::to_double(V(isf1 - 1, k)) > kVisitZeroTol) cls.push_back(k);
502 }
503 }
504 transit.push_back(cls);
505 }
506 // per cache: (unit + 1, weight) options, unit 0 = empty slot of weight 1; weight = #visiting classes of the unit
507 std::vector<std::vector<std::pair<std::size_t, double>>> topts;
508 double combos = 1.0;
509 for (const auto& cls : transit) {
510 std::vector<std::pair<std::size_t, double>> o{{0, 1.0}};
511 for (std::size_t k : cls) {
512 const std::size_t u1 = unit_of[k];
513 bool found = false;
514 for (auto& e : o)
515 if (e.first == u1) { e.second += 1.0; found = true; }
516 if (!found) o.push_back({u1, 1.0});
517 }
518 combos *= static_cast<double>(o.size());
519 topts.push_back(o);
520 }
521 if (transit.empty() || combos > 4096.0) {
522 log_nstates += placement_block(nk_eff);
523 for (const auto& cls : transit) log_nstates += std::log(1.0 + static_cast<double>(cls.size()));
524 } else {
525 std::vector<double> vals;
526 std::vector<std::size_t> pick(topts.size(), 0);
527 for (;;) {
528 std::vector<double> nk = nk_eff;
529 double lw = 0.0;
530 bool ok = true;
531 for (std::size_t t = 0; t < topts.size(); ++t) {
532 const std::size_t u1 = topts[t][pick[t]].first;
533 if (u1 == 0) continue;
534 lw += std::log(topts[t][pick[t]].second);
535 const std::size_t u = u1 - 1;
536 if (unit_closed(u)) {
537 for (std::size_t k : unit_cls[u]) nk[k] -= 1.0;
538 if (nk[unit_cls[u][0]] < 0.0) ok = false;
539 }
540 }
541 if (ok) vals.push_back(lw + placement_block(nk));
542 std::size_t pos = 0;
543 while (pos < topts.size() && pick[pos] + 1 == topts[pos].size()) { pick[pos] = 0; ++pos; }
544 if (pos == topts.size()) break;
545 ++pick[pos];
546 }
547 double top = vals[0];
548 for (double v : vals) top = std::max(top, v);
549 double acc = 0.0;
550 for (double v : vals) acc += std::exp(v - top);
551 log_nstates += top + std::log(acc);
552 }
553
554 // A sharing discipline can hold every job in service at once; a queueing one
555 // holds at most its server count.
556 for (std::size_t i = 1; i <= M; ++i) {
557 const SchedStrategy sched = sn.stations[i - 1].sched;
558 const bool share = sched == SchedStrategy::INF || sched == SchedStrategy::PS ||
559 sched == SchedStrategy::DPS || sched == SchedStrategy::GPS ||
560 sched == SchedStrategy::PSPRIO || sched == SchedStrategy::DPSPRIO ||
561 sched == SchedStrategy::GPSPRIO || sched == SchedStrategy::LPS;
562 if (is_preempt(sched)) continue; // every job's phase is already in the ordered term
563 for (std::size_t r = 1; r <= K; ++r) {
564 const double p = static_cast<double>(sn.phasessz_of(i, r));
565 if (!std::isfinite(p) || p <= 1.0 || !admits(i - 1, r - 1)) continue; // no job, no phase to keep
566 double m;
567 if (sched == SchedStrategy::EXT)
568 m = 1.0;
569 else if (share)
570 m = nk_eff[r - 1];
571 else
572 m = std::min(nk_eff[r - 1], sn.stations[i - 1].nservers);
573 if (!std::isfinite(m)) m = nk_eff[r - 1];
574 log_nstates += std::lgamma(1.0 + m + p - 1.0) - std::lgamma(1.0 + p - 1.0) -
575 std::lgamma(1.0 + m);
576 }
577 }
578
579 // Round-robin routing is stateful: the pointer over the outgoing links is
580 // part of the state. The reference reads `sn.connmatrix`; this port has no
581 // such field and takes the out-degree from `rtnodes`, the same graph after
582 // the refresh resolved the routing strategies.
583 for (std::size_t ind = 1; ind <= sn.nodes.size(); ++ind) {
584 const std::vector<RoutingStrategy>& rt_i = sn.nodes[ind - 1].routing;
585 std::size_t nrr = 0;
586 for (std::size_t r = 0; r < K && r < rt_i.size(); ++r)
587 if (rt_i[r] == RoutingStrategy::RROBIN || rt_i[r] == RoutingStrategy::WRROBIN) ++nrr;
588 if (nrr == 0) continue;
589 const std::size_t nout = sn.downstream_stations(ind).size();
590 if (nout <= 1) continue;
591 log_nstates += static_cast<double>(nrr) * std::log(static_cast<double>(nout));
592 }
593
594 // 5. cache content: a Cache node keeps the ORDERED placement of C = sum(itemcap) distinct items out of n,
595 // n!/(n-C)! local states, times the subsets of at most retrieval_capacity of the other n-C items in its
596 // retrieval system. Leaving it out priced an open LRU cache (10 items, capacity 3) as if it had one local
597 // state and let a chain 720 times larger run.
598 for (const auto& kv : sn.nodeparam) {
599 const std::size_t ind = kv.first;
600 if (ind == 0 || ind > sn.nodes.size() || sn.nodes[ind - 1].nodetype != lang::NodeType::Cache) continue;
601 const qn::CacheParam<T>& cp = kv.second;
602 if (cp.nitems == 0) continue;
603 const double n = static_cast<double>(cp.nitems);
604 double csum = 0.0;
605 for (int c : cp.itemcap) csum += c;
606 const double C = std::min(csum, n);
607 log_nstates += std::lgamma(n + 1.0) - std::lgamma(n - C + 1.0);
608 // the job in transit is priced with the placements above, summed over the slot states
609 if (cp.retrieval_capacity > 0) {
610 const double rest = n - C;
611 const double smax = std::min(static_cast<double>(cp.retrieval_capacity), rest);
612 std::vector<double> lt;
613 for (double sv = 0.0; sv <= smax; sv += 1.0)
614 lt.push_back(std::lgamma(rest + 1.0) - std::lgamma(sv + 1.0) - std::lgamma(rest - sv + 1.0));
615 double top = lt[0];
616 for (double v : lt) top = std::max(top, v);
617 double acc = 0.0;
618 for (double v : lt) acc += std::exp(v - top);
619 log_nstates += top + std::log(acc);
620 }
621 }
622
623 return log_nstates;
624}
625
626} // namespace mc
627} // namespace line
628
629#endif // LINE_API_MC_CTMC_STATE_SPACE_LOGSIZE_H
A network plus its refreshed NetworkStruct.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:185
RoutingStrategy
Routing strategies, with the values of MATLAB RoutingStrategy.
Definition lang_types.h:396
double log_ordered_joint(const std::vector< std::vector< int > > &caps_per, const std::vector< double > &cap_tot, const std::vector< int > &nj_u, const std::vector< std::size_t > &b_unit_of, const std::vector< double > &m_rem_u, const std::vector< std::vector< double > > &log_ph_per={}, const std::vector< std::vector< int > > &lev_per={})
Log count of (placement, ordering) configurations over ALL order-preserving stations at once,...
double ctmc_state_space_logsize(const qn::NetworkStruct< T > &sn, const CtmcSizeOptions &opt=CtmcSizeOptions())
Worst-case log state-space size of sn.
constexpr double kOrderGridMax
Largest (m_1..m_K) box the exact ordered-buffer DP will walk.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
Options the estimator reads; only the cutoff matters.
double cutoff
< 0 or non-finite = not given, take the solver default
The parameters of a Cache node, MATLAB's sn.nodeparam{ind} for a Cache.
int retrieval_capacity
Delayed-hit retrieval system (Cache.setRetrievalSystem).
std::vector< int > itemcap