LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
mmdp.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_MMDP_MMDP_H
6#define LINE_API_MMDP_MMDP_H
7
8/**
9 * @file
10 * @ingroup api_mmdp
11 * Markov-modulated deterministic process (MMDP), for fluid queues.
12 *
13 * Port of python/line_solver/api/mmdp/__init__.py. PYTHON-ONLY: there is no
14 * MATLAB or JAR twin, so native Python is the reference.
15 *
16 * MMDP is the DETERMINISTIC analogue of MMPP. An MMPP modulates a Poisson
17 * ARRIVAL RATE by a background chain; an MMDP modulates a deterministic FLUID
18 * FLOW RATE by one. The parameterization is BUTools': `Q` is the generator of
19 * the modulating chain (rows summing to zero) and `R` is the DIAGONAL matrix of
20 * per-state flow rates.
21 *
22 * WHY THE SCV HERE IS NOT AN INTERARRIVAL SCV. In an MMPP the variability of
23 * interest is that of the interarrival time; in an MMDP the flow is
24 * deterministic within a state, so all the variability lives in WHICH state the
25 * chain occupies. `mmdp_scv` is therefore the SCV of the RATE under the
26 * stationary law, `Var[r]/E[r]^2` with `r` the per-state rate -- not of any
27 * holding time. A two-state process with equal rates has SCV zero however
28 * fast it switches, which is the tell that this is the rate's dispersion and
29 * not a time's.
30 *
31 * ARITHMETIC: field. The stationary solve is the only numeric step and it goes
32 * through `ctmc_solve`, which is templated.
33 */
34
35#include <cmath>
36#include <cstddef>
37#include <limits>
38#include <vector>
39
41#include "line/num/number.h"
42#include "line/util/error.h"
43#include "line/util/matrix.h"
44
45namespace line {
46namespace mmdp {
47
48/**
49 * True when (Q, R) is a valid MMDP.
50 *
51 * Q must be a generator -- square, non-positive diagonal, non-negative
52 * off-diagonal, rows summing to zero -- and R must be DIAGONAL with
53 * non-negative entries. The diagonality is not a storage convention: a
54 * non-diagonal R would make the flow rate depend on a transition rather than
55 * on the state, which is a different process.
56 */
57template <class T>
58bool mmdp_isfeasible(const Matrix<T>& Q, const Matrix<T>& R, double tol = 1e-10) {
59 const std::size_t n = Q.rows();
60 if (n == 0 || Q.cols() != n) return false;
61 if (R.rows() != n || R.cols() != n) return false;
62 for (std::size_t i = 0; i < n; ++i) {
63 if (num_traits<T>::to_double(Q(i, i)) > tol) return false;
64 double rowsum = 0.0;
65 for (std::size_t j = 0; j < n; ++j) {
66 const double q = num_traits<T>::to_double(Q(i, j));
67 if (i != j && q < -tol) return false;
68 rowsum += q;
69 }
70 if (std::fabs(rowsum) > tol) return false;
71 if (num_traits<T>::to_double(R(i, i)) < -tol) return false;
72 for (std::size_t j = 0; j < n; ++j)
73 if (i != j && std::fabs(num_traits<T>::to_double(R(i, j))) > tol) return false;
74 }
75 return true;
76}
77
78/** The per-state rates, i.e. the diagonal of R. */
79template <class T>
80std::vector<T> mmdp_rates(const Matrix<T>& R) {
81 std::vector<T> r(R.rows(), num_traits<T>::from_int(0));
82 for (std::size_t i = 0; i < R.rows(); ++i) r[i] = R(i, i);
83 return r;
84}
85
86/** Stationary mean flow rate, `pi diag(R)`. */
87template <class T>
88T mmdp_mean_rate(const Matrix<T>& Q, const Matrix<T>& R) {
89 const std::size_t n = Q.rows();
90 if (n == 0) throw InputError("mmdp_mean_rate: empty generator");
91 if (R.rows() != n || R.cols() != n)
92 throw InputError("mmdp_mean_rate: R must be the same size as Q");
93 // One state cannot switch, so its rate IS the mean and no solve is needed.
94 if (n == 1) return R(0, 0);
95 const std::vector<T> pi = mc::ctmc_solve(Q);
97 for (std::size_t i = 0; i < n; ++i) m += pi[i] * R(i, i);
98 return m;
99}
100
101/**
102 * SCV of the RATE under the stationary law: `Var[r] / E[r]^2`.
103 *
104 * A one-state process has no variability and returns zero. A mean rate of zero
105 * leaves the ratio undefined, and the reference returns infinity rather than
106 * dividing -- that is a signal the caller can test, where a NaN is not.
107 */
108template <class T>
109T mmdp_scv(const Matrix<T>& Q, const Matrix<T>& R) {
110 const std::size_t n = Q.rows();
111 if (n == 0) throw InputError("mmdp_scv: empty generator");
112 if (R.rows() != n || R.cols() != n)
113 throw InputError("mmdp_scv: R must be the same size as Q");
114 const T zero = num_traits<T>::from_int(0);
115 if (n == 1) return zero;
116
117 const std::vector<T> pi = mc::ctmc_solve(Q);
118 T m = zero, m2 = zero;
119 for (std::size_t i = 0; i < n; ++i) {
120 m += pi[i] * R(i, i);
121 m2 += pi[i] * R(i, i) * R(i, i);
122 }
123 if (!(num_traits<T>::to_double(m) > 0.0))
124 return num_traits<T>::from_double(std::numeric_limits<double>::infinity());
125 return T((m2 - m * m) / (m * m));
126}
127
128/** The (Q, R) pair of an MMDP. */
129template <class T>
130struct MmdpPair {
132};
133
134/**
135 * The MMDP of a MAP: `Q = D0 + D1`, `R = diag(row sums of D1)`.
136 *
137 * The row sum of D1 is the total ARRIVAL rate out of a phase, so the fluid
138 * analogue flows at exactly the rate at which that phase would be generating
139 * arrivals. The MAP's phase process is unchanged, which is why Q is its full
140 * generator.
141 */
142template <class T>
144 const std::size_t n = D0.rows();
145 if (n == 0 || D0.cols() != n || D1.rows() != n || D1.cols() != n)
146 throw InputError("mmdp_from_map: D0 and D1 must be square and the same size");
147 MmdpPair<T> out;
148 out.Q = Matrix<T>(n, n, num_traits<T>::from_int(0));
149 out.R = Matrix<T>(n, n, num_traits<T>::from_int(0));
150 for (std::size_t i = 0; i < n; ++i) {
151 T row = num_traits<T>::from_int(0);
152 for (std::size_t j = 0; j < n; ++j) {
153 out.Q(i, j) = D0(i, j) + D1(i, j);
154 row += D1(i, j);
155 }
156 out.R(i, i) = row;
157 }
158 return out;
159}
160
161/**
162 * The two-state MMDP, in the reference's own parameterization.
163 *
164 * `sigma0` is the rate out of state 0 and `sigma1` the rate out of state 1, so
165 * the generator is `[[-s0, s0], [s1, -s1]]` and the stationary law is
166 * `(s1, s0)/(s0+s1)` -- the chain spends LONGER in the state it leaves more
167 * slowly, which is why the weights look swapped.
168 */
169template <class T>
170MmdpPair<T> mmdp2(const T& r0, const T& r1, const T& sigma0, const T& sigma1) {
171 const T zero = num_traits<T>::from_int(0);
172 if (!(num_traits<T>::to_double(sigma0) > 0.0) || !(num_traits<T>::to_double(sigma1) > 0.0))
173 throw InputError("mmdp2: both switching rates must be positive");
174 MmdpPair<T> out;
175 out.Q = Matrix<T>(2, 2, zero);
176 out.R = Matrix<T>(2, 2, zero);
177 out.Q(0, 0) = -sigma0;
178 out.Q(0, 1) = sigma0;
179 out.Q(1, 0) = sigma1;
180 out.Q(1, 1) = -sigma1;
181 out.R(0, 0) = r0;
182 out.R(1, 1) = r1;
183 return out;
184}
185
186/** The closed-form mean rate of a two-state MMDP. */
187template <class T>
188T mmdp2_mean_rate(const T& r0, const T& r1, const T& sigma0, const T& sigma1) {
189 return T((r0 * sigma1 + r1 * sigma0) / (sigma0 + sigma1));
190}
191
192/** The closed-form SCV of a two-state MMDP. */
193template <class T>
194T mmdp2_scv(const T& r0, const T& r1, const T& sigma0, const T& sigma1) {
195 const T tot = T(sigma0 + sigma1);
196 const T p0 = T(sigma1 / tot), p1 = T(sigma0 / tot);
197 const T m = T(p0 * r0 + p1 * r1);
198 if (!(num_traits<T>::to_double(m) > 0.0))
199 return num_traits<T>::from_double(std::numeric_limits<double>::infinity());
200 const T v = T(p0 * r0 * r0 + p1 * r1 * r1 - m * m);
201 return T(v / (m * m));
202}
203
204} // namespace mmdp
205} // namespace line
206
207#endif // LINE_API_MMDP_MMDP_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
Steady-state distribution of a continuous-time Markov chain.
The exception types the port throws.
Dense matrix and non-owning view.
std::vector< T > ctmc_solve(const Matrix< T > &Qin)
Steady-state distribution of a continuous-time Markov chain.
Definition ctmc_solve.h:122
T mmdp_mean_rate(const Matrix< T > &Q, const Matrix< T > &R)
Stationary mean flow rate, pi diag(R).
Definition mmdp.h:88
bool mmdp_isfeasible(const Matrix< T > &Q, const Matrix< T > &R, double tol=1e-10)
True when (Q, R) is a valid MMDP.
Definition mmdp.h:58
T mmdp2_mean_rate(const T &r0, const T &r1, const T &sigma0, const T &sigma1)
The closed-form mean rate of a two-state MMDP.
Definition mmdp.h:188
std::vector< T > mmdp_rates(const Matrix< T > &R)
The per-state rates, i.e.
Definition mmdp.h:80
MmdpPair< T > mmdp2(const T &r0, const T &r1, const T &sigma0, const T &sigma1)
The two-state MMDP, in the reference's own parameterization.
Definition mmdp.h:170
MmdpPair< T > mmdp_from_map(const Matrix< T > &D0, const Matrix< T > &D1)
The MMDP of a MAP: Q = D0 + D1, R = diag(row sums of D1).
Definition mmdp.h:143
T mmdp2_scv(const T &r0, const T &r1, const T &sigma0, const T &sigma1)
The closed-form SCV of a two-state MMDP.
Definition mmdp.h:194
T mmdp_scv(const Matrix< T > &Q, const Matrix< T > &R)
SCV of the RATE under the stationary law: Var[r] / E[r]^2.
Definition mmdp.h:109
Number-type abstraction for the templated API port.
The (Q, R) pair of an MMDP.
Definition mmdp.h:130
Matrix< T > R
Definition mmdp.h:131
Matrix< T > Q
Definition mmdp.h:131