LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_nc_conv.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_NC_SOLVER_NC_CONV_H
6#define LINE_SOLVERS_NC_SOLVER_NC_CONV_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Exact convolution analysis of a closed network with class-dependent service
12 * rates. Port of `solver_nc_conv.m`.
13 *
14 * A class-dependent station scales its nominal demand by beta_{i,r}(n), a
15 * function of the per-class population AT THAT STATION. None of the
16 * normalizing-constant algorithms in `solver_ncld` can apply it -- they read a
17 * rate lattice mu(n) and nothing else -- so a cdscaling model is routed here
18 * UNCONDITIONALLY on the method, not only on 'exact'. The multichain convolution
19 * of Sauer (1983), Sect. 5.2, eq. (40) is the exact algorithm for beta, and it
20 * is what `pfqn_conv` implements.
21 *
22 * QUEUE LENGTHS COME FROM THE MARGINAL, NOT FROM MVA. The station factor
23 * X_m(n) is built by the chain-dependent recurrence, and the marginal
24 * P_m(n | N) = X_m(n) G_{-m}(N-n) / G(N) is summed against n_k. G_{-m} is a
25 * fresh convolution over every station but m, so the cost is one convolution per
26 * lattice point per class-dependent station: this analyzer is exact and slow,
27 * which is the trade the reference makes.
28 *
29 * UTILIZATION IS NORMALIZED BY THE DECLARED PEAK. T*ST measures capacity used in
30 * units of the NOMINAL rate and reaches max_n beta, not 1, at saturation, so a
31 * beta emulating two servers would report U = 2 * (true utilization). Dividing
32 * by `sn.cdscalingpeak` restores the T*S/c convention of an ordinary multiserver
33 * station. No cap is needed: sum_r U(i,r) is a convex combination of the
34 * beta_r(n) over max_n beta, hence at most one by construction.
35 *
36 * Arithmetic: the convolution itself is field arithmetic, but lG is a log and
37 * the analyzer is guarded on `has_transcendental` for it.
38 */
39
40#include <cmath>
41#include <vector>
42
47#include "line/util/error.h"
48
49namespace line {
50namespace nc {
51
52namespace detail {
53
54/** Advance a population vector over 0 <= n <= N, as `pprod_next`; false at end. */
55inline bool conv_pprod_next(std::vector<int>& n, const std::vector<int>& N) {
56 std::size_t s = n.size();
57 while (s > 0 && n[s - 1] == N[s - 1]) {
58 n[s - 1] = 0;
59 --s;
60 }
61 if (s == 0) return false;
62 n[s - 1] += 1;
63 return true;
64}
65
66/** Column-major linear index of a population vector, as `hashpop`. */
67inline std::size_t conv_hashpop(const std::vector<int>& n, const std::vector<int>& N) {
68 std::size_t idx = 0, stride = 1;
69 for (std::size_t r = 0; r < N.size(); ++r) {
70 idx += stride * static_cast<std::size_t>(n[r]);
71 stride *= static_cast<std::size_t>(N[r]) + 1;
72 }
73 return idx;
74}
75
76} // namespace detail
77
78/**
79 * Port of `solver_nc_conv.m`.
80 *
81 * @param sn the refreshed struct; at least one station carries a cdscaling
82 * @param opt solver controls; unused, the convolution is exact and has no tuning
83 */
84template <class T>
86 (void)opt;
87 NcSolution<T> out;
88 if constexpr (!num_traits<T>::has_transcendental) {
89 (void)sn;
90 throw UnsupportedError(
91 "solver_nc_conv: the convolution analyzer reports lG = log(G); this backend has no "
92 "transcendental arithmetic");
93 } else {
94 const T zero = num_traits<T>::from_int(0);
95 const std::size_t M = sn.nstations;
96
97 // The convolution runs on CHAINS, not classes: a class-switching model
98 // splits one circulating population across several classes, so the
99 // per-class populations are zero in the classes that hold no reference
100 // jobs and the class-level recursion charges those stations nothing at
101 // all. Every other closed NC and MVA path aggregates the same way and
102 // deaggregates at the end.
104 const std::size_t K = sn.nchains;
105
106 std::vector<int> NK(K, 0);
107 for (std::size_t c = 0; c < K; ++c) {
108 if (std::isinf(d.Nchain[c]))
109 throw UnsupportedError(
110 "solver_nc_conv: the multichain convolution requires a closed queueing "
111 "network; this model has an open class");
112 NK[c] = static_cast<int>(std::llround(d.Nchain[c]));
113 }
114
115 const Matrix<T>& V = d.Vchain;
116 const Matrix<T>& ST = d.STchain;
117 const Matrix<T>& Ldemand = d.Lchain;
118
119 std::vector<std::size_t> delayIdx, queueIdx;
120 for (std::size_t i = 0; i < M; ++i)
121 (std::isinf(sn.stations[i].nservers) ? delayIdx : queueIdx).push_back(i + 1);
122 const std::size_t nQueues = queueIdx.size();
123
124 Matrix<T> Z_conv(1, K, zero);
125 for (std::size_t i : delayIdx)
126 for (std::size_t r = 0; r < K; ++r) Z_conv(0, r) = T(Z_conv(0, r) + Ldemand(i - 1, r));
127
128 Matrix<T> L_conv(nQueues, K, zero);
129 for (std::size_t q = 0; q < nQueues; ++q)
130 for (std::size_t r = 0; r < K; ++r) L_conv(q, r) = Ldemand(queueIdx[q] - 1, r);
131
132 std::vector<lang::CdScaling<T>> cd_conv(nQueues);
133 for (std::size_t q = 0; q < nQueues; ++q)
134 cd_conv[q] = sn.stations[queueIdx[q] - 1].cdscaling;
135
136 const T G_N = pfqn::pfqn_conv(L_conv, NK, Z_conv, cd_conv).G;
137 out.sol.lG = num_traits<T>::log_as_double(G_N);
138
139 std::vector<T> XN(K, zero);
140 for (std::size_t r = 0; r < K; ++r)
141 if (NK[r] > 0) {
142 std::vector<int> Nm = NK;
143 --Nm[r];
144 const T G_Nk = pfqn::pfqn_conv(L_conv, Nm, Z_conv, cd_conv).G;
145 XN[r] = T(G_Nk / G_N);
146 }
147
148 Matrix<T> TN(M, K, zero), QN(M, K, zero), RN(M, K, zero), UN(M, K, zero);
149 for (std::size_t i = 0; i < M; ++i)
150 for (std::size_t r = 0; r < K; ++r) TN(i, r) = T(V(i, r) * XN[r]);
151
152 for (std::size_t i : delayIdx)
153 for (std::size_t r = 0; r < K; ++r)
154 QN(i - 1, r) = T(Ldemand(i - 1, r) * XN[r]);
155
156 std::size_t stateSpaceSize = 1;
157 for (int v : NK) stateSpaceSize *= static_cast<std::size_t>(v) + 1;
158
159 for (std::size_t q = 0; q < nQueues; ++q) {
160 const std::size_t ist = queueIdx[q];
161 const bool isCd = static_cast<bool>(cd_conv[q]);
162
163 // the station factor X_m(n), by the chain-dependent recurrence
164 std::vector<T> Xm(stateSpaceSize, zero);
165 Xm[0] = num_traits<T>::from_int(1);
166 std::vector<int> n(K, 0);
167 do {
168 int tot = 0;
169 for (int v : n) tot += v;
170 if (tot == 0) continue;
171 const std::size_t idx = detail::conv_hashpop(n, NK);
172 if (isCd) {
173 // X_m(n) = (|n|/n_r) (L/beta_r(n)) X_m(n - e_r) for ANY r with
174 // n_r > 0; the result is path independent, so the first is used
175 for (std::size_t r = 0; r < K; ++r) {
176 if (n[r] == 0) continue;
177 std::vector<T> row(K, zero);
178 for (std::size_t j = 0; j < K; ++j)
179 row[j] = num_traits<T>::from_int(n[j]);
180 const std::vector<T> bval = cd_conv[q](row);
181 if (bval.empty())
182 throw UnsupportedError(
183 "solver_nc_conv: a class-dependence map returned no value");
184 const T beta = bval.size() > 1 ? bval[r] : bval[0];
185 const int nr = n[r];
186 n[r] -= 1;
187 const std::size_t idx_prev = detail::conv_hashpop(n, NK);
188 n[r] += 1;
189 if (beta > zero)
190 Xm[idx] = T(num_traits<T>::from_int(tot) /
191 num_traits<T>::from_int(nr) * (L_conv(q, r) / beta) *
192 Xm[idx_prev]);
193 break;
194 }
195 } else {
196 for (std::size_t r = 0; r < K; ++r) {
197 if (n[r] == 0) continue;
198 n[r] -= 1;
199 const std::size_t idx_prev = detail::conv_hashpop(n, NK);
200 n[r] += 1;
201 Xm[idx] = T(Xm[idx] + L_conv(q, r) * Xm[idx_prev]);
202 }
203 }
204 } while (detail::conv_pprod_next(n, NK));
205
206 // the complement network, every station but this one
207 Matrix<T> L_comp(nQueues > 0 ? nQueues - 1 : 0, K, zero);
208 std::vector<lang::CdScaling<T>> cd_comp;
209 for (std::size_t j = 0, w = 0; j < nQueues; ++j) {
210 if (j == q) continue;
211 for (std::size_t r = 0; r < K; ++r) L_comp(w, r) = L_conv(j, r);
212 cd_comp.push_back(cd_conv[j]);
213 ++w;
214 }
215
216 std::vector<int> m(K, 0);
217 do {
218 bool anyPos = false;
219 for (int v : m)
220 if (v > 0) anyPos = true;
221 if (!anyPos) continue;
222 const std::size_t idx = detail::conv_hashpop(m, NK);
223 std::vector<int> nmi(K, 0);
224 for (std::size_t r = 0; r < K; ++r) nmi[r] = NK[r] - m[r];
225 const T G_comp = pfqn::pfqn_conv(L_comp, nmi, Z_conv, cd_comp).G;
226 const T prob = T(Xm[idx] * G_comp / G_N);
227 for (std::size_t r = 0; r < K; ++r)
228 QN(ist - 1, r) = T(QN(ist - 1, r) + num_traits<T>::from_int(m[r]) * prob);
229 } while (detail::conv_pprod_next(m, NK));
230 }
231
232 // RN is the PER-VISIT response time Qchain/Tchain: the deaggregation
233 // below multiplies the visit ratio back in, so dividing by Xchain would
234 // count it twice
235 for (std::size_t i = 0; i < M; ++i)
236 for (std::size_t r = 0; r < K; ++r) {
237 if (TN(i, r) != zero) RN(i, r) = T(QN(i, r) / TN(i, r));
238 UN(i, r) = T(TN(i, r) * ST(i, r));
239 }
240
241 for (std::size_t q = 0; q < nQueues; ++q) {
242 if (!cd_conv[q]) continue;
243 const std::size_t ist = queueIdx[q];
244 const std::vector<T>& peak = sn.stations[ist - 1].cdscalingpeak;
245 if (peak.empty())
246 throw UnsupportedError(
247 "solver_nc_conv: a class-dependent station has no declared peak rate; pass "
248 "peakRatePerClass to setClassDependence");
249 for (std::size_t c = 0; c < K; ++c) {
250 // The peaks are declared per class, so the chain takes the
251 // largest peak among its classes: utilization is a per-station
252 // quantity with one normalizer.
253 T bmax = zero;
254 for (std::size_t kk : sn.inchain[c])
255 if (peak[kk - 1] > bmax) bmax = peak[kk - 1];
256 if (bmax > zero) UN(ist - 1, c) = T(UN(ist - 1, c) / bmax);
257 }
258 }
259
260 const mva::ClassResults<T> cls =
261 mva::sn_deaggregate_chain_results(sn, d, Matrix<T>(), UN, RN, TN, XN);
262
263 out.sol.Q = cls.Q;
264 out.sol.U = cls.U;
265 out.sol.R = cls.R;
266 out.sol.Tp = cls.Tp;
267 out.sol.X = cls.X;
268 out.sol.C = cls.C;
269 out.sol.iter = 1;
270 out.sol.method = "conv";
271 out.actualmethod = "conv";
272 return out;
273 }
274}
275
276} // namespace nc
277} // namespace line
278
279#endif // LINE_SOLVERS_NC_SOLVER_NC_CONV_H
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
The exception types the port throws.
ClassResults< T > sn_deaggregate_chain_results(const qn::NetworkStruct< T > &L, const ChainDemands< T > &d, const Matrix< T > &Qchain, const Matrix< T > &Uchain, const Matrix< T > &Rchain, const Matrix< T > &Tchain, const std::vector< T > &Xchain)
Port of sn_deaggregate_chain_results.
Definition sn_chain.h:214
ChainDemands< T > sn_get_demands_chain(const qn::NetworkStruct< T > &L)
Port of sn_get_demands_chain.
Definition sn_chain.h:63
NcSolution< T > solver_nc_conv(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Port of solver_nc_conv.m.
NcResult< T > pfqn_conv(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const std::vector< CdScaling< T > > &cdscaling)
Multichain convolution algorithm with class-dependent service rates (Sauer 1983, "Computational Algor...
Definition pfqn_conv.h:76
Controls and result shape shared by the normalizing-constant analyzers.
A queueing network and its refreshed NetworkStruct.
Multichain convolution algorithm with class-dependent service rates (Sauer 1983, "Computational Algor...
Chain aggregation and de-aggregation.
The chain-level view of a layer, as sn_get_demands_chain returns it.
Definition sn_chain.h:46
std::vector< double > Nchain
(C) population, infinite for an open chain
Definition sn_chain.h:51
Matrix< T > STchain
(M x C) mean service time
Definition sn_chain.h:48
Matrix< T > Lchain
(M x C) demand
Definition sn_chain.h:47
Matrix< T > Vchain
(M x C) visits
Definition sn_chain.h:49
Class-level results, as sn_deaggregate_chain_results returns them.
Definition sn_chain.h:191
std::vector< T > C
Definition sn_chain.h:193
std::vector< T > X
Definition sn_chain.h:193
The [Q,U,R,T,C,X,lG] of the reference, plus the algorithm that ran.
Definition nc_types.h:113
mva::MvaSolution< T > sol
Definition nc_types.h:114
std::string actualmethod
Definition nc_types.h:115
Controls, defaulting to SolverOptions('NC') in the reference.
Definition nc_types.h:33