LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_nc_cdf.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_CDF_H
6#define LINE_SOLVERS_NC_SOLVER_NC_CDF_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `@@SolverNC/getCdfRespT.m`, and of its two aliases `getSjrnT` and
12 * `sjrnT`.
13 *
14 * WHAT IT ADDS OVER THE AVERAGE TABLE. `getAvg` reports E[R]; this reports the
15 * whole law Pr[R <= t], which is what a percentile or a service-level target
16 * needs. The two are not interchangeable: a product-form response time is not
17 * exponential, so E[R] does not determine its tail.
18 *
19 * TWO KINDS OF STATION, TWO DIFFERENT LAWS. At an FCFS queue the sojourn time
20 * is the tagged-job passage time through a closed network and comes from
21 * `pfqn_stdf` (exact) or `pfqn_stdf_heur` (the `rd` heuristic). At a delay
22 * station there is no queueing at all, so the sojourn time IS the service time
23 * and the law is the distribution's own CDF, taken through `map_cdf`. The
24 * reference computes both and returns them in one station-indexed table.
25 *
26 * WHY THE TIME GRID IS WHAT IT IS. The reference builds
27 * `logspace(0, 2 log10 T, 100)` with `T = max(sum(N) * mean(1/rates))` over the
28 * FCFS stations: the population times the mean service time is the scale on
29 * which the FCFS subsystem empties, and squaring it gives a grid that still
30 * resolves the tail. It is reproduced exactly, because the grid is part of the
31 * answer -- the CDF is reported AT these points. Note which axis that `mean`
32 * runs along: `sn.rates(fcfsNodeIds,:)` is (stations x classes) and MATLAB's
33 * `mean` takes the FIRST NON-SINGLETON dimension, so it averages over STATIONS
34 * and `max` then runs over classes -- except with a single FCFS station, where
35 * the row is 1xR and the same call averages over CLASSES instead. Both branches
36 * are reproduced below; see `_kb/06-solver-catalog.md` (NC, getCdfRespT).
37 *
38 * TWO PROPERTIES OF THAT GRID TO KNOW BEFORE READING ITS OUTPUT, neither of
39 * them a porting artifact. It DESCENDS when T < 1, because 2 log10 T is then
40 * negative, and it COLLAPSES to a hundred identical points at T = 1 exactly.
41 * Delay(1)+FCFS(3) with two classes at N=3 hits the second case: T = 3 * 1/3.
42 * The law is still correct at whatever points the grid names.
43 *
44 * REFUSED, as the reference refuses it: a model with no FCFS station. MATLAB
45 * warns and returns an empty cell array, which reads as a completed analysis
46 * with no data; this throws instead.
47 *
48 * ARITHMETIC. `pfqn_stdf` and `map_cdf` are transcendental, and the grid itself
49 * is a `logspace`, so a non-transcendental backend is refused by name.
50 */
51
52#include <algorithm>
53#include <cmath>
54#include <cstddef>
55#include <string>
56#include <vector>
57
65#include "line/util/error.h"
66#include "line/util/matrix.h"
67
68namespace line {
69namespace nc {
70
71/**
72 * The response-time distributions, station by class.
73 *
74 * `RD[i][r]` is a two-column matrix whose first column is F(t) and whose second
75 * is t, which is the shape `setDistribResults` stores in the reference. An
76 * empty entry means the station-class pair has no law (a Queue that is not
77 * FCFS, or a class the station does not serve).
78 */
79template <class T>
81 std::vector<std::vector<Matrix<T>>> RD;
82 std::vector<T> tset; ///< the shared evaluation grid
83 /**
84 * Non-empty when the reference WARNS AND RETURNS EMPTY rather than
85 * computing: today only "applies only to FCFS nodes". `RD` is then empty,
86 * which a caller can test by size -- unlike the zero table the
87 * normalizing-constant path returns when a method declines a model.
88 */
89 std::string warning;
90};
91
92/**
93 * Port of `@@SolverNC/getCdfRespT.m`.
94 *
95 * @param sn the refreshed struct
96 * @param opt solver controls; `opt.cdf_algorithm` selects 'exact' or 'rd'
97 */
98template <class T>
101 if constexpr (!num_traits<T>::has_transcendental) {
102 (void)sn;
103 (void)opt;
104 throw UnsupportedError(
105 "solver_nc_cdf_respt: the sojourn-time law is evaluated on a logarithmic time grid "
106 "and inverts a generating function; it needs transcendental arithmetic");
107 } else {
108 const T zero = num_traits<T>::from_int(0);
109 const std::size_t M = sn.nstations, R = sn.nclasses;
110
111 if (opt.cdf_algorithm != "exact" && opt.cdf_algorithm != "rd")
112 throw UnsupportedError("solver_nc_cdf_respt: config.algorithm '" + opt.cdf_algorithm +
113 "' is unsupported; use 'exact' (pfqn_stdf) or 'rd' "
114 "(pfqn_stdf_heur)");
115
116 // The reference indexes fcfsNodes into the rows of the NON-DELAY
117 // stations and fcfsNodeIds into all stations; both are kept, because
118 // pfqn_stdf is given the queueing rows only while the result is
119 // reported against the full station list.
120 std::vector<std::size_t> fcfsStations, delayStations;
121 for (std::size_t i = 0; i < M; ++i) {
122 if (sn.stations[i].sched == qn::SchedStrategy::INF) {
123 delayStations.push_back(i + 1);
124 } else if (sn.stations[i].sched == qn::SchedStrategy::FCFS) {
125 fcfsStations.push_back(i + 1);
126 }
127 }
128 if (fcfsStations.empty()) {
129 // ALIGNED TO MATLAB (the empty-result ruling of 2026-07-25, register
130 // row N1 in a second shape). `getCdfRespT.m:44-45` warns
131 // "getCdfRespT applies only to FCFS nodes" and RETURNS with RD = {},
132 // never calling setDistribResults. This returns the same empty
133 // result rather than throwing. It is less lossy than N1's zero
134 // table: a caller can detect an empty result by SIZE, where a table
135 // of zeros is indistinguishable from a model that holds no jobs.
136 out.warning = "getCdfRespT applies only to FCFS nodes.";
137 return out;
138 }
139 for (double n : sn.njobs())
140 if (!std::isfinite(n))
141 throw UnsupportedError(
142 "solver_nc_cdf_respt: the tagged-job sojourn-time law is defined on a CLOSED "
143 "network; this model has an open class");
144
146
147 // The FCFS stations in the index space of the NON-DELAY stations, which
148 // is `fcfsNodes` in the reference and is what pfqn_stdf wants: the rows
149 // of D are the Queue nodes, and on a closed model the non-delay
150 // stations are exactly those.
151 std::vector<std::size_t> fcfsNonDelayPos;
152 {
153 std::size_t pos = 0;
154 for (std::size_t i = 0; i < M; ++i) {
155 if (sn.stations[i].sched == qn::SchedStrategy::INF) continue;
156 ++pos;
157 if (sn.stations[i].sched == qn::SchedStrategy::FCFS) fcfsNonDelayPos.push_back(pos);
158 }
159 }
160
161 // T = max over the FCFS stations of sum(N) * mean(1/rate).
162 //
163 // The reference USED to index `sn.rates` with `fcfsNodes`, which is in
164 // the non-delay station space while sn.rates is indexed by station, so
165 // with a Delay declared first the grid was scaled by the DELAY's service
166 // time. Found by this port and fixed in `@@SolverNC/getCdfRespT.m` with
167 // the user's approval; `fcfsNodeIds` is the station-indexed variable the
168 // next line of the reference already computes. `fcfsNodes` is still
169 // correct where it is passed to pfqn_stdf, and is left alone there.
170 double Nsum = 0.0;
171 for (double n : sn.njobs()) Nsum += n;
172 const std::size_t K = fcfsStations.size();
173 double Tmax = 0.0;
174 bool anyRow = false;
175 if (K == 1) {
176 // 1xR: MATLAB's mean collapses the CLASS axis and max sees a scalar.
177 const std::size_t ist = fcfsStations[0];
178 double acc = 0.0;
179 bool nan_row = false;
180 for (std::size_t r = 0; r < R; ++r) {
181 if (sn.disabled[ist - 1][r]) {
182 nan_row = true; // MATLAB: 1/NaN is NaN and mean() propagates it
183 break;
184 }
185 acc += 1.0 / num_traits<T>::to_double(sn.rates(ist - 1, r));
186 }
187 if (!nan_row) {
188 Tmax = Nsum * acc / static_cast<double>(R);
189 anyRow = Tmax > 0.0;
190 }
191 } else {
192 for (std::size_t r = 0; r < R; ++r) {
193 double acc = 0.0;
194 bool nan_col = false;
195 for (std::size_t ist : fcfsStations) {
196 if (sn.disabled[ist - 1][r]) {
197 nan_col = true; // one disabled pair makes the whole class NaN
198 break;
199 }
200 acc += 1.0 / num_traits<T>::to_double(sn.rates(ist - 1, r));
201 }
202 if (nan_col) continue; // MATLAB's max ignores the NaN this class produces
203 const double v = Nsum * acc / static_cast<double>(K);
204 anyRow = true;
205 if (v > Tmax) Tmax = v;
206 }
207 }
208 if (!anyRow || !(Tmax > 0.0))
209 throw NumericError(
210 "solver_nc_cdf_respt: the time grid has no scale; every FCFS station's service "
211 "rate is undefined or non-positive");
212
213 // logspace(0, 2*log10(T), 100)
214 const std::size_t npts = 100;
215 const double hi = 2.0 * std::log10(Tmax);
216 out.tset.assign(npts, zero);
217 for (std::size_t j = 0; j < npts; ++j) {
218 const double e = hi * static_cast<double>(j) / static_cast<double>(npts - 1);
219 out.tset[j] = num_traits<T>::from_double(std::pow(10.0, e));
220 }
221
222 // pfqn_stdf wants the FCFS stations as 0-based indices into the ROWS OF
223 // D, which are the Queue nodes. On a closed model the non-delay
224 // stations ARE the queues, so the reference's `fcfsNodes` is already in
225 // that space and only needs rebasing to zero.
226 std::vector<std::size_t> fcfsRows;
227 for (std::size_t pos : fcfsNonDelayPos) {
228 if (pos > pf.queue_stations.size())
229 throw UnsupportedError(
230 "solver_nc_cdf_respt: an FCFS station has no row in the product-form demand "
231 "matrix; the model has a non-Queue, non-Delay station between them");
232 fcfsRows.push_back(pos - 1);
233 }
234 Matrix<T> rates(pf.queue_stations.size(), R, zero);
235 for (std::size_t i = 0; i < pf.queue_stations.size(); ++i)
236 for (std::size_t r = 0; r < R; ++r)
237 if (!sn.disabled[pf.queue_stations[i] - 1][r])
238 rates(i, r) = sn.rates(pf.queue_stations[i] - 1, r);
239
240 std::vector<int> N(R, 0);
241 for (std::size_t r = 0; r < R; ++r)
242 N[r] = static_cast<int>(std::llround(pf.N[r]));
243
244 // pfqn_stdf takes integer server counts; an infinite one cannot appear
245 // here because every row of D is a Queue node.
246 std::vector<int> S(pf.S.size(), 1);
247 for (std::size_t i = 0; i < pf.S.size(); ++i) {
248 if (!std::isfinite(pf.S[i]))
249 throw UnsupportedError(
250 "solver_nc_cdf_respt: a queueing station has infinitely many servers, which "
251 "the tagged-job passage time cannot represent");
252 S[i] = static_cast<int>(std::llround(pf.S[i]));
253 }
254 const pfqn::StdfResult<T> sd =
255 opt.cdf_algorithm == "exact"
256 ? pfqn::pfqn_stdf(pf.D, N, pf.Z, S, fcfsRows, rates, out.tset)
257 : pfqn::pfqn_stdf_heur(pf.D, N, pf.Z, S, fcfsRows, rates, out.tset);
258
259 out.RD.assign(M, std::vector<Matrix<T>>(R));
260 for (std::size_t i = 0; i < fcfsStations.size() && i < sd.RD.size(); ++i)
261 for (std::size_t r = 0; r < R && r < sd.RD[i].size(); ++r)
262 out.RD[fcfsStations[i] - 1][r] = sd.RD[i][r];
263
264 // A delay station queues for nothing, so its sojourn law is the service
265 // distribution itself.
266 for (std::size_t ist : delayStations)
267 for (std::size_t r = 0; r < R; ++r) {
268 if (sn.disabled[ist - 1][r]) continue;
269 const std::vector<T> F = mam::map_cdf(
270 lang::dist_to_map(sn.service[ist - 1][r]), out.tset);
271 Matrix<T> A(npts, 2, zero);
272 for (std::size_t j = 0; j < npts; ++j) {
273 A(j, 0) = F[j];
274 A(j, 1) = out.tset[j];
275 }
276 out.RD[ist - 1][r] = A;
277 }
278 return out;
279 }
280}
281
282/** Port of `@@SolverNC/getSjrnT.m`: an alias of getCdfRespT. */
283template <class T>
287
288} // namespace nc
289} // namespace line
290
291#endif // LINE_SOLVERS_NC_SOLVER_NC_CDF_H
NumericError(const std::string &what)
Definition error.h:45
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
What refreshProcessRepresentations and refreshLST compute FROM a distribution: the (D0,...
The exception types the port throws.
Cumulative distribution of the inter-arrival time of a MAP.
Dense matrix and non-owning view.
mam::Map< T > dist_to_map(const Distrib< T > &d)
std::vector< T > map_cdf(const Map< T > &m, const std::vector< T > &points)
Cumulative distribution of the inter-arrival time at the given points.
Definition map_cdf.h:63
CdfRespTResult< T > solver_nc_sjrnt(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Port of @@SolverNC/getSjrnT.m: an alias of getCdfRespT.
CdfRespTResult< T > solver_nc_cdf_respt(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Port of @@SolverNC/getCdfRespT.m.
PfParams< T > sn_get_product_form_params(const qn::NetworkStruct< T > &sn)
Port of sn_get_product_form_params.
StdfResult< T > pfqn_stdf(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const std::vector< int > &S, const std::vector< std::size_t > &fcfsNodes, const Matrix< T > &rates, const std::vector< T > &tset)
Sojourn-time distribution at the listed FCFS stations.
Definition pfqn_stdf.h:322
StdfResult< T > pfqn_stdf_heur(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const std::vector< int > &S, const std::vector< std::size_t > &fcfsNodes, const Matrix< T > &rates, const std::vector< T > &tset)
Heuristic sojourn-time distribution at the listed FCFS stations.
Controls and result shape shared by the normalizing-constant analyzers.
A queueing network and its refreshed NetworkStruct.
Sojourn-time distribution at multiserver FCFS stations of a closed product-form network (J.
Heuristic sojourn-time distribution at multiserver FCFS stations, a variant of J.
Port of matlab/src/api/sn/sn_get_product_form_params.m: the CLASS-level product-form parameters.
The response-time distributions, station by class.
std::vector< std::vector< Matrix< T > > > RD
std::string warning
Non-empty when the reference WARNS AND RETURNS EMPTY rather than computing: today only "applies only ...
std::vector< T > tset
the shared evaluation grid
Controls, defaulting to SolverOptions('NC') in the reference.
Definition nc_types.h:33
The [lambda,D,N,Z,mu,S,V] of the reference.
std::vector< double > S
(Mq) server counts
Matrix< T > Z
(max(1,Mz) x R) demand at the delay stations
std::vector< double > N
(R) population, infinite on an open class
Matrix< T > D
(Mq x R) demand at the queueing stations
std::vector< std::size_t > queue_stations
(Mq) 1-based station indices
Result of pfqn_stdf / pfqn_stdf_heur, mirroring the MATLAB cell array RD.
Definition pfqn_stdf.h:97
std::vector< std::vector< Matrix< T > > > RD
Definition pfqn_stdf.h:98