LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
qsys_mdc_crommelin.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_QSYS_QSYS_MDC_CROMMELIN_H
6#define LINE_API_QSYS_QSYS_MDC_CROMMELIN_H
7
8/**
9 * @file
10 * @ingroup api_qsys
11 * M/D/c by Crommelin's embedded chain.
12 *
13 * Templated port of jar/src/main/java/jline/api/qsys/Qsys_mdc_crommelin.java and
14 * the native Python `qsys_mdc_crommelin`; MATLAB carries the method under
15 * `qsys_dmc.m`'s "see also" rather than as its own file.
16 *
17 * The chain is embedded at multiples of the deterministic service time s:
18 * X_{n+1} = max(0, X_n - c) + A_n, A_n ~ Poisson(lambda s),
19 * because in one service period exactly min(X_n, c) jobs complete and the
20 * arrivals in that period are Poisson. The embedded epochs are Poisson arrival
21 * epochs, so PASTA makes the embedded law the time-average law, and the result
22 * is EXACT for M/D/c under FCFS, not an approximation.
23 *
24 * THE TRUNCATION IS THE ONLY ERROR. The default level is
25 * max(200, min(2500, 10/(1-rho) + 200)), which the references settled on
26 * empirically at six digits for moderate c; the cap keeps the dense LU
27 * affordable, since the transition matrix is triangular-banded but not sparse.
28 * A caller comparing against another codebase must pass the SAME truncation to
29 * both, as the defaults are the only free parameter.
30 *
31 * The Poisson weights are formed in logs and exponentiated once, which is what
32 * keeps lambda s in the hundreds from overflowing the factorial.
33 *
34 * ARITHMETIC: transcendental, for the Poisson weights.
35 */
36
37#include <algorithm>
38#include <cmath>
39#include <cstddef>
40#include <vector>
41
43#include "line/num/number.h"
44#include "line/util/error.h"
45#include "line/util/lu.h"
46#include "line/util/matrix.h"
47
48namespace line {
49namespace qsys {
50
51template <class T>
53 T meanQueueLength; ///< E[N], jobs in system
54 T meanWaitingQueue; ///< Lq = E[(N-c)+]
55 T meanWaitingTime; ///< Wq = Lq/lambda
56 T meanSojournTime; ///< W = Wq + s
57 T utilization; ///< rho = lambda s / c
58};
59
60/**
61 * @brief M/D/c by Crommelin's embedded chain.
62 *
63 * @param lambda_arr Poisson arrival rate
64 * @param s deterministic service time
65 * @param c number of servers
66 * @param truncation state-space cap; <= 0 selects the reference's automatic level
67 */
68template <class T>
69MDcCrommelinResult<T> qsys_mdc_crommelin(const T& lambda_arr, const T& s, unsigned c,
70 long truncation = -1) {
72 "qsys_mdc_crommelin forms Poisson weights in logs");
73 const double lam = num_traits<T>::to_double(lambda_arr);
74 const double sv = num_traits<T>::to_double(s);
75 if (!(lam > 0.0)) throw InputError("qsys_mdc_crommelin: the arrival rate must be positive");
76 if (!(sv > 0.0)) throw InputError("qsys_mdc_crommelin: the service time must be positive");
77 if (c < 1) throw InputError("qsys_mdc_crommelin: the number of servers must be at least one");
78
79 const double a = lam * sv;
80 const double rho = a / static_cast<double>(c);
81 if (!(rho < 1.0 - 1e-12))
82 throw InputError("qsys_mdc_crommelin: the load must be strictly below one");
83
84 const long autoN = std::max(
85 200L, std::min(2500L, static_cast<long>(10.0 / (1.0 - rho)) + 200L));
86 const std::size_t nMax = static_cast<std::size_t>(truncation > 0 ? truncation : autoN);
87 const std::size_t n = nMax + 1;
88
89 // Poisson(a) weights in logs, so a in the hundreds does not overflow.
90 std::vector<double> pmf(n);
91 const double logA = std::log(a);
92 double logFact = 0.0;
93 for (std::size_t k = 0; k < n; ++k) {
94 pmf[k] = std::exp(-a + static_cast<double>(k) * logA - logFact);
95 logFact += std::log(static_cast<double>(k + 1));
96 }
97
98 // (P' - I) with the last row replaced by the normalization sum(pi) = 1.
99 // P(i,j) = pmf[j - max(0, i - c)] for j >= max(0, i - c), zero otherwise.
100 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
101 Matrix<T> A(n, n, zero);
102 for (std::size_t i = 0; i < n; ++i) {
103 const std::size_t base = (i <= c) ? 0 : i - c;
104 for (std::size_t j = base; j < n; ++j)
105 A(j, i) = num_traits<T>::from_double(pmf[j - base]);
106 }
107 for (std::size_t i = 0; i < n; ++i) A(i, i) = T(A(i, i) - one);
108 for (std::size_t j = 0; j < n; ++j) A(n - 1, j) = one;
109
110 std::vector<T> rhs(n, zero);
111 rhs[n - 1] = one;
112 const std::vector<T> pi = solve(A, rhs);
113
114 T meanN = zero, Lq = zero;
115 for (std::size_t i = 0; i < n; ++i) {
116 meanN += num_traits<T>::from_int(static_cast<long>(i)) * pi[i];
117 if (i > c) Lq += num_traits<T>::from_int(static_cast<long>(i - c)) * pi[i];
118 }
119
121 r.meanQueueLength = meanN;
122 r.meanWaitingQueue = Lq;
123 r.meanWaitingTime = T(Lq / lambda_arr);
124 r.meanSojournTime = T(r.meanWaitingTime + s);
126 return r;
127}
128
129} // namespace qsys
130} // namespace line
131
132#endif // LINE_API_QSYS_QSYS_MDC_CROMMELIN_H
InputError(const std::string &what)
Definition error.h:39
The exception types the port throws.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
MDcCrommelinResult< T > qsys_mdc_crommelin(const T &lambda_arr, const T &s, unsigned c, long truncation=-1)
M/D/c by Crommelin's embedded chain.
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 return type and arithmetic helpers for the templated qsys port.
T meanQueueLength
E[N], jobs in system.