LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_fnc.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_API_PFQN_PFQN_FNC_H
6#define LINE_API_PFQN_PFQN_FNC_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Load-dependent rates of the functional server f(n) = n + c.
12 *
13 * Templated port of matlab/src/api/pfqn/pfqn_fnc.m (Casale, "On Single-Class
14 * Load-Dependent Normalizing Constant Equations", QEST 2006). Given the
15 * balance-function increments alpha(i,n) of an existing station, the rates
16 * mu(i,n) of the functional server are peeled off by
17 *
18 * mu(i,1) = alpha(i,1)/(1 + c_i)
19 * mu(i,n) = alphanum(n,n-1) alpha(i,1) / (prod_{k<n} mu(i,k)) / (1 - rho)
20 * rho = sum_{k=1}^{n-1} (alphanum(n,k) - alphaden(n,k)) / prod_{j<=k} mu(i,j)
21 *
22 * with alphanum(n,k) = prod_{j=0}^{k-1} alpha(i,n-j) and
23 * alphaden(n,k) = prod_{j=0}^{k-1} alpha(i,n-1-j).
24 *
25 * OFFSET SEARCH. With no c supplied the reference tries c = 0, then
26 * c = -1/2, then walks c upwards in steps of 0.05 until every rate is finite
27 * or c reaches 2. The port keeps that ladder, with c a length-M vector rather
28 * than a scalar, and a zero-column guard: a caller that shifted a single-column
29 * mu gets an M x 0 result rather than an out-of-range index (every closed model
30 * of total population one reaches this through SolverNC 'exact').
31 *
32 * NON-FINITE RATES. MATLAB maps NaN and any |mu| > 1e15 to Inf and then
33 * saturates each row from its first Inf onwards. The port keeps both, since a
34 * downstream load-dependent solver reads Inf as "this station cannot hold that
35 * many jobs" and would misread a large finite rate as a physical one. The
36 * 1e15 threshold is a floating-point guard and is a double constant in every
37 * arithmetic, exactly as in the reference.
38 *
39 * ARITHMETIC. The recursion is a finite sequence of field operations, so it is
40 * EXACT in rational arithmetic and is deliberately left ungated -- but the
41 * automatic offset ladder tests finiteness, which is a floating-point notion;
42 * with an exact type the first ladder step that produces no division by zero
43 * is accepted.
44 */
45
46#include <cmath>
47#include <limits>
48#include <cstddef>
49#include <vector>
50
52#include "line/num/number.h"
53#include "line/util/error.h"
54#include "line/util/matrix.h"
55
56namespace line {
57namespace pfqn {
58
59/** Return value of pfqn_fnc, mirroring [mu, c]. */
60template <class T>
61struct FncResult {
63 std::vector<T> c;
64};
65
66namespace detail {
67
68/**
69 * MATLAB's retry predicate `any(~isfinite(mu(:)))`: the offset ladder fires
70 * as soon as ANY rate of ANY station is non-finite. (An earlier reading of the
71 * reference, `~all(isfinite(mu))`, reduced per column for M > 1 and fired only
72 * when every population column held a non-finite rate; MATLAB has since been
73 * corrected to the any-entry test and this port follows it. On the example
74 * alpha = [1 2 3; 2 2 2], c = 0 gives mu = [1 Inf Inf; 2 2 2], which is now
75 * rejected and retried with c = -1/2.)
76 */
77template <class T>
78bool matlab_retry_needed(const Matrix<T>& m) {
79 for (std::size_t j = 0; j < m.cols(); ++j)
80 for (std::size_t i = 0; i < m.rows(); ++i)
81 if (is_inf_marker(m(i, j))) return true;
82 return false;
83}
84
85} // namespace detail
86
87/** Rates for a given offset vector c (the two-argument MATLAB branch). */
88template <class T>
89Matrix<T> pfqn_fnc_at(const Matrix<T>& alpha, const std::vector<T>& c) {
90 const std::size_t M = alpha.rows(), N = alpha.cols();
91 if (c.size() != M) throw InputError("pfqn_fnc: the offset vector must have one entry per station");
92 if (N == 0) return Matrix<T>(M, 0);
93 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
94 const T inf = detail::num_inf_marker<T>();
95
96 Matrix<T> mu(M, N, zero);
97 for (std::size_t i = 0; i < M; ++i) {
98 const T d0 = T(one + c[i]);
99 if (d0 == zero) throw NumericError(ErrorKind{"Degenerate"}, "pfqn_fnc: offset -1 makes the first rate undefined");
100 mu(i, 0) = T(alpha(i, 0) / d0);
101 // alphanum(n,k) = prod_{j=0}^{k-1} alpha(i, n-j), 1-based n and k.
102 Matrix<T> anum(N + 1, N + 1, zero), aden(N + 1, N + 1, zero);
103 for (std::size_t n = 2; n <= N; ++n) {
104 anum(n, 1) = alpha(i, n - 1);
105 aden(n, 1) = alpha(i, n - 2);
106 for (std::size_t k = 2; k + 1 <= n; ++k) {
107 anum(n, k) = T(anum(n, k - 1) * alpha(i, n - k));
108 aden(n, k) = T(aden(n, k - 1) * alpha(i, n - k - 1));
109 }
110 }
111 for (std::size_t n = 2; n <= N; ++n) {
112 T rho = zero, muden = one;
113 bool bad = false;
114 for (std::size_t k = 1; k + 1 <= n; ++k) {
115 muden *= mu(i, k - 1);
116 if (muden == zero) {
117 bad = true;
118 break;
119 }
120 rho += T(T(anum(n, k) - aden(n, k)) / muden);
121 }
122 if (bad || T(one - rho) == zero) {
123 mu(i, n - 1) = inf;
124 continue;
125 }
126 T v = T(anum(n, n - 1) * alpha(i, 0) / muden);
127 v = T(v / T(one - rho));
128 mu(i, n - 1) = v;
129 }
130 }
131 // MATLAB: mu(isnan) = Inf; mu(abs(mu) > 1e15) = Inf; then saturate each
132 // row from its first Inf onwards.
133 const double big = 1e15;
134 for (std::size_t i = 0; i < M; ++i) {
135 for (std::size_t n = 0; n < N; ++n) {
136 const double v = num_traits<T>::to_double(mu(i, n));
137 if (std::isnan(v) || std::fabs(v) > big) mu(i, n) = inf; // the reference's Inf
138 }
139 for (std::size_t n = 0; n < N; ++n) {
140 if (detail::is_inf_marker(mu(i, n))) {
141 for (std::size_t k = n; k < N; ++k) mu(i, k) = inf;
142 break;
143 }
144 }
145 }
146 return mu;
147}
148
149/** Automatic offset search (the one-argument MATLAB branch). */
150template <class T>
152 const std::size_t M = alpha.rows();
153 FncResult<T> r;
154 if (alpha.cols() == 0) {
155 // A caller that shifted a single-column mu has no rate to build.
156 r.mu = Matrix<T>(M, 0);
157 r.c.assign(M, num_traits<T>::from_int(0));
158 return r;
159 }
160 r.c.assign(M, num_traits<T>::from_int(0));
161 r.mu = pfqn_fnc_at(alpha, r.c);
162 if (!detail::matlab_retry_needed(r.mu)) return r;
163 r.c.assign(M, num_traits<T>::from_rational(-1, 2));
164 r.mu = pfqn_fnc_at(alpha, r.c);
165 if (!detail::matlab_retry_needed(r.mu)) return r;
166 for (int step = 1; step <= 50; ++step) {
167 const T v = T(num_traits<T>::from_rational(-1, 2) +
168 num_traits<T>::from_rational(step, 20)); // dt in steps of 0.05
169 r.c.assign(M, v);
170 r.mu = pfqn_fnc_at(alpha, r.c);
171 if (!detail::matlab_retry_needed(r.mu)) return r;
172 if (num_traits<T>::to_double(v) >= 2.0) break;
173 }
174 return r;
175}
176
177template <class T>
178FncResult<T> pfqn_fnc(const Matrix<T>& alpha, const std::vector<T>& c) {
179 FncResult<T> r;
180 r.c = c;
181 r.mu = pfqn_fnc_at(alpha, c);
182 return r;
183}
184
185} // namespace pfqn
186} // namespace line
187
188#endif // LINE_API_PFQN_PFQN_FNC_H
InputError(const std::string &what)
Definition error.h:105
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
NumericError(const std::string &what)
Definition error.h:112
The exception types the port throws.
Dense matrix and non-owning view.
Matrix< T > pfqn_fnc_at(const Matrix< T > &alpha, const std::vector< T > &c)
Rates for a given offset vector c (the two-argument MATLAB branch).
Definition pfqn_fnc.h:89
FncResult< T > pfqn_fnc(const Matrix< T > &alpha)
Automatic offset search (the one-argument MATLAB branch).
Definition pfqn_fnc.h:151
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
Number-type abstraction for the templated API port.
Shared scalar machinery for the integration / asymptotic members of the pfqn family (pfqn_le,...
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
Return value of pfqn_fnc, mirroring [mu, c].
Definition pfqn_fnc.h:61
std::vector< T > c
Definition pfqn_fnc.h:63