LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
fj_quorum_moments.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_FJ_QUORUM_MOMENTS_H
6#define LINE_API_FJ_QUORUM_MOMENTS_H
7
8/**
9 * @file
10 * @ingroup api_fj
11 * Mean and variance of a k-of-n (quorum) join completion time, from the mean
12 * and variance of each branch.
13 *
14 * Templated port of matlab/src/api/fj/fj_quorum_moments.m, cross-checked
15 * against jar/src/main/java/jline/api/fj/FJ_quorum.java (identical
16 * three-point fit, merged grid and Poisson-binomial recurrence).
17 *
18 * Each branch is expanded into a three-point discrete step CDF matching its
19 * first two moments. At any time the number of completed branches is
20 * Poisson-binomial, so its distribution is built by the recurrence
21 * q_j <- q_{j-1} F_i + q_j (1 - F_i) over branches, and the k-th order
22 * statistic is the upper tail sum_{j>=k} q_j. At k = n this reduces to the
23 * product of the branch CDFs (the ordinary AND-join) and at k = 1 to
24 * 1 - prod(1 - F_i) (the minimum), two identities the port must reproduce.
25 *
26 * The recurrence is the reason for the whole construction: it computes the
27 * same quantity as the inclusion-exclusion identity used by LQNS, but every
28 * term is a probability in [0,1] and none is subtracted, so it avoids the
29 * catastrophic cancellation the alternating binomial sum incurs as n grows.
30 *
31 * static_assert(num_traits<T>::has_transcendental) -- the three-point fit
32 * takes the standard deviation, so a single sqrt puts the whole function
33 * outside the field. Everything downstream of that sqrt is rational.
34 *
35 * Follows Omari, Franks, Woodside and Pan, as implemented in LQNS 6.x
36 * (randomvar.cc).
37 */
38
39#include <algorithm>
40#include <cstddef>
41#include <vector>
42
44#include "line/num/number.h"
45#include "line/util/error.h"
46#include "line/util/matrix.h"
47
48namespace line {
49namespace fj {
50
51namespace detail {
52
53/** A step CDF as abscissae t with cumulative masses A. */
54template <class T>
55struct StepCdf {
56 std::vector<T> t;
57 std::vector<T> A;
58};
59
60/** Two-moment fit of a branch to a three-point discrete distribution. */
61template <class T>
62StepCdf<T> three_point_fit(const T& mu, const T& var) {
63 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
64 const T two = num_traits<T>::from_int(2);
65 if (mu < zero || var < zero)
66 throw InputError("fj_quorum_moments: branch mean and variance must be non-negative");
67 StepCdf<T> f;
68 if (mu == zero) return f; // no mass: caller replaces it with an instant completion
69 const T sd = num_sqrt(var);
70 if (sd == zero) {
71 f.t.push_back(mu);
72 f.A.push_back(one);
73 return f;
74 }
75 const T t1 = (mu > sd) ? T(mu - sd) : zero;
76 const T t2 = mu;
77 const T t3 = (sd >= mu) ? T(mu + two * var / mu) : T(mu + two * sd);
78 const T delta = t1 * t1 * (t3 - t2) + t2 * t2 * (t1 - t3) + t3 * t3 * (t2 - t1);
79 if (delta == zero) { // abscissae not distinct, the fit is undetermined
80 f.t.push_back(mu);
81 f.A.push_back(one);
82 return f;
83 }
84 const T temp = var + mu * mu;
85 const T a1 = (temp * (t3 - t2) + t2 * t2 * (mu - t3) + t3 * t3 * (t2 - mu)) / delta;
86 const T a3 = (t1 * t1 * (mu - t2) + t2 * t2 * (t1 - mu) + temp * (t2 - t1)) / delta;
87 f.t = {t1, t2, t3};
88 f.A = {a1, T(one - a3), one};
89 return f;
90}
91
92} // namespace detail
93
94/** Branch counts above this are refused: evaluation is cubic in n. */
95constexpr std::size_t FJ_QUORUM_MAX_BRANCHES = 512;
96
97/**
98 * @brief Mean and variance of a k-of-n (quorum) join completion time, from
99 * the mean and variance of each branch.
100 *
101 * @param branchMeans mean completion time of each branch
102 * @param branchVars variance of each branch, same length
103 * @param k quorum size, 1 <= k <= n
104 * @return [m, v], the mean and variance of the k-th smallest
105 */
106template <class T>
107FJQuorumMomentsResult<T> fj_quorum_moments(const std::vector<T>& branchMeans,
108 const std::vector<T>& branchVars, std::size_t k) {
110 "fj_quorum_moments requires transcendental arithmetic");
111 const std::size_t n = branchMeans.size();
112 if (n != branchVars.size())
113 throw InputError("fj_quorum_moments: branchMeans and branchVars must have the same length");
114 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
115 if (n == 0) return {zero, zero};
116 if (k < 1 || k > n) throw InputError("fj_quorum_moments: k must satisfy 1 <= k <= n");
118 throw InputError(
119 "fj_quorum_moments: too many branches; evaluation is cubic in the branch count");
120
121 std::vector<detail::StepCdf<T>> branches(n);
122 for (std::size_t i = 0; i < n; ++i) {
123 branches[i] = detail::three_point_fit(branchMeans[i], branchVars[i]);
124 if (branches[i].t.empty()) { // a massless branch completes instantly
125 branches[i].t.push_back(zero);
126 branches[i].A.push_back(one);
127 }
128 }
129
130 std::vector<T> grid;
131 for (std::size_t i = 0; i < n; ++i) grid.insert(grid.end(), branches[i].t.begin(), branches[i].t.end());
132 std::sort(grid.begin(), grid.end());
133 grid.erase(std::unique(grid.begin(), grid.end()), grid.end());
134 const std::size_t ngrid = grid.size();
135
136 // Tabulate each branch along the merged grid by a single monotone walk.
137 Matrix<T> fv(n, ngrid, zero);
138 for (std::size_t i = 0; i < n; ++i) {
139 std::size_t p = 0;
140 T cur = zero;
141 for (std::size_t g = 0; g < ngrid; ++g) {
142 while (p < branches[i].t.size() && branches[i].t[p] <= grid[g]) {
143 cur = branches[i].A[p];
144 ++p;
145 }
146 fv(i, g) = cur;
147 }
148 }
149
150 std::vector<T> A(ngrid, zero);
151 for (std::size_t g = 0; g < ngrid; ++g) {
152 std::vector<T> q(n + 1, zero);
153 q[0] = one;
154 for (std::size_t i = 0; i < n; ++i) {
155 const T f = fv(i, g);
156 for (std::size_t j = std::min(i + 1, n); j >= 1; --j) q[j] = q[j - 1] * f + q[j] * (one - f);
157 q[0] *= (one - f);
158 }
159 T tail = zero;
160 for (std::size_t j = k; j <= n; ++j) tail += q[j];
161 A[g] = tail;
162 }
163
164 T m = zero, prev = zero;
165 for (std::size_t g = 0; g < ngrid; ++g) {
166 m += (A[g] - prev) * grid[g];
167 prev = A[g];
168 }
169 T v = zero;
170 prev = zero;
171 for (std::size_t g = 0; g < ngrid; ++g) {
172 const T d = grid[g] - m;
173 v += (A[g] - prev) * d * d;
174 prev = A[g];
175 }
176 if (v < zero) v = zero;
177 return {m, v};
178}
179
180} // namespace fj
181} // namespace line
182
183#endif // LINE_API_FJ_QUORUM_MOMENTS_H
InputError(const std::string &what)
Definition error.h:39
The exception types the port throws.
Shared return types and arithmetic helpers for the templated fork-join port.
Dense matrix and non-owning view.
constexpr std::size_t FJ_QUORUM_MAX_BRANCHES
Branch counts above this are refused: evaluation is cubic in n.
FJQuorumMomentsResult< T > fj_quorum_moments(const std::vector< T > &branchMeans, const std::vector< T > &branchVars, std::size_t k)
Mean and variance of a k-of-n (quorum) join completion time, from the mean and variance of each branc...
Number-type abstraction for the templated API port.
[m, v] of fj_quorum_moments: mean and variance of the k-of-n join time.
Definition fj_types.h:107