LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
sn_to_qrf_blocking.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_SN_SN_TO_QRF_BLOCKING_H
6#define LINE_API_SN_SN_TO_QRF_BLOCKING_H
7
8/**
9 * @file
10 * @ingroup api_sn
11 * The QRF BAS blocking tables (f, MR, BB, MM, ZZ, MM1), derived from an sn.
12 *
13 * Port of matlab/src/api/sn/sn_to_qrf_capacity.m and
14 * matlab/src/api/sn/sn_to_qrf_blocking.m.
15 *
16 * `qrf_bas` describes a Blocking-After-Service network by a finite-capacity
17 * queue f and an enumeration of the BLOCKING CONFIGURATIONS reachable behind
18 * it. Everything in that enumeration is implied by the model, so it is derived
19 * here rather than demanded from the caller; `options.config.qrf_params`
20 * remains an explicit override.
21 *
22 * The tables, and the constraint that reads each one in `qrf_bas`:
23 *
24 * f the ONE finite-capacity queue. The formulation carries a scalar f
25 * (ZERO4/ZERO7/ZERO8, THM30, THM3I, THM3L all index it), so a model
26 * with two binding buffers is refused here.
27 * F(i) min(buffer size, N) for every queue; N where the buffer is
28 * unbounded, since no queue can hold more than the population.
29 * BB(m,i) 1 iff queue i is blocked in configuration m.
30 * ZZ(m) the blocking depth of configuration m.
31 * MM(m,0) head of the FIFO blocking order: the queue that takes the slot
32 * when f completes. `qrf_bas` reads ONLY the first column.
33 * MM1(m,j) index of the configuration reached from m when j becomes blocked.
34 * Read by THM3L alone, at depth ZM-1.
35 *
36 * THREE INVARIANTS, each a correctness condition rather than a convention:
37 *
38 * 1. Configuration 1 MUST be the empty one. ZERO4 iterates `m = 2:MR` and
39 * ZERO5/ZERO7/ZERO8 test `m >= 2` to mean "some queue is blocked".
40 * 2. ZM = max(ZZ) MUST be the reachable maximum. `qrf_bas` recomputes ZM from
41 * ZZ and closes the depth ladder there, so a truncated enumeration excises
42 * states the real chain visits and the polytope stops containing the true
43 * distribution -- the bound stops bounding. The size guard therefore
44 * REFUSES; it never truncates.
45 * 3. Blocking APPENDS at the tail: a queue that becomes blocked joins behind
46 * those already waiting, so MM1's successor is the configuration with j
47 * appended, and the head MM(m,0) names never moves.
48 *
49 * THE ENUMERATION IS THE FULL ORDERED ONE, and it has to be. A (set, head)
50 * collapse looks sound -- the LP reads configurations only through BB, ZZ,
51 * MM(:,0) and MM1, and both objective and readout sum over m -- and it would
52 * shrink MR from sum_z P(B,z) to 1 + sum_z C(B,z)*z. It was tried and it is
53 * WRONG. Merging the depth-ZM configurations that share a set and a head makes
54 * several THM3L rows, one per depth-(ZM-1) predecessor, reference the SAME
55 * merged successor block. That is extra coupling the fine system does not have,
56 * so the collapsed polytope is strictly SMALLER, not a projection of the fine
57 * one, and it can cut off the true distribution. Measured on a 4-station model
58 * with three feeders (B=3, ZM=3, MR 13 collapsed vs 16 full), the collapse
59 * reported upper bounds of 0.681/0.979/0.768 where the full enumeration gives
60 * 0.709/0.982/0.800: tighter, from a coarser state space, which is the
61 * signature of a cut that is not valid.
62 *
63 * So MR is factorial in the number of feeders B, and the size guard is what
64 * keeps that honest: it REFUSES an oversized instance rather than trimming the
65 * enumeration, because trimming is the same unsound cut by another name.
66 *
67 * WHO CAN BE BLOCKED is read from `sn.isbasblocking`, not from `sn.droprule`:
68 * LINE accepts the BAS declaration on the upstream station or on the full
69 * destination, and reading droprule at the capped station sees only the second
70 * (BUG-83).
71 */
72
73#include <algorithm>
74#include <cmath>
75#include <cstddef>
76#include <map>
77#include <sstream>
78#include <string>
79#include <vector>
80
83
84namespace line {
85namespace sn {
86
87/**
88 * Variable-count ceiling of the derived LP. A guard, not a tuning knob: the
89 * enumeration cannot be truncated (invariant 2), so an oversized model is
90 * refused rather than approximated.
91 */
92const double kQrfDefaultMaxVars = 5e5;
93
94/** Per-station occupancy bounds, and which of them bind. */
96 std::vector<int> F; ///< (M) occupancy bound of each station, in jobs
97 std::vector<bool> binding; ///< (M) true where the buffer can refuse a job
98 std::string msg; ///< empty on success
99};
100
101/**
102 * F is an OCCUPANCY BOUND, not a declared capacity: the station's buffer where
103 * that buffer BINDS, and the population N everywhere else, since no queue of a
104 * closed model can hold more than N jobs. Binding is decided by
105 * `sn_get_buffer_size`, the single place in LINE that makes that call.
106 *
107 * Both QRF blocking bounds need this. `qrf.bas` needs it beside the blocking
108 * tables; `qrf.rsrd` needs it ALONE, since its PBB constraint reads only which
109 * queues can be full and it carries no blocking tables at all.
110 */
111template <class T>
113 QrfCapacity out;
114 const std::size_t M = sn.nstations;
115 out.F.assign(M, 0);
116 out.binding.assign(M, false);
117
118 double Nd = 0.0;
119 const std::vector<double> njobs = sn.njobs();
120 for (std::size_t r = 0; r < njobs.size(); ++r) Nd += njobs[r];
121 if (!(Nd >= 1.0) || std::isinf(Nd)) {
122 out.msg = "the QRF bounds need a closed model with a finite population.";
123 return out;
124 }
125 const int N = static_cast<int>(Nd + 0.5);
126
127 for (std::size_t i = 0; i < M; ++i) {
128 const double b = sn_get_buffer_size(sn, i + 1); // 1-based
129 out.binding[i] = std::isfinite(b);
130 out.F[i] = (!std::isfinite(b) || b > static_cast<double>(N)) ? N
131 : static_cast<int>(b + 0.5);
132 if (out.F[i] < 1) {
133 std::ostringstream os;
134 os << "station " << (i + 1) << " has capacity " << out.F[i]
135 << ": the QRF bounds need every queue to be able to hold at least one job.";
136 out.msg = os.str();
137 return out;
138 }
139 }
140 return out;
141}
142
143/** The derived blocking tables, in the reference's 1-based queue indexing. */
145 int f = 1; ///< finite-capacity queue, 1-based
146 std::vector<int> F; ///< (M) occupancy bounds
147 int MR = 1; ///< number of blocking configurations
148 std::vector<std::vector<int> > BB; ///< (MR x M) blocking state
149 std::vector<std::vector<int> > MM; ///< (MR x .) blocking order, 1-based, 0 = absent
150 std::vector<std::vector<int> > MM1; ///< (MR x M) successor map, 1-based, 0 = absent
151 std::vector<int> ZZ; ///< (MR) blocked count per configuration
152 int ZM = 0; ///< maximum reachable blocking depth
153 std::vector<int> blockers; ///< 1-based stations that can be blocked behind f
154 std::string msg; ///< empty on success
155};
156
157namespace detail {
158
159/** The one-configuration table for a model in which no blocking is reachable. */
160inline QrfBlocking qrf_empty_blocking(const std::vector<int>& F, int f_one_based,
161 std::size_t M) {
162 QrfBlocking b;
163 b.f = f_one_based;
164 b.F = F;
165 b.MR = 1;
166 b.BB.assign(1, std::vector<int>(M, 0));
167 b.MM.assign(1, std::vector<int>(2, 0));
168 b.MM1.assign(1, std::vector<int>(M, 0));
169 b.ZZ.assign(1, 0);
170 b.ZM = 0;
171 return b;
172}
173
174/**
175 * 0-based station indices that hold a completed job when f is full. A blocker
176 * must route into f, must not be f, and must not be an infinite server (which
177 * has a server per job and cannot be held). BAS itself is read from the BUG-83
178 * marker, with a structural fallback for an sn built without it.
179 */
180template <class T>
181std::vector<std::size_t> qrf_blockers(const qn::NetworkStruct<T>& sn, std::size_t f,
182 std::size_t M) {
183 std::vector<bool> declared(M, false);
184 bool any = false;
185 for (std::size_t i = 0; i < M; ++i) {
186 const std::size_t ind = (i + 1 <= sn.station_to_node.size()) ? sn.station_to_node[i] : 0;
187 if (ind >= 1 && ind <= sn.isbasblocking.size() && sn.isbasblocking[ind - 1]) {
188 declared[i] = true;
189 any = true;
190 }
191 }
192 if (!any && !sn.droprule.empty()) {
193 // Fallback: BAS declared on the upstream station or on the full
194 // destination, the same two forms refresh_bas_blocking resolves.
195 bool dest_bas = false;
196 if (sn.droprule.size() > f)
197 for (std::size_t r = 0; r < sn.droprule[f].size(); ++r)
198 if (sn.droprule[f][r] == qn::DropStrategy::BAS) dest_bas = true;
199 for (std::size_t i = 0; i < M; ++i) {
200 if (i == f || i >= sn.droprule.size()) continue;
201 bool here_bas = false;
202 for (std::size_t r = 0; r < sn.droprule[i].size(); ++r)
203 if (sn.droprule[i][r] == qn::DropStrategy::BAS) here_bas = true;
204 if (dest_bas || here_bas) declared[i] = true;
205 }
206 }
207
208 const std::size_t R = sn.nclasses;
209 std::vector<std::size_t> out;
210 for (std::size_t i = 0; i < M; ++i) {
211 if (i == f || !declared[i]) continue;
212 if (sn.stations[i].sched == qn::SchedStrategy::INF) continue;
213 // Summed over class pairs so the test survives a multiclass sn, even
214 // though the QRF gate upstream admits one class only.
215 bool routes = false;
216 for (std::size_t r = 0; r < R && !routes; ++r)
217 for (std::size_t s = 0; s < R; ++s)
218 if (sn.rt(i * R + r, f * R + s) > 0) {
219 routes = true;
220 break;
221 }
222 if (routes) out.push_back(i);
223 }
224 std::sort(out.begin(), out.end());
225 return out;
226}
227
228/**
229 * Every ordered sequence of distinct blockers up to length ZM, first entry the
230 * head. Deterministic order -- depth ascending, then subsets lexicographic by
231 * ascending station index, then the orders of each subset sorted -- so every
232 * codebase emits identical tables. The empty configuration is first
233 * (invariant 1).
234 */
235inline std::vector<std::vector<std::size_t> > qrf_enumerate_permutations(
236 const std::vector<std::size_t>& blockers, int ZM) {
237 std::vector<std::vector<std::size_t> > cfg;
238 cfg.push_back(std::vector<std::size_t>());
239 const std::size_t nb = blockers.size();
240 for (int z = 1; z <= ZM; ++z) {
241 std::vector<bool> pick(nb, false);
242 for (std::size_t i = 0; i < static_cast<std::size_t>(z) && i < nb; ++i) pick[i] = true;
243 // iterate subsets in lexicographic order of ascending index
244 std::vector<std::vector<std::size_t> > subsets;
245 std::vector<std::size_t> idx(z, 0);
246 for (int d = 0; d < z; ++d) idx[d] = d;
247 while (true) {
248 std::vector<std::size_t> members;
249 for (int d = 0; d < z; ++d) members.push_back(blockers[idx[d]]);
250 subsets.push_back(members);
251 int d = z - 1;
252 while (d >= 0 && idx[d] == nb - z + d) --d;
253 if (d < 0) break;
254 ++idx[d];
255 for (int e = d + 1; e < z; ++e) idx[e] = idx[e - 1] + 1;
256 }
257 for (std::size_t s = 0; s < subsets.size(); ++s) {
258 std::vector<std::size_t> order = subsets[s]; // already ascending
259 do {
260 cfg.push_back(order);
261 } while (std::next_permutation(order.begin(), order.end()));
262 }
263 }
264 return cfg;
265}
266
267/**
268 * Identity of a configuration: the whole blocking order, since that is what
269 * distinguishes configurations in the enumeration `qrf_bas` is entitled to.
270 */
271inline std::string qrf_cfg_key(const std::vector<std::size_t>& seq) {
272 std::ostringstream os;
273 for (std::size_t i = 0; i < seq.size(); ++i) os << seq[i] << ',';
274 return os.str();
275}
276
277/** Total service phases across stations, which sizes the QRF variable space. */
278template <class T>
279int qrf_total_phases(const std::vector<std::pair<Matrix<T>, Matrix<T> > >& MAPs) {
280 int total = 0;
281 for (std::size_t i = 0; i < MAPs.size(); ++i)
282 total += std::max<int>(1, static_cast<int>(MAPs[i].first.rows()));
283 return total;
284}
285
286} // namespace detail
287
288/**
289 * @brief The QRF BAS blocking tables (f, MR, BB, MM, ZZ, MM1), derived from
290 * an sn.
291 *
292 * @param sn network structure
293 * @param Ktot total service phases, which sizes the LP together with MR and N
294 * @param max_vars variable-count ceiling; pass kQrfDefaultMaxVars for the default
295 * @return the derived tables, or a QrfBlocking carrying a non-empty msg
296 */
297template <class T>
299 double max_vars = kQrfDefaultMaxVars) {
300 QrfBlocking out;
301 const std::size_t M = sn.nstations;
302
303 double Nd = 0.0;
304 const std::vector<double> njobs = sn.njobs();
305 for (std::size_t r = 0; r < njobs.size(); ++r) Nd += njobs[r];
306 const int N = static_cast<int>(Nd + 0.5);
307
308 const QrfCapacity cap = sn_to_qrf_capacity(sn);
309 if (!cap.msg.empty()) {
310 out.msg = cap.msg;
311 return out;
312 }
313
314 std::vector<std::size_t> fcand;
315 for (std::size_t i = 0; i < M; ++i)
316 if (cap.binding[i]) fcand.push_back(i);
317
318 if (fcand.empty()) {
319 // No binding buffer: callers gate on sn_has_blocking first, so this is
320 // a defensive branch rather than a normal path.
321 return detail::qrf_empty_blocking(cap.F, 1, M);
322 }
323 if (fcand.size() > 1) {
324 std::ostringstream os;
325 os << "'qrf.bas' models a single finite-capacity queue (its f is a scalar), but "
326 << fcand.size() << " stations have a binding buffer: ";
327 for (std::size_t c = 0; c < fcand.size(); ++c) {
328 if (c) os << ", ";
329 os << sn.stations[fcand[c]].name;
330 }
331 os << ". Use 'qrf.rsrd', whose PBB constraint sums over every full queue and therefore "
332 "admits several, or cap only one station.";
333 out.msg = os.str();
334 return out;
335 }
336 const std::size_t f = fcand[0];
337
338 const std::vector<std::size_t> blockers = detail::qrf_blockers(sn, f, M);
339
340 // Blocking needs f at capacity plus one held job per blocked queue, so the
341 // population caps the depth as tightly as the feeder count does.
342 int ZM = std::min<int>(static_cast<int>(blockers.size()), N - cap.F[f]);
343 if (ZM < 0) ZM = 0;
344 if (ZM == 0) {
345 QrfBlocking b = detail::qrf_empty_blocking(cap.F, static_cast<int>(f) + 1, M);
346 for (std::size_t i = 0; i < blockers.size(); ++i)
347 b.blockers.push_back(static_cast<int>(blockers[i]) + 1);
348 return b;
349 }
350
351 const std::vector<std::vector<std::size_t> > cfg =
352 detail::qrf_enumerate_permutations(blockers, ZM);
353 const int MR = static_cast<int>(cfg.size());
354
355 // Size guard: refuse, never truncate (invariant 2).
356 const double n_vars =
357 static_cast<double>(MR) * (N + 1) * (N + 1) * Ktot * Ktot + Ktot;
358 if (n_vars > max_vars) {
359 std::ostringstream os;
360 os << "the QRF BAS linear program for this model would carry " << n_vars
361 << " variables (MR=" << MR << " blocking configurations, N=" << N << ", " << Ktot
362 << " service phases in total), above the qrf_maxvars limit of " << max_vars
363 << ". The enumeration cannot be truncated -- a depth below the reachable maximum ZM="
364 << ZM << " excises states the chain visits, and the result would no longer bound. "
365 << "Reduce the population, the number of stations feeding " << sn.stations[f].name
366 << ", or the phase counts; or raise the limit deliberately.";
367 out.msg = os.str();
368 return out;
369 }
370
371 out.f = static_cast<int>(f) + 1;
372 out.F = cap.F;
373 out.MR = MR;
374 out.ZM = ZM;
375 out.BB.assign(MR, std::vector<int>(M, 0));
376 out.MM.assign(MR, std::vector<int>(std::max<std::size_t>(2, blockers.size()), 0));
377 out.MM1.assign(MR, std::vector<int>(M, 0));
378 out.ZZ.assign(MR, 0);
379 for (std::size_t i = 0; i < blockers.size(); ++i)
380 out.blockers.push_back(static_cast<int>(blockers[i]) + 1);
381
382 std::map<std::string, int> index;
383 for (int m = 0; m < MR; ++m) {
384 const std::vector<std::size_t>& seq = cfg[m];
385 out.ZZ[m] = static_cast<int>(seq.size());
386 for (std::size_t z = 0; z < seq.size(); ++z) {
387 out.BB[m][seq[z]] = 1;
388 // only the first column is read; the rest records the full order
389 out.MM[m][z] = static_cast<int>(seq[z]) + 1;
390 }
391 index[detail::qrf_cfg_key(seq)] = m;
392 }
393 for (int m = 0; m < MR; ++m) {
394 if (out.ZZ[m] >= ZM) continue; // THM3L reads MM1 only below ZM
395 for (std::size_t b = 0; b < blockers.size(); ++b) {
396 const std::size_t j = blockers[b];
397 if (out.BB[m][j]) continue;
398 std::vector<std::size_t> succ = cfg[m];
399 succ.push_back(j);
400 const std::map<std::string, int>::const_iterator it =
401 index.find(detail::qrf_cfg_key(succ));
402 if (it != index.end()) out.MM1[m][j] = it->second + 1;
403 }
404 }
405 return out;
406}
407
408} // namespace sn
409} // namespace line
410
411#endif // LINE_API_SN_SN_TO_QRF_BLOCKING_H
A network plus its refreshed NetworkStruct.
Matrix< T > rt
sn.rt and sn.rtnodes: the class-expanded routing.
std::vector< Station< T > > stations
stations[k-1] is the k-th station
std::vector< std::vector< DropStrategy > > droprule
QrfBlocking sn_to_qrf_blocking(const qn::NetworkStruct< T > &sn, int Ktot, double max_vars=kQrfDefaultMaxVars)
The QRF BAS blocking tables (f, MR, BB, MM, ZZ, MM1), derived from an sn.
const double kQrfDefaultMaxVars
Variable-count ceiling of the derived LP.
double sn_get_buffer_size(const qn::NetworkStruct< T > &sn, std::size_t ist)
Physical buffer size of a station, in jobs, the one in service included.
QrfCapacity sn_to_qrf_capacity(const qn::NetworkStruct< T > &sn)
F is an OCCUPANCY BOUND, not a declared capacity: the station's buffer where that buffer BINDS,...
A queueing network and its refreshed NetworkStruct.
Physical buffer size of a station, in jobs, the one in service included.
The derived blocking tables, in the reference's 1-based queue indexing.
int ZM
maximum reachable blocking depth
std::vector< int > ZZ
(MR) blocked count per configuration
int f
finite-capacity queue, 1-based
std::vector< std::vector< int > > BB
(MR x M) blocking state
std::vector< std::vector< int > > MM1
(MR x M) successor map, 1-based, 0 = absent
int MR
number of blocking configurations
std::vector< int > blockers
1-based stations that can be blocked behind f
std::vector< std::vector< int > > MM
(MR x .) blocking order, 1-based, 0 = absent
std::vector< int > F
(M) occupancy bounds
std::string msg
empty on success
Per-station occupancy bounds, and which of them bind.
std::vector< int > F
(M) occupancy bound of each station, in jobs
std::string msg
empty on success
std::vector< bool > binding
(M) true where the buffer can refuse a job