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(log10 T - 2, log10 T + 1, 100)`, where T is sum(N) times the mean
28 * service time (averaged over the classes a station serves) at the slowest FCFS
29 * station, or a Delay's longest mean service time when that is longer. It is
30 * reproduced exactly, because the grid is part of the answer -- the CDF is
31 * reported AT these points. The former `logspace(0, 2 log10 T, 100)` started
32 * at t = 1 whatever the scale, so a fast system was at CDF ~ 1 on its first
33 * point; for T < 1 it ran DOWN to T^2 and at T = 1 it collapsed to a hundred
34 * identical points. See `_kb/06-solver-catalog.md` (NC, getCdfRespT).
35 *
36 * REFUSED, as the reference refuses it: a model with no FCFS station. MATLAB
37 * warns and returns an empty cell array, which reads as a completed analysis
38 * with no data; this throws instead.
39 *
40 * ARITHMETIC. `pfqn_stdf` and `map_cdf` are transcendental, and the grid itself
41 * is a `logspace`, so a non-transcendental backend is refused by name.
42 */
43
44#include <algorithm>
45#include <cmath>
46#include <cstddef>
47#include <string>
48#include <vector>
49
57#include "line/util/error.h"
58#include "line/util/matrix.h"
59
60namespace line {
61namespace nc {
62
63/**
64 * The response-time distributions, station by class.
65 *
66 * `RD[i][r]` is a two-column matrix whose first column is F(t) and whose second
67 * is t, which is the shape `setDistribResults` stores in the reference. An
68 * empty entry means the station-class pair has no law (a Queue that is not
69 * FCFS, or a class the station does not serve).
70 */
71template <class T>
73 std::vector<std::vector<Matrix<T>>> RD;
74 std::vector<T> tset; ///< the shared evaluation grid
75 /**
76 * Non-empty when the reference WARNS AND RETURNS EMPTY rather than
77 * computing: today only "applies only to FCFS nodes". `RD` is then empty,
78 * which a caller can test by size -- unlike the zero table the
79 * normalizing-constant path returns when a method declines a model.
80 */
81 std::string warning;
82};
83
84/**
85 * Port of `@@SolverNC/getCdfRespT.m`.
86 *
87 * @param sn the refreshed struct
88 * @param opt solver controls; `opt.cdf_algorithm` selects 'exact' or 'rd'
89 */
90template <class T>
93 if constexpr (!num_traits<T>::has_transcendental) {
94 (void)sn;
95 (void)opt;
96 throw UnsupportedError(ErrorKind{"Arithmetic"},
97 "solver_nc_cdf_respt: the sojourn-time law is evaluated on a logarithmic time grid "
98 "and inverts a generating function; it needs transcendental arithmetic");
99 } else {
100 const T zero = num_traits<T>::from_int(0);
101 const std::size_t M = sn.nstations, R = sn.nclasses;
102
103 if (opt.cdf_algorithm != "exact" && opt.cdf_algorithm != "rd")
104 throw UnsupportedError(ErrorKind{"Option"}, "solver_nc_cdf_respt: config.algorithm '" + opt.cdf_algorithm +
105 "' is unsupported; use 'exact' (pfqn_stdf) or 'rd' "
106 "(pfqn_stdf_heur)");
107
108 // The reference indexes fcfsNodes into the rows of the NON-DELAY
109 // stations and fcfsNodeIds into all stations; both are kept, because
110 // pfqn_stdf is given the queueing rows only while the result is
111 // reported against the full station list.
112 std::vector<std::size_t> fcfsStations, delayStations;
113 for (std::size_t i = 0; i < M; ++i) {
114 if (sn.stations[i].sched == qn::SchedStrategy::INF) {
115 delayStations.push_back(i + 1);
116 } else if (sn.stations[i].sched == qn::SchedStrategy::FCFS) {
117 fcfsStations.push_back(i + 1);
118 }
119 }
120 if (fcfsStations.empty()) {
121 // ALIGNED TO MATLAB (the empty-result ruling of 2026-07-25, register
122 // row N1 in a second shape). `getCdfRespT.m:44-45` warns
123 // "getCdfRespT applies only to FCFS nodes" and RETURNS with RD = {},
124 // never calling setDistribResults. This returns the same empty
125 // result rather than throwing. It is less lossy than N1's zero
126 // table: a caller can detect an empty result by SIZE, where a table
127 // of zeros is indistinguishable from a model that holds no jobs.
128 out.warning = "getCdfRespT applies only to FCFS nodes.";
129 return out;
130 }
131 for (double n : sn.njobs())
132 if (!std::isfinite(n))
133 throw UnsupportedError(ErrorKind{"OpenClass"},
134 "solver_nc_cdf_respt: the tagged-job sojourn-time law is defined on a CLOSED "
135 "network; this model has an open class");
136
138
139 // The FCFS stations in the index space of the NON-DELAY stations, which
140 // is `fcfsNodes` in the reference and is what pfqn_stdf wants: the rows
141 // of D are the Queue nodes, and on a closed model the non-delay
142 // stations are exactly those.
143 std::vector<std::size_t> fcfsNonDelayPos;
144 {
145 std::size_t pos = 0;
146 for (std::size_t i = 0; i < M; ++i) {
147 if (sn.stations[i].sched == qn::SchedStrategy::INF) continue;
148 ++pos;
149 if (sn.stations[i].sched == qn::SchedStrategy::FCFS) fcfsNonDelayPos.push_back(pos);
150 }
151 }
152
153 // T is the time scale of a sojourn, as `getCdfRespT.m` computes it since
154 // 2026-10-16: sum(N) times the mean (over the classes the station serves)
155 // of 1/rate, at the slowest FCFS station; a Delay's own longest mean
156 // service when that is longer, since its CDF is reported on the same
157 // grid. MATLAB reads a disabled pair as NaN: `mean(...,2,'omitnan')`
158 // skips it and `max` ignores a row that is NaN throughout.
159 double Nsum = 0.0;
160 for (double n : sn.njobs()) Nsum += n;
161 double Tmax = 0.0;
162 bool anyRow = false;
163 for (std::size_t ist : fcfsStations) {
164 double acc = 0.0;
165 std::size_t cnt = 0;
166 for (std::size_t r = 0; r < R; ++r) {
167 if (sn.disabled[ist - 1][r]) continue;
168 acc += 1.0 / num_traits<T>::to_double(sn.rates(ist - 1, r));
169 ++cnt;
170 }
171 if (cnt == 0) continue;
172 const double v = Nsum * acc / static_cast<double>(cnt);
173 if (!anyRow || v > Tmax) Tmax = v;
174 anyRow = true;
175 }
176 for (std::size_t ist : delayStations)
177 for (std::size_t r = 0; r < R; ++r) {
178 if (sn.disabled[ist - 1][r]) continue;
179 const double v = 1.0 / num_traits<T>::to_double(sn.rates(ist - 1, r));
180 if (!anyRow || v > Tmax) Tmax = v;
181 anyRow = true;
182 }
183 if (!anyRow || !(Tmax > 0.0) || !std::isfinite(Tmax))
184 throw NumericError(ErrorKind{"Degenerate"},
185 "solver_nc_cdf_respt: the time grid has no scale; every FCFS station's service "
186 "rate is undefined or non-positive");
187
188 // logspace(log10(T)-2, log10(T)+1, 100): two decades below T, one above.
189 const std::size_t npts = 100;
190 const double lo = std::log10(Tmax) - 2.0;
191 out.tset.assign(npts, zero);
192 for (std::size_t j = 0; j < npts; ++j) {
193 const double e = lo + 3.0 * static_cast<double>(j) / static_cast<double>(npts - 1);
194 out.tset[j] = num_traits<T>::from_double(std::pow(10.0, e));
195 }
196
197 // pfqn_stdf wants the FCFS stations as 0-based indices into the ROWS OF
198 // D, which are the Queue nodes. On a closed model the non-delay
199 // stations ARE the queues, so the reference's `fcfsNodes` is already in
200 // that space and only needs rebasing to zero.
201 std::vector<std::size_t> fcfsRows;
202 for (std::size_t pos : fcfsNonDelayPos) {
203 if (pos > pf.queue_stations.size())
204 throw UnsupportedError(ErrorKind{"SchedStrategy_FCFS"},
205 "solver_nc_cdf_respt: an FCFS station has no row in the product-form demand "
206 "matrix; the model has a non-Queue, non-Delay station between them");
207 fcfsRows.push_back(pos - 1);
208 }
209 Matrix<T> rates(pf.queue_stations.size(), R, zero);
210 for (std::size_t i = 0; i < pf.queue_stations.size(); ++i)
211 for (std::size_t r = 0; r < R; ++r)
212 if (!sn.disabled[pf.queue_stations[i] - 1][r])
213 rates(i, r) = sn.rates(pf.queue_stations[i] - 1, r);
214
215 std::vector<int> N(R, 0);
216 for (std::size_t r = 0; r < R; ++r)
217 N[r] = static_cast<int>(std::llround(pf.N[r]));
218
219 // pfqn_stdf takes integer server counts; an infinite one cannot appear
220 // here because every row of D is a Queue node.
221 std::vector<int> S(pf.S.size(), 1);
222 for (std::size_t i = 0; i < pf.S.size(); ++i) {
223 if (!std::isfinite(pf.S[i]))
224 throw UnsupportedError(ErrorKind{"DelayStation"},
225 "solver_nc_cdf_respt: a queueing station has infinitely many servers, which "
226 "the tagged-job passage time cannot represent");
227 S[i] = static_cast<int>(std::llround(pf.S[i]));
228 }
229 const pfqn::StdfResult<T> sd =
230 opt.cdf_algorithm == "exact"
231 ? pfqn::pfqn_stdf(pf.D, N, pf.Z, S, fcfsRows, rates, out.tset)
232 : pfqn::pfqn_stdf_heur(pf.D, N, pf.Z, S, fcfsRows, rates, out.tset);
233
234 out.RD.assign(M, std::vector<Matrix<T>>(R));
235 for (std::size_t i = 0; i < fcfsStations.size() && i < sd.RD.size(); ++i)
236 for (std::size_t r = 0; r < R && r < sd.RD[i].size(); ++r)
237 out.RD[fcfsStations[i] - 1][r] = sd.RD[i][r];
238
239 // A delay station queues for nothing, so its sojourn law is the service
240 // distribution itself.
241 for (std::size_t ist : delayStations)
242 for (std::size_t r = 0; r < R; ++r) {
243 if (sn.disabled[ist - 1][r]) continue;
244 const std::vector<T> F = mam::map_cdf(
245 lang::dist_to_map(sn.service[ist - 1][r]), out.tset);
246 Matrix<T> A(npts, 2, zero);
247 for (std::size_t j = 0; j < npts; ++j) {
248 A(j, 0) = F[j];
249 A(j, 1) = out.tset[j];
250 }
251 out.RD[ist - 1][r] = A;
252 }
253 return out;
254 }
255}
256
257/** Port of `@@SolverNC/getSjrnT.m`: an alias of getCdfRespT. */
258template <class T>
262
263} // namespace nc
264} // namespace line
265
266#endif // LINE_SOLVERS_NC_SOLVER_NC_CDF_H
NumericError(const std::string &what)
Definition error.h:112
UnsupportedError(const std::string &what)
Definition error.h:119
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)
The (D0,D1) pair that reaches sn.proc.
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.
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.
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 <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
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.
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