LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
transform_solve.h
Go to the documentation of this file.
1// Copyright (c) 2012-2026, QORE Lab, Imperial College London
2// All rights reserved.
3#ifndef LINE_SOLVERS_TR_TRANSFORM_SOLVE_H
4#define LINE_SOLVERS_TR_TRANSFORM_SOLVE_H
5
6/**
7 * @file
8 * @ingroup line_solvers
9 * Solver-agnostic driver of a model TRANSFORMATION, the sibling of
10 * `solvers/mva/fj_driver.h`.
11 *
12 * A transformation rewrites the model into one or more subproblems, solves
13 * those with a REAL solver, and maps the metrics back onto the original classes
14 * and stations:
15 *
16 * expand -> solve -> lift
17 *
18 * THE INNER SOLVER IS THE OUTER SOLVER. The caller supplies `inner_solve` as a
19 * template parameter, exactly as `fj_driver.h` takes its `InnerSolve`, so a
20 * transformation written once serves every engine rather than the one it was
21 * first written for. `solver_ctmc_chain_aggregation` used to call
22 * `solver_ctmc_run_analyzer` directly, which is what kept a transform with
23 * nothing CTMC-specific in it out of reach of MVA, NC and Fluid.
24 *
25 * WHAT "ANY SOLVER" MEANS HERE, AND WHAT IT DOES NOT. The inner solve is a
26 * TEMPLATED ANALYZER, not the `solvers/solver.h` facade, which is a
27 * double-only interface whose only accessor is `avg_table()`. That reaches
28 * MVA, NC, CTMC and Fluid; it does NOT reach JMT or LDES, which are subprocess
29 * wrappers behind that facade.
30 *
31 * NO METHOD NAME TABLE HERE, deliberately. MATLAB, the JAR and python resolve
32 * `options.config.transform` through a method name table because their options carry
33 * a string-keyed config map. C++ selects the transform at the call site from a
34 * typed option (`CtmcOptions::chain_aggregation`), so a runtime method name table
35 * would add a failure mode without adding reach.
36 *
37 * Mirrors MATLAB `@NetworkSolver/transformSolve.m` with
38 * `solver_tr_chains_analyzer.m`, the JAR `jline.solvers.tr.TransformSolve` with
39 * `ChainsStrategy`, and python `line_solver/solvers/transform_driver.py`.
40 */
41
42#include <string>
43
44#include <algorithm>
45#include <cmath>
46#include <vector>
47
55
56namespace line {
57namespace tr {
58
59/**
60 * Chain aggregation: collapse every chain onto a single class, solve, and map
61 * the chain metrics back onto the classes.
62 *
63 * `api::sn_aggregate_chains` builds the collapsed model and
64 * `mva::sn_deaggregate_chain_results` maps its metrics back through alpha, the
65 * per-station share of the chain's visits each class carries.
66 *
67 * WHAT IS TRADED. Exactness on a non-product-form model: one aggregate service
68 * law, fitted to the alpha-weighted first two moments, replaces the per-class
69 * ones. On a product-form model the chain IS the unit MVA and convolution
70 * already solve in, so the answer is exact and the state space is the smaller
71 * one.
72 *
73 * SINGLE PASS: one solve of the aggregate determines the answer, so there is no
74 * sweep and no convergence test.
75 *
76 * @param sn the original struct
77 * @param inner_solve solves the aggregated struct, normally with the caller's
78 * own analyzer bound to its own options
79 * @param method the caller's method name, for the reported compound name
80 */
81template <class T, class InnerSolve>
83 InnerSolve inner_solve,
84 const std::string& method) {
85 // expand
87
88 // solve
89 const mva::AvgResult<T> chain = inner_solve(agg.model.get_struct());
90
91 // lift. The deaggregation reads the ORIGINAL struct's chain demands, which
92 // is where alpha, Lchain, STchain and Vchain all come from; the transform's
93 // own copy of them is the same object and is left to the caller.
96 sn, d, chain.QN, chain.UN, chain.RN, chain.TN, chain.XN);
97
99 out.QN = cls.Q;
100 out.UN = cls.U;
101 out.RN = cls.R;
102 out.TN = cls.Tp;
103 out.CN = cls.C;
104 out.XN = cls.X;
107 out.method = method;
108 out.actualmethod = method + "/chainaggr";
109 return out;
110}
111
112
113/**
114 * LOAD CONCEALMENT (Birman-Kogan Algorithm 2) as a transformation, and the
115 * first ITERATED one.
116 *
117 * Chain `l` is solved on its own against the residual capacity the others leave
118 * it, `A_i = 1 - sum_{k!=l} L(i,k) X_k`, so it sees the concealed demand
119 * `L(i,l)/A_i`. Sweeping the chains in GAUSS-SEIDEL order -- publishing each
120 * chain's throughput the moment it is known, so chain `l+1` of the same sweep
121 * already sees it -- and iterating to a fixed point is the algorithm.
122 *
123 * WHAT THIS ADDS OVER THE KERNEL. `pfqn_bklc` solves each single-chain
124 * subproblem on a DEMAND VECTOR with the inner solve hard-wired to MVA or the
125 * uniform expansion. Here the subproblem is a real single-class struct, so the
126 * inner solve is whichever analyzer the caller bound, which is what makes the
127 * concealment approximation measurable rather than merely asserted.
128 *
129 * IT IS NOT A STRICTLY BETTER LC: the kernel sees only `L`, while the chain
130 * aggregation refits the chain service law to two moments.
131 *
132 * THE TOLERANCE IS FIXED AT 1e-10, a PARITY requirement and not a knob: a
133 * looser one stops the sweep at a different iteration in each codebase.
134 *
135 * Mirrors MATLAB `solver_tr_lc_analyzer.m`, python `_lc_strategy` and the JAR
136 * `jline.solvers.tr.LcStrategy`.
137 *
138 * @param sn the original struct
139 * @param inner_solve solves one concealed single-class struct
140 * @param method the caller's method name, for the reported compound name
141 * @param iter_max sweep cap; the kernel's own default is 1000
142 */
143template <class T, class InnerSolve>
145 InnerSolve inner_solve,
146 const std::string& method,
147 std::size_t iter_max = 1000) {
148 const double kTol = 1e-10;
149 const double kFineTol = 1e-12;
150
151 // expand: chain aggregation, then one single-class struct per chain
153 const qn::NetworkStruct<T> snChain = agg.model.get_struct();
155 const std::size_t M = snChain.nstations;
156 const std::size_t R = dem.Lchain.cols();
157 if (snChain.nclasses != R)
158 throw InputError("transform_solve_lc: the chain-aggregated model has a class count that "
159 "differs from its chain count; load concealment needs one class per chain");
160
161 // The concealment slows QUEUEING stations only: a delay holds no queue, so
162 // zeroing its rows makes A come out as exactly 1 there.
163 std::vector<bool> is_delay(M, false);
165 std::vector<double> Z(R, 0.0), N(R, 0.0);
166 for (std::size_t i = 0; i < M; ++i)
167 is_delay[i] = snChain.stations[i].sched == qn::SchedStrategy::INF;
168 for (std::size_t r = 0; r < R; ++r) {
169 N[r] = dem.Nchain[r];
170 for (std::size_t i = 0; i < M; ++i) {
171 if (is_delay[i])
172 Z[r] += num_traits<T>::to_double(dem.Lchain(i, r));
173 else
174 L(i, r) = dem.Lchain(i, r);
175 }
176 }
177
178 // One single-class struct per chain, built ONCE and re-concealed in place.
179 std::vector<qn::NetworkStruct<T>> subs;
180 std::vector<std::vector<lang::Distrib<T>>> base(R);
181 subs.reserve(R);
182 for (std::size_t l = 0; l < R; ++l) {
183 qn::NetworkStruct<T> m = snChain;
184 // Descending, so removing a higher class never shifts the index of the
185 // one being kept or of any class still to be removed.
186 for (std::size_t k = R; k >= 1; --k)
187 if (k != l + 1) m = api::sn_remove_class(m, k);
188 subs.push_back(m);
189 base[l].resize(M);
190 for (std::size_t i = 0; i < M; ++i)
191 if (!is_delay[i]) base[l][i] = m.service[i][0];
192 }
193
194 // Step 1 of Algorithm 2, reproducing the kernel's own seed: the saddle point
195 // utilizations of Corollary 1, the N/(Z+sum L) fallback and the 1/max L
196 // capacity clamp. Seeding identically is what keeps the SWEEP COUNT, and not
197 // only the fixed point, comparable with the kernel and the other codebases.
198 std::vector<double> X(R, 0.0);
199 // GUARDED AT COMPILE TIME. pfqn_bk static_asserts on
200 // num_traits<T>::has_transcendental (its saddle point expansion needs log),
201 // so at T = Rational the call must not be instantiated at all; an `if` alone
202 // would still compile the branch and fire the assert. Rational then starts
203 // from the closed-form bound below, which costs a few extra sweeps but
204 // reaches the same fixed point.
205 if constexpr (num_traits<T>::has_transcendental) {
206 std::vector<T> Nv(R), Zv(R);
207 for (std::size_t r = 0; r < R; ++r) {
208 Nv[r] = num_traits<T>::from_double(N[r]);
209 Zv[r] = num_traits<T>::from_double(Z[r]);
210 }
211 try {
212 const pfqn::BkResult<T> bk = pfqn::pfqn_bk(L, Nv, Zv);
213 if (bk.X.size() == R)
214 for (std::size_t r = 0; r < R; ++r) X[r] = num_traits<T>::to_double(bk.X[r]);
215 } catch (const std::exception&) {
216 // the seed is a starting point, not an answer: an unusable saddle
217 // point falls through to the closed-form bound below
218 }
219 }
220 for (std::size_t r = 0; r < R; ++r) {
221 if (!std::isfinite(X[r]) || X[r] < 0) X[r] = 0.0;
222 double sum = Z[r], cap = 0.0;
223 for (std::size_t i = 0; i < M; ++i) {
224 const double l = num_traits<T>::to_double(L(i, r));
225 sum += l;
226 cap = std::max(cap, l);
227 }
228 if (X[r] == 0.0 && N[r] > 0 && sum > 0) X[r] = N[r] / sum;
229 if (cap > 0) X[r] = std::min(X[r], 1.0 / cap);
230 }
231
232 auto conceal_all = [&]() {
233 for (std::size_t l = 0; l < R; ++l)
234 for (std::size_t i = 0; i < M; ++i) {
235 if (is_delay[i]) continue;
236 double busy = 0.0;
237 for (std::size_t k = 0; k < R; ++k)
238 if (k != l) busy += num_traits<T>::to_double(L(i, k)) * X[k];
239 const double a = std::max(1.0 - busy, kFineTol);
240 // dist_scale_rate multiplies the RATE, so a factor of A_i divides
241 // the mean by A_i: exactly the concealed demand L(i,l)/A_i.
242 subs[l].set_service(i + 1, 1, lang::dist_scale_rate(base[l][i],
244 }
245 };
246 conceal_all();
247
248 std::vector<mva::AvgResult<T>> res(R);
249 std::vector<double> Xold(R, -1.0);
250 std::size_t iters = 0;
251 for (std::size_t it = 1; it <= std::max<std::size_t>(1, iter_max); ++it) {
252 iters = it;
253 for (std::size_t l = 0; l < R; ++l) {
254 res[l] = inner_solve(subs[l]);
255 // GAUSS-SEIDEL: publish chain l now, so chain l+1 of this same sweep
256 // already sees it.
257 X[l] = res[l].XN.empty() ? 0.0 : num_traits<T>::to_double(res[l].XN[0]);
258 if (!(X[l] >= 0)) X[l] = 0.0;
259 conceal_all();
260 }
261 double diff = 0.0, scale = 1.0;
262 for (std::size_t r = 0; r < R; ++r) {
263 diff = std::max(diff, std::abs(X[r] - Xold[r]));
264 scale = std::max(scale, std::abs(X[r]));
265 }
266 Xold = X;
267 if (diff <= kTol * scale) break;
268 }
269
270 // lift: reassemble the chain table, then deaggregate onto the classes
273 std::vector<T> Xc(R, num_traits<T>::from_int(0));
274 for (std::size_t l = 0; l < R; ++l) {
275 for (std::size_t i = 0; i < M && i < res[l].QN.rows(); ++i) {
276 Q(i, l) = res[l].QN(i, 0);
277 U(i, l) = res[l].UN(i, 0);
278 Rr(i, l) = res[l].RN(i, 0);
279 Tp(i, l) = res[l].TN(i, 0);
280 }
281 Xc[l] = num_traits<T>::from_double(X[l]);
282 }
283
285 if (sn.nchains >= sn.nclasses) {
286 // ONE CLASS PER CHAIN: the chain answer already IS the class answer, so
287 // the lift is a re-indexing rather than a deaggregation.
288 out.QN = Q; out.UN = U; out.RN = Rr; out.TN = Tp; out.XN = Xc;
289 out.CN.assign(R, num_traits<T>::from_int(0));
290 for (std::size_t l = 0; l < R; ++l) {
291 T acc = num_traits<T>::from_int(0);
292 for (std::size_t i = 0; i < M; ++i) acc = acc + Rr(i, l);
293 out.CN[l] = acc;
294 }
295 } else {
297 const mva::ClassResults<T> cls =
298 mva::sn_deaggregate_chain_results(sn, d, Q, U, Rr, Tp, Xc);
299 out.QN = cls.Q; out.UN = cls.U; out.RN = cls.R;
300 out.TN = cls.Tp; out.CN = cls.C; out.XN = cls.X;
301 }
304 out.method = method;
305 out.actualmethod = method + "/lc";
306 out.iter = static_cast<int>(iters);
307 return out;
308}
309
310} // namespace tr
311} // namespace line
312
313#endif // LINE_SOLVERS_TR_TRANSFORM_SOLVE_H
InputError(const std::string &what)
Definition error.h:39
A network plus its refreshed NetworkStruct.
std::vector< std::vector< Distrib< T > > > service
service[i][r], 0-based station and class; a disabled entry marks a pair never visited.
std::vector< Station< T > > stations
stations[k-1] is the k-th station
Rate-scaled copy of a distribution, preserving its shape.
ChainAggregationResult< T > sn_aggregate_chains(const qn::NetworkStruct< T > &sn, const std::string &suffix=std::string())
Collapse every chain onto one class, port of ModelAdapter.aggregateChains.
qn::NetworkStruct< T > sn_remove_class(const qn::NetworkStruct< T > &sn, std::size_t cls)
The model without class cls (1-based), leaving sn untouched.
Distrib< T > dist_scale_rate(const Distrib< T > &d, const T &factor)
The law of X / factor, in the same family as d.
Matrix< T > sn_get_residt_from_respt(const qn::NetworkStruct< T > &L, const Matrix< T > &RN)
Port of sn_get_residt_from_respt: the per-JOB residence time.
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
Matrix< T > sn_get_arvr_from_tput(const qn::NetworkStruct< T > &L, const Matrix< T > &TN)
ChainDemands< T > sn_get_demands_chain(const qn::NetworkStruct< T > &L)
Port of sn_get_demands_chain.
Definition sn_chain.h:63
BkResult< T > pfqn_bk(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z)
Birman-Kogan saddle point normalizing constant with bottleneck detection.
Definition pfqn_bk.h:162
mva::AvgResult< T > transform_solve_lc(const qn::NetworkStruct< T > &sn, InnerSolve inner_solve, const std::string &method, std::size_t iter_max=1000)
LOAD CONCEALMENT (Birman-Kogan Algorithm 2) as a transformation, and the first ITERATED one.
mva::AvgResult< T > transform_solve_chains(const qn::NetworkStruct< T > &sn, InnerSolve inner_solve, const std::string &method)
Chain aggregation: collapse every chain onto a single class, solve, and map the chain metrics back on...
A queueing network and its refreshed NetworkStruct.
Birman-Kogan asymptotic evaluation of closed networks with many stations.
Collapse every chain onto one class, port of ModelAdapter.aggregateChains.
Chain aggregation and de-aggregation.
Drop one job class from a model, port of ModelAdapter.removeClass.
The SolverMVA class surface: @@SolverMVA/runAnalyzer.m and the gates around it.
What sn_aggregate_chains returns.
qn::Network< T > model
the aggregate, one class per chain
The metrics getAvg returns, after filtering.
Matrix< T > TN
throughput
Matrix< T > RN
response time, per visit
Matrix< T > UN
utilization
Matrix< T > WN
residence time, per job
std::string method
the method asked for
std::string actualmethod
the algorithm that ran
Matrix< T > QN
queue length
std::vector< T > CN
system response time per class
std::vector< T > XN
system throughput per class
Matrix< T > AN
arrival rate
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 > Lchain
(M x C) demand
Definition sn_chain.h:47
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
Return value of pfqn_bk, mirroring [G, lG, X, U, A, B].
Definition pfqn_bk.h:55
std::vector< T > X
the saddle point coordinates
Definition pfqn_bk.h:58