LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_lldsingle.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_LLDSINGLE_H
6#define LINE_API_PFQN_LLDSINGLE_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Exact normalizing constant of a SINGLE-CLASS closed network whose stations
12 * are LIMITED load dependent, i.e. whose rate functions stay constant past a
13 * per-station threshold.
14 *
15 * Templated port of matlab/src/api/pfqn/pfqn_lldsingle.m.
16 *
17 * Same recursion, same arithmetic and bit-identical results to
18 * pfqn_gldsingle, but with the rate-offset axis truncated at that threshold
19 * instead of at the population. Unrolling the recursion of pfqn_gldsingle,
20 *
21 * g(m, n, t) = g(m-1, n, 1) + L(m) g(m, n-1, t+1) / mu(m, t)
22 *
23 * shows that its third index is an offset into station m's rate function,
24 *
25 * g(m,n,t) = sum_{j=0..n} prod_{i=0..j-1} L(m)/mu(m,t+i) * g(m-1,n-j,1)
26 *
27 * so once t >= s_m, where s_m is the population past which mu(m, .) stays
28 * constant, every factor is mu(m, s_m), the product collapses to
29 * (L(m)/mu(m,s_m))^j and
30 *
31 * g(m, n, t) = g(m, n, s_m) for all t >= s_m
32 *
33 * The N - s_m upper slices that pfqn_gldsingle materializes are duplicates of
34 * one another. Capping the offset at s_m and reading g(m, n-1, min(t+1, s_m))
35 * keeps every value the answer reads.
36 *
37 * COST. O(N sum_k s_k) time against O(M N^2) for pfqn_gldsingle, and
38 * O(N max_k s_k) space against O(M N^2), the station levels being rolled. On a
39 * multiserver model, where s_k is the server count, this is LINEAR in the
40 * population rather than quadratic. Unlike pfqn_explicit_ld, which reaches the
41 * same asymptotics through Gordon's alternating partial fraction, this loses no
42 * digits to cancellation at T = double: the arithmetic performed is a SUBSET of
43 * pfqn_gldsingle's, so the two agree to the last bit in every field.
44 *
45 * There is no gain on a station whose rates never settle, an infinite server
46 * mu(m,n) = n being the usual case: it gets s_m = N and costs what it costs in
47 * pfqn_gldsingle. The saving is over the OTHER stations, so a model carrying
48 * one delay among M queues drops from O(M N^2) to O(N^2 + N sum_k s_k).
49 *
50 * Arithmetic: EXACT-CAPABLE, and the threshold scan is what keeps it so. It
51 * compares rates with the field's own operator==, never a tolerance: at
52 * T = Rational or T = Real<D> a tolerance has no meaning, and at T = double a
53 * multiserver row repeats its tail exactly. A MISSED tie only costs time, since
54 * the routine then behaves as pfqn_gldsingle; a FALSE tie would be a wrong
55 * answer, which exact comparison cannot produce. The reference carries an
56 * eps-relative tolerance instead, being confined to IEEE double.
57 *
58 * As in pfqn_gldsingle this port keeps only the linear recursion, the
59 * reference's log-space branch being a range-management device for IEEE double,
60 * and an infinite rate is accepted the same way: the term L/mu vanishes.
61 */
62
63#include <algorithm>
64#include <cstddef>
65#include <vector>
66
68#include "line/num/number.h"
69#include "line/util/error.h"
70#include "line/util/matrix.h"
71
72namespace line {
73namespace pfqn {
74
75/**
76 * @brief Exact normalizing constant of a SINGLE-CLASS closed network whose
77 * stations are LIMITED load dependent, i.e. whose rate functions stay
78 * constant past a per-station threshold.
79 *
80 * @param L (M x 1) service demands, one class
81 * @param N population
82 * @param mu (M x >=N) load-dependent rates, mu(i,k) with k jobs at station i
83 */
84template <class T>
85NcResult<T> pfqn_lldsingle(const Matrix<T>& L, int N, const Matrix<T>& mu) {
86 if (!L.empty() && L.cols() != 1)
87 throw InputError("pfqn_lldsingle: multiclass model detected, this routine is single class");
88 if (N < 0) throw InputError("pfqn_lldsingle: negative population");
89
90 const std::size_t M = L.empty() ? 0 : L.rows();
91 const T zero = num_traits<T>::from_int(0);
92 const T one = num_traits<T>::from_int(1);
93
94 if (N == 0) return {one, 0.0};
95 if (M == 0) return {zero, num_traits<T>::log_as_double(zero)};
96 if (mu.rows() != M) throw InputError("pfqn_lldsingle: mu has the wrong station count");
97 if (static_cast<int>(mu.cols()) < N)
98 throw InputError("pfqn_lldsingle: mu has fewer rate columns than the population");
99
100 const std::size_t Nu = static_cast<std::size_t>(N);
101
102 // pfqn_gldsingle reads every rate column 1..N somewhere in its triangle and
103 // rejects a zero there; the capped sweep reads a subset, so the rejection is
104 // hoisted to keep the two routines refusing the same models.
105 for (std::size_t m = 0; m < M; ++m)
106 for (std::size_t t = 0; t < Nu; ++t)
107 if (mu(m, t) == zero)
108 throw NumericError(
109 "pfqn_lldsingle: a load-dependent rate is zero, the station cannot serve "
110 "and the normalizing constant diverges");
111
112 // s[m]: the smallest offset past which row m of mu is constant, so that
113 // mu(m,t) == mu(m,s[m]) for every t >= s[m]. Stored one-based.
114 std::vector<std::size_t> s(M, Nu);
115 for (std::size_t m = 0; m < M; ++m) {
116 const T& tail = mu(m, Nu - 1);
117 for (std::size_t n = Nu - 1; n >= 1; --n) {
118 if (mu(m, n - 1) == tail)
119 s[m] = n;
120 else
121 break;
122 }
123 }
124
125 // gprev[n] = g(m-1, n, 1); cur[n * sm + (t-1)] = g(m, n, t)
126 std::vector<T> gprev(Nu + 1, zero);
127 gprev[0] = one; // g(0, 0, 1) = 1, and g(0, n, 1) = 0 for n >= 1
128 std::vector<T> cur;
129 for (std::size_t m = 1; m <= M; ++m) {
130 const std::size_t sm = s[m - 1];
131 cur.assign((Nu + 1) * sm, zero);
132 for (std::size_t t = 0; t < sm; ++t) cur[t] = one; // g(m, 0, t) = 1
133 for (std::size_t n = 1; n <= Nu; ++n) {
134 // offsets above N-n+1 are never read back, exactly as in
135 // pfqn_gldsingle, so the triangle is kept
136 const std::size_t tmax = std::min(sm, Nu - n + 1);
137 for (std::size_t t = 1; t <= tmax; ++t) {
138 const std::size_t tsrc = std::min(t + 1, sm);
139 cur[n * sm + (t - 1)] =
140 gprev[n] + L(m - 1, 0) * cur[(n - 1) * sm + (tsrc - 1)] / mu(m - 1, t - 1);
141 }
142 }
143 for (std::size_t n = 0; n <= Nu; ++n) gprev[n] = cur[n * sm];
144 }
145
146 const T G = gprev[Nu];
147 return {G, num_traits<T>::log_as_double(G)};
148}
149
150} // namespace pfqn
151} // namespace line
152
153#endif // LINE_API_PFQN_LLDSINGLE_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
bool empty() const
Definition matrix.h:92
NumericError(const std::string &what)
Definition error.h:45
The exception types the port throws.
Dense matrix and non-owning view.
NcResult< T > pfqn_lldsingle(const Matrix< T > &L, int N, const Matrix< T > &mu)
Exact normalizing constant of a SINGLE-CLASS closed network whose stations are LIMITED load dependent...
Number-type abstraction for the templated API port.
Convolution algorithm for the exact normalizing constant of a closed product-form network (Buzen 1973...
Return value of the normalizing-constant family, mirroring Ret.pfqnNc.
Definition pfqn_ca.h:44