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 // Fold the JOINT-dependence handles (`jdscaling`, the non-product-form
136 // eta_i) into the same per-station handle the recursion reads. cd and jd
137 // are evaluated identically, and the product reproduces the
138 // single-mechanism case when only one is declared
139 // (solver_nc_conv.m:67-84). Ported 2026-09-13: before it, a
140 // joint-dependent model reached this recursion with eta DROPPED and was
141 // answered as the unscaled network.
142 for (std::size_t q = 0; q < nQueues; ++q) {
143 const lang::CdScaling<T>& jdh = sn.stations[queueIdx[q] - 1].jdscaling;
144 if (!jdh) continue;
145 if (!cd_conv[q]) {
146 cd_conv[q] = jdh;
147 } else {
148 const lang::CdScaling<T> cdh = cd_conv[q];
149 cd_conv[q] = [cdh, jdh](const std::vector<T>& ni) {
150 std::vector<T> a = cdh(ni);
151 const std::vector<T> b = jdh(ni);
152 for (std::size_t r = 0; r < a.size() && r < b.size(); ++r) a[r] = T(a[r] * b[r]);
153 return a;
154 };
155 }
156 }
157
158 const T G_N = pfqn::pfqn_conv(L_conv, NK, Z_conv, cd_conv).G;
159 out.sol.lG = num_traits<T>::log_as_double(G_N);
160
161 std::vector<T> XN(K, zero);
162 for (std::size_t r = 0; r < K; ++r)
163 if (NK[r] > 0) {
164 std::vector<int> Nm = NK;
165 --Nm[r];
166 const T G_Nk = pfqn::pfqn_conv(L_conv, Nm, Z_conv, cd_conv).G;
167 XN[r] = T(G_Nk / G_N);
168 }
169
170 Matrix<T> TN(M, K, zero), QN(M, K, zero), RN(M, K, zero), UN(M, K, zero);
171 for (std::size_t i = 0; i < M; ++i)
172 for (std::size_t r = 0; r < K; ++r) TN(i, r) = T(V(i, r) * XN[r]);
173
174 for (std::size_t i : delayIdx)
175 for (std::size_t r = 0; r < K; ++r)
176 QN(i - 1, r) = T(Ldemand(i - 1, r) * XN[r]);
177
178 std::size_t stateSpaceSize = 1;
179 for (int v : NK) stateSpaceSize *= static_cast<std::size_t>(v) + 1;
180
181 for (std::size_t q = 0; q < nQueues; ++q) {
182 const std::size_t ist = queueIdx[q];
183 const bool isCd = static_cast<bool>(cd_conv[q]);
184
185 // the station factor X_m(n), by the chain-dependent recurrence
186 std::vector<T> Xm(stateSpaceSize, zero);
187 Xm[0] = num_traits<T>::from_int(1);
188 std::vector<int> n(K, 0);
189 do {
190 int tot = 0;
191 for (int v : n) tot += v;
192 if (tot == 0) continue;
193 const std::size_t idx = detail::conv_hashpop(n, NK);
194 if (isCd) {
195 // X_m(n) = (|n|/n_r) (L/beta_r(n)) X_m(n - e_r) for ANY r with
196 // n_r > 0; the result is path independent, so the first is used
197 for (std::size_t r = 0; r < K; ++r) {
198 if (n[r] == 0) continue;
199 std::vector<T> row(K, zero);
200 for (std::size_t j = 0; j < K; ++j)
201 row[j] = num_traits<T>::from_int(n[j]);
202 const std::vector<T> bval = cd_conv[q](row);
203 if (bval.empty())
204 throw UnsupportedError(
205 "solver_nc_conv: a class-dependence map returned no value");
206 const T beta = bval.size() > 1 ? bval[r] : bval[0];
207 const int nr = n[r];
208 n[r] -= 1;
209 const std::size_t idx_prev = detail::conv_hashpop(n, NK);
210 n[r] += 1;
211 if (beta > zero)
212 Xm[idx] = T(num_traits<T>::from_int(tot) /
213 num_traits<T>::from_int(nr) * (L_conv(q, r) / beta) *
214 Xm[idx_prev]);
215 break;
216 }
217 } else {
218 for (std::size_t r = 0; r < K; ++r) {
219 if (n[r] == 0) continue;
220 n[r] -= 1;
221 const std::size_t idx_prev = detail::conv_hashpop(n, NK);
222 n[r] += 1;
223 Xm[idx] = T(Xm[idx] + L_conv(q, r) * Xm[idx_prev]);
224 }
225 }
226 } while (detail::conv_pprod_next(n, NK));
227
228 // the complement network, every station but this one
229 Matrix<T> L_comp(nQueues > 0 ? nQueues - 1 : 0, K, zero);
230 std::vector<lang::CdScaling<T>> cd_comp;
231 for (std::size_t j = 0, w = 0; j < nQueues; ++j) {
232 if (j == q) continue;
233 for (std::size_t r = 0; r < K; ++r) L_comp(w, r) = L_conv(j, r);
234 cd_comp.push_back(cd_conv[j]);
235 ++w;
236 }
237
238 std::vector<int> m(K, 0);
239 do {
240 bool anyPos = false;
241 for (int v : m)
242 if (v > 0) anyPos = true;
243 if (!anyPos) continue;
244 const std::size_t idx = detail::conv_hashpop(m, NK);
245 std::vector<int> nmi(K, 0);
246 for (std::size_t r = 0; r < K; ++r) nmi[r] = NK[r] - m[r];
247 const T G_comp = pfqn::pfqn_conv(L_comp, nmi, Z_conv, cd_comp).G;
248 const T prob = T(Xm[idx] * G_comp / G_N);
249 for (std::size_t r = 0; r < K; ++r)
250 QN(ist - 1, r) = T(QN(ist - 1, r) + num_traits<T>::from_int(m[r]) * prob);
251 } while (detail::conv_pprod_next(m, NK));
252 }
253
254 // RN is the PER-VISIT response time Qchain/Tchain: the deaggregation
255 // below multiplies the visit ratio back in, so dividing by Xchain would
256 // count it twice
257 for (std::size_t i = 0; i < M; ++i)
258 for (std::size_t r = 0; r < K; ++r) {
259 if (TN(i, r) != zero) RN(i, r) = T(QN(i, r) / TN(i, r));
260 UN(i, r) = T(TN(i, r) * ST(i, r));
261 }
262
263 for (std::size_t q = 0; q < nQueues; ++q) {
264 if (!cd_conv[q]) continue;
265 const std::size_t ist = queueIdx[q];
266 const qn::Station<T>& st = sn.stations[ist - 1];
267 const bool has_cd = static_cast<bool>(st.cdscaling);
268 const bool has_jd = static_cast<bool>(st.jdscaling);
269 if (has_cd && st.cdscalingpeak.empty())
270 throw UnsupportedError(
271 "solver_nc_conv: a class-dependent station has no declared peak rate; pass "
272 "peakRatePerClass to setClassDependence");
273 if (has_jd && st.jdscalingpeak.empty())
274 throw UnsupportedError(
275 "solver_nc_conv: a joint-dependent station has no declared peak rate; pass "
276 "peakRatePerClass to setJointDependence");
277 for (std::size_t c = 0; c < K; ++c) {
278 // The peaks are declared per class, so the chain takes the
279 // largest peak among its classes: utilization is a per-station
280 // quantity with one normalizer. A station declaring BOTH
281 // mechanisms normalizes by their product, as the handles multiply
282 // (solver_nc_conv.m:215-229).
283 T bmax = num_traits<T>::from_int(1);
284 if (has_cd) {
285 T m = zero;
286 for (std::size_t kk : sn.inchain[c])
287 if (st.cdscalingpeak[kk - 1] > m) m = st.cdscalingpeak[kk - 1];
288 bmax = T(bmax * m);
289 }
290 if (has_jd) {
291 T m = zero;
292 for (std::size_t kk : sn.inchain[c])
293 if (st.jdscalingpeak[kk - 1] > m) m = st.jdscalingpeak[kk - 1];
294 bmax = T(bmax * m);
295 }
296 if (bmax > zero) UN(ist - 1, c) = T(UN(ist - 1, c) / bmax);
297 }
298 }
299
300 const mva::ClassResults<T> cls =
301 mva::sn_deaggregate_chain_results(sn, d, Matrix<T>(), UN, RN, TN, XN);
302
303 out.sol.Q = cls.Q;
304 out.sol.U = cls.U;
305 out.sol.R = cls.R;
306 out.sol.Tp = cls.Tp;
307 out.sol.X = cls.X;
308 out.sol.C = cls.C;
309 out.sol.iter = 1;
310 out.sol.method = "conv";
311 out.actualmethod = "conv";
312 return out;
313 }
314}
315
316} // namespace nc
317} // namespace line
318
319#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.
std::function< std::vector< T >(const std::vector< T > &)> CdScaling
A class-dependent scaling map, sn.cdscaling.
Definition lang_types.h:731
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
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
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
One station of the network.
std::vector< T > jdscalingpeak
sn.jdscalingpeak for this station: the declared peak joint-dependent scaling per class.
CdScaling< T > jdscaling
sn.jdscaling for this station: MATLAB's Station.ljdScaling, the JOINT dependence map eta_i(n),...
std::vector< T > cdscalingpeak
sn.cdscalingpeak for this station: the DECLARED peak rate scaling per class, empty when the station i...
CdScaling< T > cdscaling
sn.cdscaling for this station: the class-dependence map, empty when unset.