LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_propfair.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_PROPFAIR_H
6#define LINE_API_PFQN_PFQN_PROPFAIR_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Proportionally fair allocation estimate of the normalizing constant
12 * (Schweitzer 1979; Walton, "Proportional fairness and its relationship with
13 * multi-class queueing networks", 2009).
14 *
15 * Templated port of matlab/src/api/pfqn/pfqn_propfair.m. The asymptotic
16 * throughput vector solves the convex program
17 *
18 * maximize sum_r (N_r - X_r Z_r) log(X_r + 1e-6)
19 * subject to L X <= 1, X >= 0
20 *
21 * after which log G = sum_r (N_r - X_r Z_r) log(1/X_r) - sum_r factln(X_r Z_r).
22 * The estimate is asymptotically exact for networks of single-server PS
23 * queues; delay stations are handled by the heuristic above.
24 *
25 * OPTIMIZER. MATLAB calls fmincon. There is no fmincon here, and a generic
26 * nonlinear programming solver is not something to invent, so the port solves
27 * the same program with a primal log-barrier Newton method: the objective is
28 * separable and strictly concave (d^2/dX_r^2 = -2 Z_r/(X_r+eps) -
29 * (N_r - X_r Z_r)/(X_r+eps)^2 < 0 wherever the objective is defined), the
30 * feasible set is a polytope, so the barrier path is well defined and the
31 * method converges to the same maximizer fmincon reports. The centering
32 * parameter is raised geometrically until the duality gap (M+R)/t falls below
33 * the tolerance. What differs from MATLAB is only the path taken, not the
34 * point reached; the accompanying test asserts agreement with the MATLAB
35 * value.
36 *
37 * The reference starts fmincon at the origin, which is on the boundary of the
38 * feasible set and outside the domain of the barrier. The port starts at the
39 * strictly interior point X_r = 1/(2 max_m sum_s L_ms) instead, which is the
40 * only deviation the barrier formulation forces.
41 *
42 * MATLAB DEAD CODE, noted rather than reproduced: pfqn_propfair.m accumulates
43 * a first value of lG in a loop over the classes with Z_r > 0 and then
44 * OVERWRITES it on the next line. The loop has no effect on the returned
45 * value, so it is not ported.
46 *
47 * ARITHMETIC. Logarithms throughout, in the objective and in the barrier, so
48 * gated on num_traits<T>::has_transcendental.
49 */
50
51#include <cmath>
52#include <cstddef>
53#include <vector>
54
56#include "line/num/number.h"
57#include "line/util/error.h"
58#include "line/util/lu.h"
59#include "line/util/matrix.h"
60
61namespace line {
62namespace pfqn {
63
64/** Return value of pfqn_propfair, mirroring [G, lG, Xasy]. */
65template <class T>
67 T G;
68 T lG;
69 std::vector<T> Xasy;
70};
71
72/**
73 * @brief Proportionally fair allocation estimate of the normalizing constant
74 * (Schweitzer 1979; Walton, "Proportional fairness and its
75 * relationship with multi-class queueing networks", 2009).
76 *
77 * @param L (M x R) demands, @param N (R) population, @param Z (R) think times
78 */
79template <class T>
80PropfairResult<T> pfqn_propfair(const Matrix<T>& L, const std::vector<T>& N,
81 const std::vector<T>& Z) {
83 "pfqn_propfair requires transcendental arithmetic (logarithmic objective)");
84 using std::log;
85 const std::size_t M = L.rows(), R = L.cols();
86 if (N.size() != R) throw InputError("pfqn_propfair: L and N disagree on the class count");
87 if (!Z.empty() && Z.size() != R) throw InputError("pfqn_propfair: Z has the wrong length");
88 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
89 const std::vector<T> Zv = Z.empty() ? std::vector<T>(R, zero) : Z;
90 const T eps = num_traits<T>::from_double(1e-6); // MATLAB's log(X + 1e-6)
91
92 // Strictly interior start.
93 T rowmax = zero;
94 for (std::size_t m = 0; m < M; ++m) {
95 T s = zero;
96 for (std::size_t r = 0; r < R; ++r) s += L(m, r);
97 if (s > rowmax) rowmax = s;
98 }
99 if (rowmax <= zero) throw InputError("pfqn_propfair: every demand is zero");
100 std::vector<T> X(R, T(one / T(num_traits<T>::from_int(2) * rowmax)));
101
102 const auto objective = [&](const std::vector<T>& x) {
103 T f = zero;
104 for (std::size_t r = 0; r < R; ++r) f += T(N[r] - x[r] * Zv[r]) * log(T(x[r] + eps));
105 return f;
106 };
107 const auto feasible = [&](const std::vector<T>& x) {
108 for (std::size_t r = 0; r < R; ++r)
109 if (!(x[r] > zero)) return false;
110 for (std::size_t m = 0; m < M; ++m) {
111 T s = zero;
112 for (std::size_t r = 0; r < R; ++r) s += L(m, r) * x[r];
113 if (!(s < one)) return false;
114 }
115 return true;
116 };
117
118 const T gaptol = num_traits<T>::from_double(1e-10);
120 for (int outer = 0; outer < 60; ++outer) {
121 // Newton on the barrier subproblem: maximize t f(X) + barrier(X).
122 for (int inner = 0; inner < 100; ++inner) {
123 std::vector<T> slack(M);
124 for (std::size_t m = 0; m < M; ++m) {
125 T s = zero;
126 for (std::size_t r = 0; r < R; ++r) s += L(m, r) * X[r];
127 slack[m] = T(one - s);
128 }
129 std::vector<T> grad(R, zero);
130 Matrix<T> H(R, R, zero);
131 for (std::size_t r = 0; r < R; ++r) {
132 const T d = T(X[r] + eps);
133 const T num = T(N[r] - X[r] * Zv[r]);
134 grad[r] = T(t * T(T(-Zv[r] * log(d)) + T(num / d)));
135 H(r, r) = T(t * T(T(-num_traits<T>::from_int(2) * Zv[r] / d) - T(num / T(d * d))));
136 // Barrier for X_r > 0.
137 grad[r] += T(one / X[r]);
138 H(r, r) -= T(one / T(X[r] * X[r]));
139 }
140 for (std::size_t m = 0; m < M; ++m) {
141 for (std::size_t r = 0; r < R; ++r) {
142 grad[r] -= T(L(m, r) / slack[m]);
143 for (std::size_t s = 0; s < R; ++s)
144 H(r, s) -= T(L(m, r) * L(m, s) / T(slack[m] * slack[m]));
145 }
146 }
147 // Newton step solves H dx = -grad; H is negative definite here.
148 std::vector<T> rhs(R);
149 for (std::size_t r = 0; r < R; ++r) rhs[r] = T(-grad[r]);
150 std::vector<T> dx;
151 try {
152 dx = solve(H, rhs);
153 } catch (const NumericError&) {
154 break; // singular Hessian: accept the current iterate
155 }
156 T dec = zero; // Newton decrement, -grad' dx
157 for (std::size_t r = 0; r < R; ++r) dec -= grad[r] * dx[r];
158 if (num_traits<T>::to_double(num_abs(dec)) < 1e-14) break;
159
160 T step = one;
161 std::vector<T> Xn(R);
162 bool ok = false;
163 const T f0 = T(t * objective(X));
164 for (int b = 0; b < 80; ++b) {
165 for (std::size_t r = 0; r < R; ++r) Xn[r] = T(X[r] + step * dx[r]);
166 if (feasible(Xn) && T(t * objective(Xn)) >= f0) {
167 ok = true;
168 break;
169 }
170 step = T(step / num_traits<T>::from_int(2));
171 }
172 if (!ok) break;
173 X = Xn;
174 }
175 const T gap = T(num_traits<T>::from_int(static_cast<long>(M + R)) / t);
176 if (gap < gaptol) break;
177 t = T(t * num_traits<T>::from_int(10));
178 }
179
181 res.Xasy = X;
182 T lG = zero;
183 for (std::size_t r = 0; r < R; ++r) {
184 if (X[r] <= zero) throw NumericError("pfqn_propfair: non-positive asymptotic throughput");
185 lG += T(N[r] - X[r] * Zv[r]) * log(T(one / X[r]));
186 lG -= detail::num_factln<T>(T(X[r] * Zv[r]));
187 }
188 using std::exp;
189 res.lG = lG;
190 res.G = exp(lG);
191 return res;
192}
193
194} // namespace pfqn
195} // namespace line
196
197#endif // LINE_API_PFQN_PFQN_PROPFAIR_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
The algorithm cannot proceed on this instance (singular matrix, ...).
Definition error.h:43
NumericError(const std::string &what)
Definition error.h:45
The exception types the port throws.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
PropfairResult< T > pfqn_propfair(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z)
Proportionally fair allocation estimate of the normalizing constant (Schweitzer 1979; Walton,...
T num_abs(const T &v)
Definition number.h:172
std::vector< T > solve(const Matrix< T > &A, const std::vector< T > &b)
Convenience: solve Ax = b, leaving A and b untouched.
Definition lu.h:158
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_propfair, mirroring [G, lG, Xasy].