LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_mam_dt.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_MAM_SOLVER_MAM_DT_H
6#define LINE_SOLVERS_MAM_SOLVER_MAM_DT_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `solver_mam_dt.m`: discrete-time (slotted) analysis of an open
12 * network whose interarrival and service laws all live on the slot lattice.
13 *
14 * A single queueing station is solved EXACTLY by the Q-MAM discrete-time
15 * queues, `q_dt_ph_ph_1` when both laws are renewal discrete phase-type and
16 * `q_dt_map_map_1` when either side is a D-MAP. Several stations are solved by
17 * a discrete-time parametric decomposition, which is an approximation: the
18 * departure process is truncated at a finite level and compressed back to a
19 * bounded phase dimension, and correlations between the streams entering a
20 * station are not preserved.
21 *
22 * Time is measured in slots internally and converted back on exit, so Q and U
23 * are dimensionless, T is per time unit and R is in time units.
24 *
25 * Convention: late arrival system with delayed access (LAS-DA), matching the
26 * Q-MAM discrete-time queues and the LDES slotted engine. `A_1 = kron(C1,D0)`
27 * is the claim that a job arriving at the end of a slot is not served in it.
28 *
29 * ARITHMETIC: field with transcendentals. The queue solve iterates to a
30 * tolerance, so the analyzer stands down under exact arithmetic exactly as the
31 * MAP/MAP/1 fast path does.
32 */
33
34#include <algorithm>
35#include <cmath>
36#include <cstddef>
37#include <string>
38#include <vector>
39
40#include "line/api/mam/dtime.h"
46#include "line/util/error.h"
47
48namespace line {
49namespace mam {
50
51namespace dt_detail {
52
53/** Rejects the model features the discrete-time path cannot represent. */
54template <class T>
55void assert_scope(const qn::NetworkStruct<T>& sn) {
57 if (!api::sn_is_open_model(sn))
58 throw UnsupportedError(
59 "SolverMAM: the discrete-time path supports open models only. A closed slotted model "
60 "needs a level-dependent discrete chain, which the Q-MAM discrete-time catalogue does "
61 "not cover.");
62 if (sn.nclasses > 1)
63 throw UnsupportedError(
64 "SolverMAM: the discrete-time path supports one class only. Independent per-class "
65 "lattice sources fire in the same slot with positive probability, and a batch of "
66 "simultaneous arrivals of different classes is not an MMAP[K], which is what "
67 "Q_DT_MMAPK_PHK_1 consumes.");
68 for (std::size_t ist = 0; ist < sn.nstations; ++ist) {
69 const SchedStrategy sched = sn.stations[ist].sched;
70 if (sched == SchedStrategy::EXT) continue;
71 if (sched != SchedStrategy::FCFS)
72 throw UnsupportedError(
73 "SolverMAM: the discrete-time path supports FCFS single-server stations and the "
74 "Source only.");
75 if (sn.stations[ist].nservers > 1)
76 throw UnsupportedError(
77 "SolverMAM: the discrete-time path models one server per station; a slotted "
78 "multiserver queue needs the level-dependent boundary of Geo/Geo/c.");
79 }
80}
81
82/** Discrete-time law of a station, expressed in slots. */
83template <class T>
84Dmap<T> station_law(const qn::NetworkStruct<T>& sn, std::size_t ist, std::size_t r,
85 double slot_length) {
87 const ProcessType pt = sn.procid(ist + 1, r + 1);
88 if (pt == ProcessType::DMAP) {
89 if (slot_length != 1.0)
90 throw InputError(
91 "SolverMAM: a DMAP is defined on its own slot, so it cannot be combined with a "
92 "slotlength other than one.");
93 Dmap<T> d;
94 d.D0 = sn.service[ist][r].D0;
95 d.D1 = sn.service[ist][r].D1;
96 return d;
97 }
98 const T mean_slots =
99 num_traits<T>::from_double(1.0 / (num_traits<T>::to_double(sn.rates(ist, r)) * slot_length));
100 return dph_to_dmap(dph_from_dist<T>(pt, mean_slots, sn.scv(ist, r)));
101}
102
103/** Exact single-station analysis through the Q-MAM discrete-time queues. */
104template <class T>
105std::vector<T> solve_single_station(const Dmap<T>& arv, const Dmap<T>& svc,
106 std::size_t max_num_comp) {
107 if (dmap_is_renewal(arv) && dmap_is_renewal(svc))
108 return q_dt_ph_ph_1(dmap_to_dph(arv), dmap_to_dph(svc), max_num_comp);
109 return q_dt_map_map_1(arv, svc, max_num_comp);
110}
111
112/**
113 * Station-to-station routing probabilities, source first and sink stripped.
114 *
115 * The Sink feeds back into the Source to keep the routing matrix stochastic; an
116 * open traffic equation must not see that edge. Routers and class switches
117 * carry no service, so their transit is censored into (I - Pnn)^-1.
118 */
119template <class T>
120Matrix<T> routing(const qn::NetworkStruct<T>& sn, std::size_t source_idx,
121 const std::vector<std::size_t>& queue_idx) {
122 using lang::NodeType;
123 const T zero = num_traits<T>::from_int(0);
124 const std::size_t I = sn.nof_nodes(), K = sn.nclasses;
125 Matrix<T> Pn(I, I, zero);
126 for (std::size_t a = 0; a < I; ++a)
127 for (std::size_t b = 0; b < I; ++b) Pn(a, b) = sn.rtnodes(a * K, b * K);
128
129 std::vector<std::size_t> sink_nodes;
130 for (std::size_t ind = 0; ind < I; ++ind)
131 if (sn.nodes[ind].nodetype == NodeType::Sink) {
132 sink_nodes.push_back(ind);
133 for (std::size_t b = 0; b < I; ++b) Pn(ind, b) = zero;
134 }
135
136 std::vector<std::size_t> station_nodes;
137 station_nodes.push_back(sn.station_to_node[source_idx] - 1);
138 for (std::size_t idx = 0; idx < queue_idx.size(); ++idx)
139 station_nodes.push_back(sn.station_to_node[queue_idx[idx]] - 1);
140
141 std::vector<std::size_t> inter_nodes;
142 for (std::size_t ind = 0; ind < I; ++ind) {
143 const bool is_station =
144 std::find(station_nodes.begin(), station_nodes.end(), ind) != station_nodes.end();
145 const bool is_sink = std::find(sink_nodes.begin(), sink_nodes.end(), ind) != sink_nodes.end();
146 if (!is_station && !is_sink) inter_nodes.push_back(ind);
147 }
148
149 auto sub = [&](const std::vector<std::size_t>& rows, const std::vector<std::size_t>& cols) {
150 Matrix<T> out(rows.size(), cols.size(), zero);
151 for (std::size_t a = 0; a < rows.size(); ++a)
152 for (std::size_t b = 0; b < cols.size(); ++b) out(a, b) = Pn(rows[a], cols[b]);
153 return out;
154 };
155
156 const std::size_t ns = station_nodes.size();
157 Matrix<T> full = sub(station_nodes, station_nodes);
158 if (!inter_nodes.empty()) {
159 Matrix<T> Psn = sub(station_nodes, inter_nodes);
160 Matrix<T> Pnn = sub(inter_nodes, inter_nodes);
161 Matrix<T> Pns = sub(inter_nodes, station_nodes);
162 Matrix<T> Inn = detail::dt_eye<T>(inter_nodes.size());
163 Matrix<T> inv = detail::dt_left_solve(detail::dt_sub(Inn, Pnn), Inn);
164 full = detail::dt_add(full, detail::dt_mul(detail::dt_mul(Psn, inv), Pns));
165 }
166 // column 0 is the Source, which receives nothing
167 Matrix<T> P(ns, ns - 1, zero);
168 for (std::size_t a = 0; a < ns; ++a)
169 for (std::size_t b = 1; b < ns; ++b) P(a, b - 1) = full(a, b);
170 return P;
171}
172
173/** Arrival stream of a queue: source share plus thinned upstream departures. */
174template <class T>
175DBatch<T> arrivals(const DBatch<T>& src, const std::vector<DBatch<T>>& dep, const Matrix<T>& P,
176 std::size_t idx, std::size_t space_max) {
177 DBatch<T> arv;
178 bool have = false;
179 if (num_traits<T>::to_double(P(0, idx)) > 0) {
180 arv = dmap_thin(src, P(0, idx));
181 have = true;
182 }
183 for (std::size_t j = 0; j < dep.size(); ++j) {
184 const T p = P(1 + j, idx);
185 if (num_traits<T>::to_double(p) <= 0) continue;
186 DBatch<T> contrib = dmap_thin(dep[j], p);
187 if (!have) {
188 arv = contrib;
189 have = true;
190 } else {
191 arv = dmap_super(arv, contrib);
192 arv = dmap_compress_batch(arv, space_max);
193 }
194 }
195 if (!have)
196 throw InputError(
197 "SolverMAM: a queue receives no arrivals in the discrete-time routing matrix.");
198 return arv;
199}
200
201/** Total visit ratio of a station across the chains. */
202template <class T>
203T visit_ratio(const qn::NetworkStruct<T>& sn, std::size_t ist) {
204 T v = num_traits<T>::from_int(0);
205 for (std::size_t c = 0; c < sn.visits.size(); ++c) {
206 const Matrix<T>& vm = sn.visits[c];
207 if (ist >= vm.rows()) continue;
208 for (std::size_t r = 0; r < vm.cols(); ++r) v = v + vm(ist, r);
209 }
210 return v;
211}
212
213} // namespace dt_detail
214
215/**
216 * Discrete-time analysis of `sn`, returning the same metric tuple as every
217 * other MAM analyzer. `slot_length` is the slot in model time units.
218 */
219template <class T>
221 double slot_length) {
223 const T zero = num_traits<T>::from_int(0);
224 dt_detail::assert_scope(sn);
225
226 const std::size_t M = sn.nstations, K = sn.nclasses;
227 std::vector<Dmap<T>> law(M);
228 for (std::size_t ist = 0; ist < M; ++ist) law[ist] = dt_detail::station_law(sn, ist, 0, slot_length);
229
230 std::size_t source_idx = M;
231 std::vector<std::size_t> queue_idx;
232 for (std::size_t ist = 0; ist < M; ++ist) {
233 if (sn.stations[ist].sched == SchedStrategy::EXT)
234 source_idx = ist;
235 else
236 queue_idx.push_back(ist);
237 }
238 if (source_idx == M)
239 throw UnsupportedError("SolverMAM: the discrete-time path requires an open model with a Source.");
240
241 const std::size_t nq = queue_idx.size();
242 std::vector<T> QN(nq, zero), UN(nq, zero), TN(nq, zero);
243 std::string method;
244 int totiter = 1;
245
246 DBatch<T> src_batch;
247 src_batch.push_back(law[source_idx].D0);
248 src_batch.push_back(law[source_idx].D1);
249 const T lambda_slot = dmap_lambda_batch(src_batch);
250
251 if (nq == 1) {
252 const std::vector<T> ql =
253 dt_detail::solve_single_station(law[source_idx], law[queue_idx[0]], 1000);
254 for (std::size_t i = 0; i < ql.size(); ++i)
255 QN[0] = QN[0] + num_traits<T>::from_int(static_cast<int>(i)) * ql[i];
256 UN[0] = num_traits<T>::from_int(1) - ql[0];
257 TN[0] = lambda_slot;
258 method = "dt.qmam";
259 } else {
260 // Initial departure streams: Bernoulli of the exact station throughput,
261 // the slotted counterpart of seeding a decomposition with Poisson streams
262 Matrix<T> P = dt_detail::routing(sn, source_idx, queue_idx);
263 std::vector<DBatch<T>> dep(nq);
264 for (std::size_t idx = 0; idx < nq; ++idx) {
265 const T p = lambda_slot * dt_detail::visit_ratio(sn, queue_idx[idx]);
266 DBatch<T> b;
267 b.push_back(Matrix<T>(1, 1, num_traits<T>::from_int(1) - p));
268 b.push_back(Matrix<T>(1, 1, p));
269 dep[idx] = b;
270 }
271
272 std::vector<T> QNprev(nq, zero), QNprev2(nq, zero), UNprev(nq, zero), TNprev(nq, zero);
273 const int iter_max = opt.iter_max > 0 ? opt.iter_max : 100;
274 const double iter_tol = opt.tol > 0 ? opt.tol : 1e-3;
275 auto max_rel = [&](const std::vector<T>& a, const std::vector<T>& b) {
276 double worst = 0;
277 for (std::size_t i = 0; i < a.size(); ++i) {
278 const double d = std::max(std::abs(num_traits<T>::to_double(b[i])), 1e-14);
279 worst = std::max(worst, std::abs(num_traits<T>::to_double(a[i] - b[i])) / d);
280 }
281 return worst;
282 };
283
284 for (int it = 1; it <= iter_max; ++it) {
285 totiter = it;
286 for (std::size_t idx = 0; idx < nq; ++idx) {
287 DBatch<T> arv = dt_detail::arrivals(src_batch, dep, P, idx, opt.space_max);
288 DtQueueResult<T> r = mg1_dt_queue(arv, law[queue_idx[idx]], 1000, true);
289 QN[idx] = r.QN;
290 UN[idx] = r.UN;
291 TN[idx] = r.TN;
292 DBatch<T> d;
293 d.push_back(r.dep.D0);
294 d.push_back(r.dep.D1);
295 dep[idx] = dmap_compress_batch(d, opt.space_max);
296 }
297 if (it > 1 && max_rel(QN, QNprev) < iter_tol) break;
298 if (it > 2 && max_rel(QN, QNprev2) < iter_tol) {
299 // Feedback loops settle into a period-two cycle rather than a
300 // point: re-solving a station with the departure process it just
301 // produced moves it back. The cycle amplitude sits far below the
302 // error of the decomposition itself, so the midpoint is reported
303 // instead of burning iter_max sweeps on an orbit that will not close.
304 const T half = num_traits<T>::from_double(0.5);
305 for (std::size_t idx = 0; idx < nq; ++idx) {
306 QN[idx] = (QN[idx] + QNprev[idx]) * half;
307 UN[idx] = (UN[idx] + UNprev[idx]) * half;
308 TN[idx] = (TN[idx] + TNprev[idx]) * half;
309 }
310 break;
311 }
312 QNprev2 = QNprev;
313 QNprev = QN;
314 UNprev = UN;
315 TNprev = TN;
316 }
317 method = "dt.dec";
318 }
319
320 MamSolution<T> out;
321 mva::MvaSolution<T>& s = out.sol;
322 s.Q = Matrix<T>(M, K, zero);
323 s.U = Matrix<T>(M, K, zero);
324 s.R = Matrix<T>(M, K, zero);
325 s.Tp = Matrix<T>(M, K, zero);
326 s.C.assign(K, zero);
327 s.X.assign(K, zero);
328
329 const T slot = num_traits<T>::from_double(slot_length);
330 for (std::size_t idx = 0; idx < nq; ++idx) {
331 const std::size_t ist = queue_idx[idx];
332 s.Q(ist, 0) = QN[idx];
333 s.U(ist, 0) = UN[idx];
334 s.Tp(ist, 0) = TN[idx] / slot;
335 if (num_traits<T>::to_double(TN[idx]) > 0) s.R(ist, 0) = QN[idx] / TN[idx] * slot;
336 }
337 const T lambda = lambda_slot / slot;
338 s.Tp(source_idx, 0) = lambda;
339 s.X[0] = lambda;
340 T qtot = zero;
341 for (std::size_t idx = 0; idx < nq; ++idx) qtot = qtot + QN[idx];
342 s.C[0] = qtot / lambda;
343 s.iter = totiter;
344 out.actualmethod = method;
345 return out;
346}
347
348} // namespace mam
349} // namespace line
350
351#endif // LINE_SOLVERS_MAM_SOLVER_MAM_DT_H
InputError(const std::string &what)
Definition error.h:39
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
Discrete-time (slotted) matrix-analytic primitives and queues.
The exception types the port throws.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
The option and result types SolverMAM shares with its analyzers.
bool sn_is_open_model(const qn::NetworkStruct< T > &sn)
all(isinf(sn.njobs)): EVERY class is open, which a mixed model fails.
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:483
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:324
DBatch< T > dmap_compress_batch(const DBatch< T > &A, std::size_t max_order)
Order reduction of a BATCH stream.
Definition dtime.h:480
std::vector< T > q_dt_map_map_1(const Dmap< T > &arv, const Dmap< T > &svc, std::size_t max_num_comp=1000)
Queue length distribution of a discrete-time D-MAP/D-MAP/1/FCFS queue, the queue-length half of Q_DT_...
Definition dtime.h:724
std::vector< Matrix< T > > DBatch
A discrete batch arrival stream, entry k carrying the slots with k events.
Definition dtime.h:64
T dmap_lambda_batch(const DBatch< T > &A)
Mean number of EVENTS per slot, pi sum_k k A_k e.
Definition dtime.h:277
std::vector< T > q_dt_ph_ph_1(const Dph< T > &arv, const Dph< T > &svc, std::size_t max_num_comp=1000)
Queue length of a discrete-time DPH/DPH/1/FCFS queue, via the D-MAP route.
Definition dtime.h:805
DtQueueResult< T > mg1_dt_queue(const DBatch< T > &arv, const Dmap< T > &svc, std::size_t max_num_comp=1000, bool want_departure=false)
Discrete-time single-server queue with batch D-MAP arrivals, DBMAP/D-MAP/1.
Definition dtime.h:835
Dph< T > dph_from_dist(lang::ProcessType type, const T &mean_slots, const T &scv)
Exact discrete phase-type representation of a lattice-valued law.
Definition dtime.h:157
Dmap< T > dph_to_dmap(const Dph< T > &d)
Renewal D-MAP (A, a alpha) of a discrete phase-type law.
Definition dtime.h:216
MamSolution< T > solver_mam_dt(const qn::NetworkStruct< T > &sn, const MamOptions &opt, double slot_length)
Discrete-time analysis of sn, returning the same metric tuple as every other MAM analyzer.
Dph< T > dmap_to_dph(const Dmap< T > &d)
Discrete phase-type law underlying a renewal D-MAP.
Definition dtime.h:254
DBatch< T > dmap_super(const DBatch< T > &A, const DBatch< T > &B)
Superposition, E_k = sum_{i+j=k} kron(A_i, B_j).
Definition dtime.h:300
bool dmap_is_renewal(const Dmap< T > &d)
True when D1 has rank one, i.e.
Definition dtime.h:233
DBatch< T > dmap_thin(const DBatch< T > &A, const T &p)
Bernoulli thinning, B_k = sum_{n>=k} C(n,k) p^k (1-p)^(n-k) A_n.
Definition dtime.h:320
A queueing network and its refreshed NetworkStruct.
Port of matlab/src/api/sn/sn_is_discrete_time.m.
Ports of the sn_has_* / sn_is_* predicate family of matlab/src/api/sn.
Outcome of a slotted station solve.
Definition dtime.h:812
The options SolverMAM reads.
Definition mam_types.h:29
What the MAM dispatch returns: the metrics plus the algorithm that ran.
Definition mam_types.h:124
std::string actualmethod
The concrete algorithm, as the reference's actualmethod.
Definition mam_types.h:127
mva::MvaSolution< T > sol
Definition mam_types.h:125
Class-level results, the [Q,U,R,T,C,X] of the MATLAB analyzers.
Definition mva_types.h:96
std::vector< T > X
Definition mva_types.h:98
std::vector< T > C
Definition mva_types.h:98