LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_ctmc_pools.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_SOLVERS_CTMC_SOLVER_CTMC_POOLS_H
6#define LINE_SOLVERS_CTMC_SOLVER_CTMC_POOLS_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `solver_ctmc_pools.m`: rewrite every heterogeneous-server station
12 * (`Queue.addServerType`) into the form the CTMC state space enumerates exactly,
13 * following the LDES semantics.
14 *
15 * A class-r job in service at a pooled station occupies one server of ONE
16 * compatible pool t and is served by that pool's law, or by the station's own
17 * law for r when the pool declares none. The per-class service process becomes a
18 * block-diagonal phase-type law with one block per compatible pool, in ascending
19 * pool order, so a phase of the class-r server block names both the pool and the
20 * service phase. The bookkeeping read by `qn::after_event_station_pool` and
21 * `qn::from_marginal_pool` goes to `sn.ctmcpool`. Under ALIS and FAIRNESS, when
22 * some class has two or more compatible pools, the global rotating pool order is
23 * part of the state: its index into `perms` is the station's single local
24 * variable (nvars column 2R+1). The PHASE synchronizations a multi-phase pool law
25 * needs follow from `refresh_sync`, which reads the rewritten laws.
26 */
27
28#include <algorithm>
29#include <cmath>
30#include <cstddef>
31#include <string>
32#include <vector>
33
38#include "line/util/error.h"
39#include "line/util/matrix.h"
40
41namespace line {
42namespace ctmc {
43
44namespace pools_detail {
45
46/** A renewal phase-type law is what a pool server can carry; anything else is refused by name. */
47template <class T>
48void require_ph(const lang::Distrib<T>& d, const mam::Map<T>& m, const std::string& name,
49 const std::string& cname, const std::string& what) {
50 const lang::ProcessType pt = d.type;
54 throw UnsupportedError(
55 "SolverCTMC serves a heterogeneous server pool with a renewal phase-type law; class '" +
56 cname + "' at station '" + name + "' has a " + std::string(lang::process_to_text(pt)) +
57 " law in " + what + ".");
58 const std::size_t n = m.D0.rows();
59 const std::vector<T> a = mam::map_pie(m);
60 double asum = 0, d1n = 0, err = 0;
61 for (std::size_t j = 0; j < n; ++j) asum += num_traits<T>::to_double(a[j]);
62 asum = std::max(asum, lang::GlobalConstants::FineTol);
63 bool bad = false;
64 for (std::size_t i = 0; i < n; ++i) {
65 double ex = 0;
66 for (std::size_t j = 0; j < n; ++j) ex -= num_traits<T>::to_double(m.D0(i, j));
67 if (ex < -lang::GlobalConstants::FineTol) bad = true;
68 for (std::size_t j = 0; j < n; ++j) {
69 if (i != j && num_traits<T>::to_double(m.D0(i, j)) < -lang::GlobalConstants::FineTol) bad = true;
70 const double d1 = num_traits<T>::to_double(m.D1(i, j));
71 d1n += std::fabs(d1);
72 err += std::fabs(d1 - ex * num_traits<T>::to_double(a[j]) / asum);
73 }
74 }
75 if (bad || err > lang::GlobalConstants::CoarseTol * std::max(1.0, d1n))
76 throw UnsupportedError(
77 "SolverCTMC serves a heterogeneous server pool with a renewal phase-type law; class '" +
78 cname + "' at station '" + name + "' has a correlated or matrix-exponential law in " +
79 what + ".");
80}
81
82/** The features a pooled station cannot be combined with, `sub_refuse_features` of the reference. */
83template <class T>
84void refuse_features(const qn::NetworkStruct<T>& sn, std::size_t ind, std::size_t ist) {
85 const std::size_t R = sn.nclasses;
86 const qn::Station<T>& st = sn.stations[ist - 1];
87 std::string why;
88 bool lld = false;
89 for (std::size_t j = 0; j < st.lldscaling.size(); ++j)
90 if (num_traits<T>::to_double(st.lldscaling[j]) != 1.0) lld = true;
91 bool retrial = false;
92 const auto rit = sn.retrialparam.find(ist);
93 if (rit != sn.retrialparam.end())
94 for (std::size_t r = 0; r < rit->second.retrial_proc.size(); ++r)
95 if (!rit->second.retrial_proc[r].disabled) retrial = true;
96 bool balk = false, reneg = false, immf = false, reply = false, par = false, rr = false,
97 sig = false;
98 for (std::size_t r = 0; r < st.balking.size(); ++r)
99 if (st.balking[r].strategy != lang::BalkingStrategy::NONE) balk = true;
100 for (std::size_t r = 0; r < st.impatience.size(); ++r)
101 if (st.impatience[r] != lang::ImpatienceType::NONE) reneg = true;
102 if (sn.immfeed.size() >= ist)
103 for (std::size_t r = 0; r < sn.immfeed[ist - 1].size(); ++r)
104 if (sn.immfeed[ist - 1][r]) immf = true;
105 if (sn.replyblock.size() >= ind)
106 for (std::size_t r = 0; r < sn.replyblock[ind - 1].size(); ++r)
107 if (sn.replyblock[ind - 1][r]) reply = true;
108 for (std::size_t r = 0; r < st.server_parallelism.size(); ++r)
109 if (st.server_parallelism[r] > 1) par = true;
110 const std::vector<lang::RoutingStrategy>& rt = sn.nodes[ind - 1].routing;
111 for (std::size_t r = 0; r < rt.size(); ++r)
113 rr = true;
114 for (std::size_t s = 0; s < sn.issignal.size() && s < R && !sig; ++s) {
115 if (!sn.issignal[s]) continue;
116 const std::size_t col = (ind - 1) * R + s;
117 for (std::size_t i = 0; i < sn.rtnodes.rows() && col < sn.rtnodes.cols(); ++i)
118 if (num_traits<T>::to_double(sn.rtnodes(i, col)) > 0) { sig = true; break; }
119 }
120 if (lld) why = "load-dependent service";
121 else if (st.cdscaling) why = "class-dependent service";
122 else if (st.jdscaling) why = "joint-dependent service";
123 else if (static_cast<bool>(sn.gdscaling)) why = "global dependence";
124 else if (sn.breakdownparam.count(ist)) why = "server breakdowns";
125 else if (retrial) why = "retrial";
126 else if (balk) why = "balking";
127 else if (reneg) why = "reneging";
128 else if (sn.isbasblocking.size() >= ind && sn.isbasblocking[ind - 1]) why = "BAS blocking";
129 else if (immf) why = "immediate feedback";
130 else if (reply) why = "synchronous calls";
131 else if (par) why = "server parallelism";
132 else if (rr) why = "round-robin routing";
133 else if (sig) why = "signals";
134 if (!why.empty())
135 throw UnsupportedError("SolverCTMC does not combine heterogeneous server pools with " + why +
136 " (station '" + sn.nodes[ind - 1].name + "').");
137}
138
139/** Rewrite one station; the struct is the solver's private copy. */
140template <class T>
141void pool_station(qn::NetworkStruct<T>& sn, std::size_t ind, std::size_t ist) {
142 const std::size_t R = sn.nclasses;
143 qn::Station<T>& st = sn.stations[ist - 1];
144 const std::string& name = sn.nodes[ind - 1].name;
145 const std::size_t NT = st.server_types.size();
146 switch (st.sched) {
152 break;
153 default:
154 throw UnsupportedError(
155 "SolverCTMC supports heterogeneous server pools under FCFS, HOL, FCFSPRIO, LCFS, "
156 "LCFSPRIO and SIRO; station '" + name + "' uses " +
157 std::string(lang::sched_to_text(st.sched)) + ".");
158 }
159 refuse_features(sn, ind, ist);
160 if (sn.nvars_of(ind) > 0)
161 throw UnsupportedError(
162 "Station '" + name + "' combines heterogeneous server pools with another feature that "
163 "owns a local variable (polling, BAS blocking, breakdown, a modulated process); "
164 "SolverCTMC cannot represent both.");
165 qn::CtmcPool<T> pool;
166 pool.ntypes = NT;
167 pool.policy = st.hetero_policy;
168 pool.count.assign(NT, 0.0);
169 pool.compat.assign(NT, std::vector<bool>(R, false));
170 for (std::size_t t = 0; t < NT; ++t) {
171 pool.count[t] = st.server_types[t].count;
172 const std::vector<bool>& c = st.server_types[t].compatible;
173 for (std::size_t r = 0; r < R; ++r) pool.compat[t][r] = c.empty() || (r < c.size() && c[r]);
174 }
175 pool.pools.assign(R, std::vector<std::size_t>());
176 pool.off.assign(R, std::vector<std::size_t>());
177 pool.len.assign(R, std::vector<std::size_t>());
178 pool.alpha.assign(R, std::vector<std::vector<T>>());
179 pool.exit.assign(R, std::vector<std::vector<T>>());
180 pool.D0.assign(R, std::vector<Matrix<T>>());
181 pool.fsfrate.assign(NT, std::vector<double>(R, 0.0));
182 const T zero = num_traits<T>::from_int(0);
183 for (std::size_t r = 0; r < R; ++r) {
184 const lang::Distrib<T>& base = sn.service[ist - 1][r];
185 const bool baseOk = !base.disabled && base.type != lang::ProcessType::DISABLED;
186 bool hasPoolLaw = false;
187 for (std::size_t t = 0; t < NT; ++t) {
188 const std::vector<lang::Distrib<T>>& sv = st.server_types[t].service;
189 hasPoolLaw = hasPoolLaw || (pool.compat[t][r] && r < sv.size() && !sv[r].disabled);
190 }
191 if (!baseOk && !hasPoolLaw) continue; // the class is not served here
192 for (std::size_t t = 0; t < NT; ++t)
193 if (pool.compat[t][r]) pool.pools[r].push_back(t + 1);
194 const std::string& cname = sn.classes[r].name;
195 if (pool.pools[r].empty())
196 throw UnsupportedError("Station '" + name +
197 "' declares no server pool compatible with class '" + cname +
198 "', so a job of that class would wait forever.");
199 mam::Map<T> basemap;
200 if (baseOk) {
201 basemap = lang::dist_to_map(base);
202 require_ph(base, basemap, name, cname, "its default service");
203 }
204 std::vector<mam::Map<T>> blocks;
205 std::size_t shift = 0;
206 for (std::size_t k = 0; k < pool.pools[r].size(); ++k) {
207 const std::size_t t = pool.pools[r][k];
208 const std::vector<lang::Distrib<T>>& sv = st.server_types[t - 1].service;
209 mam::Map<T> law;
210 if (r < sv.size() && !sv[r].disabled) {
211 law = lang::dist_to_map(sv[r]);
212 require_ph(sv[r], law, name, cname, "pool '" + st.server_types[t - 1].name + "'");
213 } else {
214 if (!baseOk)
215 throw UnsupportedError(
216 "Server pool '" + st.server_types[t - 1].name + "' of station '" + name +
217 "' accepts class '" + cname + "' but declares no law for it, and the "
218 "station's own service for the class is disabled.");
219 law = basemap;
220 }
221 const std::size_t n = law.D0.rows();
222 std::vector<T> a = mam::map_pie(law);
223 T as = zero;
224 for (std::size_t j = 0; j < n; ++j) as += a[j];
225 for (std::size_t j = 0; j < n; ++j) a[j] = T(a[j] / as);
226 std::vector<T> ex(n, zero);
227 for (std::size_t i = 0; i < n; ++i)
228 for (std::size_t j = 0; j < n; ++j) ex[i] -= law.D0(i, j);
229 pool.off[r].push_back(shift);
230 pool.len[r].push_back(n);
231 pool.alpha[r].push_back(a);
232 pool.exit[r].push_back(ex);
233 pool.D0[r].push_back(law.D0);
234 pool.fsfrate[t - 1][r] = 1.0 / num_traits<T>::to_double(mam::map_mean(law));
235 blocks.push_back(law);
236 shift += n;
237 }
238 Matrix<T> D0x(shift, shift, zero), D1x(shift, shift, zero);
239 std::vector<T> exx(shift, zero);
240 for (std::size_t k = 0; k < blocks.size(); ++k) {
241 const std::size_t o = pool.off[r][k];
242 for (std::size_t i = 0; i < pool.len[r][k]; ++i) {
243 exx[o + i] = pool.exit[r][k][i];
244 for (std::size_t j = 0; j < pool.len[r][k]; ++j) D0x(o + i, o + j) = blocks[k].D0(i, j);
245 }
246 }
247 // a completion restarts in the first block's entry law; which block a job takes
248 // is decided by the event handler, so this D1 only fixes the renewal form
249 for (std::size_t i = 0; i < shift; ++i)
250 for (std::size_t j = 0; j < pool.len[r][0]; ++j) D1x(i, j) = T(exx[i] * pool.alpha[r][0][j]);
253 d.disabled = false;
254 d.D0 = D0x;
255 d.D1 = D1x;
257 sn.service[ist - 1][r] = d;
258 if (sn.disabled.size() >= ist && sn.disabled[ist - 1].size() > r) sn.disabled[ist - 1][r] = false;
259 }
260 std::vector<std::size_t> ncls(NT, 0);
261 for (std::size_t t = 0; t < NT; ++t)
262 for (std::size_t r = 0; r < R; ++r) ncls[t] += pool.compat[t][r] ? 1 : 0;
263 pool.alfsorder.resize(NT);
264 for (std::size_t t = 0; t < NT; ++t) pool.alfsorder[t] = t + 1;
265 std::stable_sort(pool.alfsorder.begin(), pool.alfsorder.end(),
266 [&](std::size_t a, std::size_t b) { return ncls[a - 1] < ncls[b - 1]; });
267 bool multi = false;
268 for (std::size_t r = 0; r < R; ++r) multi = multi || pool.pools[r].size() >= 2;
271 if (pool.rotate) {
272 std::vector<std::size_t> p(NT);
273 for (std::size_t t = 0; t < NT; ++t) p[t] = t + 1;
274 do {
275 pool.perms.push_back(p); // lexicographic, identity first
276 } while (std::next_permutation(p.begin(), p.end()));
277 sn.nvars[ind - 1][2 * R] = 1;
278 }
279 double total = 0;
280 for (std::size_t t = 0; t < NT; ++t) total += pool.count[t];
281 st.nservers = total; // the pools are the server bank, as in LDES
282 sn.ctmcpool[ind] = pool;
283}
284
285} // namespace pools_detail
286
287/** True when the struct has a heterogeneous-server station that `ctmc_pools` rewrites. */
288template <class T>
290 for (std::size_t ind = 1; ind <= sn.nodes.size(); ++ind) {
291 const std::size_t ist = sn.nodes[ind - 1].station;
292 if (ist == 0 || sn.stations[ist - 1].server_types.empty() || sn.ctmcpool.count(ind)) continue;
293 const lang::SchedStrategy s = sn.stations[ist - 1].sched;
294 // PAS/OI model heterogeneous compatible servers through the OI rank rate, not pools
295 if (s == lang::SchedStrategy::PAS || s == lang::SchedStrategy::OI) continue;
296 return true;
297 }
298 return false;
299}
300
301/**
302 * Rewrite a copy of `in` into `out` when it has pooled stations; false, leaving
303 * `out` untouched, when there is nothing to rewrite. Idempotent: a station that
304 * already carries `ctmcpool` is skipped. A declared initial state at a pooled
305 * station is carried as its marginal and rebuilt in the pooled layout.
306 */
307template <class T>
309 if (!ctmc_has_pools(in)) return false;
310 if (in.isfjaugmented)
311 throw UnsupportedError(
312 "SolverCTMC does not combine heterogeneous server pools with fork-join: the tag "
313 "augmentation adds sibling classes the pool compatibility does not name.");
314 out = in;
315 const std::size_t R = in.nclasses;
316 for (std::size_t ind = 1; ind <= in.nodes.size(); ++ind) {
317 const std::size_t ist = in.nodes[ind - 1].station;
318 if (ist == 0 || in.stations[ist - 1].server_types.empty() || in.ctmcpool.count(ind)) continue;
319 const lang::SchedStrategy s = in.stations[ist - 1].sched;
320 if (s == lang::SchedStrategy::PAS || s == lang::SchedStrategy::OI) continue;
321 pools_detail::pool_station(out, ind, ist);
322 const auto sp = in.statespace.find(ind);
323 if (sp == in.statespace.end() || sp->second.rows() == 0) continue;
324 if (sp->second.rows() > 1)
325 throw UnsupportedError("SolverCTMC cannot place a distribution over initial states on "
326 "the heterogeneous server pools of station '" +
327 in.nodes[ind - 1].name + "'.");
328 std::vector<T> row(sp->second.cols());
329 for (std::size_t c = 0; c < sp->second.cols(); ++c) row[c] = sp->second(0, c);
330 const std::pair<T, std::vector<T>> mg = qn::to_marginal_aggr(in, ind, row);
332 for (std::size_t r = 0; r < R; ++r) m(0, r) = mg.second[r];
333 out.statespace[ind] = m;
334 }
335 return true;
336}
337
338/** Utilization at a pooled station: the mean number of busy servers of the class over the bank. */
339template <class T>
340bool ctmc_pool_station(const qn::NetworkStruct<T>& sn, std::size_t ist) {
341 const std::size_t ind = sn.node_of_station(ist);
342 return ind != 0 && !sn.ctmcpool.empty() && sn.ctmcpool.count(ind) != 0;
343}
344
345} // namespace ctmc
346} // namespace line
347
348#endif // LINE_SOLVERS_CTMC_SOLVER_CTMC_POOLS_H
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
std::vector< Station< T > > stations
stations[k-1] is the k-th station
std::vector< NodeDef > nodes
every node, in creation order
std::map< std::size_t, CtmcPool< T > > ctmcpool
Heterogeneous server pools rewritten for the CTMC, by 1-based NODE index.
std::map< std::size_t, Matrix< T > > statespace
bool isfjaugmented
sn.isfjaugmented: this struct came out of fj_tag, so its Fork nodes are STATEFUL and its Join nodes c...
What refreshProcessRepresentations and refreshLST compute FROM a distribution: the (D0,...
The exception types the port throws.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
Dense matrix and non-owning view.
void pool_station(qn::NetworkStruct< T > &sn, std::size_t ind, std::size_t ist)
Rewrite one station; the struct is the solver's private copy.
void refuse_features(const qn::NetworkStruct< T > &sn, std::size_t ind, std::size_t ist)
The features a pooled station cannot be combined with, sub_refuse_features of the reference.
void require_ph(const lang::Distrib< T > &d, const mam::Map< T > &m, const std::string &name, const std::string &cname, const std::string &what)
A renewal phase-type law is what a pool server can carry; anything else is refused by name.
bool ctmc_pools(const qn::NetworkStruct< T > &in, qn::NetworkStruct< T > &out)
Rewrite a copy of in into out when it has pooled stations; false, leaving out untouched,...
bool ctmc_pool_station(const qn::NetworkStruct< T > &sn, std::size_t ist)
Utilization at a pooled station: the mean number of busy servers of the class over the bank.
bool ctmc_has_pools(const qn::NetworkStruct< T > &sn)
True when the struct has a heterogeneous-server station that ctmc_pools rewrites.
mam::Map< T > dist_to_map(const Distrib< T > &d)
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
ProcessType
Distribution kinds, with the values of MATLAB ProcessType.
Definition lang_types.h:485
const char * sched_to_text(SchedStrategy s)
Definition lang_types.h:230
const char * process_to_text(ProcessType p)
The MATLAB ProcessType name, as sn.procid prints it.
Definition lang_types.h:596
void dist_refresh_moments(Distrib< T > &d)
Fill in the first two moments of a distribution given by its matrices.
T map_mean(const Map< T > &m)
Mean inter-arrival time, 1/lambda.
Definition map_moment.h:101
std::vector< T > map_pie(const Map< T > &m)
Phase distribution seen by an arriving job, pie = pi D1 / (pi D1 e).
Definition map_moment.h:89
std::pair< T, std::vector< T > > to_marginal_aggr(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< T > &state_i)
Port of State.toMarginalAggr: the job counts of one node's state row, without the per-phase detail to...
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
Port of the event half of MATLAB's +State package: the successor states an event produces at one node...
Matrix< T > D0
The (D0,D1) pair when the type carries one directly.
Definition lang_types.h:853
static constexpr double FineTol
Definition lang_types.h:760
static constexpr double CoarseTol
Definition lang_types.h:761
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
Matrix< T > D1
Definition map_moment.h:55
Matrix< T > D0
Definition map_moment.h:54
CTMC bookkeeping of a heterogeneous-server station, written by ctmc::ctmc_pools on the solver's priva...
std::vector< std::vector< std::size_t > > len
offset and length of block k of class r
std::vector< std::vector< std::vector< T > > > alpha
entry vector of block k of class r
std::vector< std::vector< std::vector< T > > > exit
exit rates of block k of class r
std::vector< std::vector< bool > > compat
compat[t-1][r-1]
std::vector< double > count
servers per pool
std::vector< std::size_t > alfsorder
pools by ascending class count (ALFS)
std::vector< std::vector< Matrix< T > > > D0
D0 of block k of class r.
std::vector< std::vector< std::size_t > > pools
pools[r-1]: compatible pools of r, ascending
lang::HeteroSchedPolicy policy
bool rotate
ALIS/FAIRNESS order is part of the state.
std::vector< std::vector< std::size_t > > off
std::vector< std::vector< double > > fsfrate
fsfrate[t-1][r-1] = 1/mean (FSF)
std::vector< std::vector< std::size_t > > perms
every pool order, identity first
One station of the network.
std::vector< BalkingParam > balking
std::vector< std::size_t > server_parallelism
Queue.setServerParallelism(class, n): the servers a job seizes for the whole of its service,...
SchedStrategy sched
double nservers
may be infinite (a Delay, or an inf-scheduled task)
CdScaling< T > jdscaling
sn.jdscaling for this station: MATLAB's Station.ljdScaling, the JOINT dependence map eta_i(n),...
std::vector< T > lldscaling
sn.lldscaling for this station: the multiplier at population 1, 2, ... Empty when the station is not ...
std::vector< lang::ImpatienceType > impatience
lang::HeteroSchedPolicy hetero_policy
CdScaling< T > cdscaling
sn.cdscaling for this station: the class-dependence map, empty when unset.
std::vector< ServerType > server_types