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, including its two documented
28 * repairs: the zero-column guard (a caller that shifted a single-column mu
29 * gets an M x 0 result rather than an out-of-range index, which used to break
30 * SolverNC 'exact' on every closed model of total population one), and the
31 * fact that c is a length-M vector rather than a scalar.
32 *
33 * NON-FINITE RATES. MATLAB maps NaN and any |mu| > 1e15 to Inf and then
34 * saturates each row from its first Inf onwards. The port keeps both, since a
35 * downstream load-dependent solver reads Inf as "this station cannot hold that
36 * many jobs" and would misread a large finite rate as a physical one. The
37 * 1e15 threshold is a floating-point guard and is a double constant in every
38 * arithmetic, exactly as in the reference.
39 *
40 * ARITHMETIC. The recursion is a finite sequence of field operations, so it is
41 * EXACT in rational arithmetic and is deliberately left ungated -- but the
42 * automatic offset ladder tests finiteness, which is a floating-point notion;
43 * with an exact type the first ladder step that produces no division by zero
44 * is accepted.
45 */
46
47#include <cmath>
48#include <limits>
49#include <cstddef>
50#include <vector>
51
53#include "line/num/number.h"
54#include "line/util/error.h"
55#include "line/util/matrix.h"
56
57namespace line {
58namespace pfqn {
59
60/** Return value of pfqn_fnc, mirroring [mu, c]. */
61template <class T>
62struct FncResult {
64 std::vector<T> c;
65};
66
67namespace detail {
68
69/**
70 * MATLAB's retry predicate `~all(isfinite(mu))` used as an `if` condition,
71 * reproduced verbatim including its vectorization.
72 *
73 * REFERENCE DEFECT. `all` reduces along the first non-singleton dimension, so
74 * for a multi-station mu (M > 1) it returns one flag PER COLUMN, `~` negates
75 * each, and `if` on a vector is true only when EVERY element is true. The
76 * retry therefore fires only when every population column contains a
77 * non-finite rate, not when any single rate is non-finite. On the two-station
78 * example alpha = [1 2 3; 2 2 2] MATLAB accepts c = 0 and returns
79 * mu = [1 Inf Inf; 2 2 2], which a plain "any entry non-finite" reading would
80 * have rejected and retried. The port reproduces MATLAB, because a caller
81 * comparing against it must get the same offset vector.
82 */
83template <class T>
84bool matlab_retry_needed(const Matrix<T>& m) {
85 if (m.cols() == 0) return false;
86 if (m.rows() == 1) { // row vector: all() reduces to a scalar
87 for (std::size_t j = 0; j < m.cols(); ++j)
88 if (is_inf_marker(m(0, j))) return true;
89 return false;
90 }
91 for (std::size_t j = 0; j < m.cols(); ++j) {
92 bool colHasNonFinite = false;
93 for (std::size_t i = 0; i < m.rows(); ++i)
94 if (is_inf_marker(m(i, j))) colHasNonFinite = true;
95 if (!colHasNonFinite) return false;
96 }
97 return true;
98}
99
100} // namespace detail
101
102/** Rates for a given offset vector c (the two-argument MATLAB branch). */
103template <class T>
104Matrix<T> pfqn_fnc_at(const Matrix<T>& alpha, const std::vector<T>& c) {
105 const std::size_t M = alpha.rows(), N = alpha.cols();
106 if (c.size() != M) throw InputError("pfqn_fnc: the offset vector must have one entry per station");
107 if (N == 0) return Matrix<T>(M, 0);
108 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
109 const T inf = detail::num_inf_marker<T>();
110
111 Matrix<T> mu(M, N, zero);
112 for (std::size_t i = 0; i < M; ++i) {
113 const T d0 = T(one + c[i]);
114 if (d0 == zero) throw NumericError("pfqn_fnc: offset -1 makes the first rate undefined");
115 mu(i, 0) = T(alpha(i, 0) / d0);
116 // alphanum(n,k) = prod_{j=0}^{k-1} alpha(i, n-j), 1-based n and k.
117 Matrix<T> anum(N + 1, N + 1, zero), aden(N + 1, N + 1, zero);
118 for (std::size_t n = 2; n <= N; ++n) {
119 anum(n, 1) = alpha(i, n - 1);
120 aden(n, 1) = alpha(i, n - 2);
121 for (std::size_t k = 2; k + 1 <= n; ++k) {
122 anum(n, k) = T(anum(n, k - 1) * alpha(i, n - k));
123 aden(n, k) = T(aden(n, k - 1) * alpha(i, n - k - 1));
124 }
125 }
126 for (std::size_t n = 2; n <= N; ++n) {
127 T rho = zero, muden = one;
128 bool bad = false;
129 for (std::size_t k = 1; k + 1 <= n; ++k) {
130 muden *= mu(i, k - 1);
131 if (muden == zero) {
132 bad = true;
133 break;
134 }
135 rho += T(T(anum(n, k) - aden(n, k)) / muden);
136 }
137 if (bad || T(one - rho) == zero) {
138 mu(i, n - 1) = inf;
139 continue;
140 }
141 T v = T(anum(n, n - 1) * alpha(i, 0) / muden);
142 v = T(v / T(one - rho));
143 mu(i, n - 1) = v;
144 }
145 }
146 // MATLAB: mu(isnan) = Inf; mu(abs(mu) > 1e15) = Inf; then saturate each
147 // row from its first Inf onwards.
148 const double big = 1e15;
149 for (std::size_t i = 0; i < M; ++i) {
150 for (std::size_t n = 0; n < N; ++n) {
151 const double v = num_traits<T>::to_double(mu(i, n));
152 if (std::isnan(v) || std::fabs(v) > big) mu(i, n) = inf; // the reference's Inf
153 }
154 for (std::size_t n = 0; n < N; ++n) {
155 if (detail::is_inf_marker(mu(i, n))) {
156 for (std::size_t k = n; k < N; ++k) mu(i, k) = inf;
157 break;
158 }
159 }
160 }
161 return mu;
162}
163
164/** Automatic offset search (the one-argument MATLAB branch). */
165template <class T>
167 const std::size_t M = alpha.rows();
168 FncResult<T> r;
169 if (alpha.cols() == 0) {
170 // A caller that shifted a single-column mu has no rate to build.
171 r.mu = Matrix<T>(M, 0);
172 r.c.assign(M, num_traits<T>::from_int(0));
173 return r;
174 }
175 r.c.assign(M, num_traits<T>::from_int(0));
176 r.mu = pfqn_fnc_at(alpha, r.c);
177 if (!detail::matlab_retry_needed(r.mu)) return r;
178 r.c.assign(M, num_traits<T>::from_rational(-1, 2));
179 r.mu = pfqn_fnc_at(alpha, r.c);
180 if (!detail::matlab_retry_needed(r.mu)) return r;
181 for (int step = 1; step <= 50; ++step) {
182 const T v = T(num_traits<T>::from_rational(-1, 2) +
183 num_traits<T>::from_rational(step, 20)); // dt in steps of 0.05
184 r.c.assign(M, v);
185 r.mu = pfqn_fnc_at(alpha, r.c);
186 if (!detail::matlab_retry_needed(r.mu)) return r;
187 if (num_traits<T>::to_double(v) >= 2.0) break;
188 }
189 return r;
190}
191
192template <class T>
193FncResult<T> pfqn_fnc(const Matrix<T>& alpha, const std::vector<T>& c) {
194 FncResult<T> r;
195 r.c = c;
196 r.mu = pfqn_fnc_at(alpha, c);
197 return r;
198}
199
200} // namespace pfqn
201} // namespace line
202
203#endif // LINE_API_PFQN_PFQN_FNC_H
InputError(const std::string &what)
Definition error.h:39
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:45
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:104
FncResult< T > pfqn_fnc(const Matrix< T > &alpha)
Automatic offset search (the one-argument MATLAB branch).
Definition pfqn_fnc.h:166
Number-type abstraction for the templated API port.
Shared scalar machinery for the integration / asymptotic members of the pfqn family (pfqn_le,...
Return value of pfqn_fnc, mirroring [mu, c].
Definition pfqn_fnc.h:62
std::vector< T > c
Definition pfqn_fnc.h:64