LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
map_moment_extra.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_MAM_MAP_MOMENT_EXTRA_H
6#define LINE_API_MAM_MAP_MOMENT_EXTRA_H
7
8/**
9 * @file
10 * @ingroup api_mam
11 * Three small MAP quantities the C++ tree had not carried: the factorial and
12 * joint moments, and the MMAP generator.
13 *
14 * Port of `map_factorial_moment`, `map_joint_moment` (from
15 * python/line_solver/api/mam/map_derivatives.py) and `mmap_infgen` (from
16 * mmap_ops.py). PYTHON-ONLY: no MATLAB or JAR twin.
17 *
18 * ALL THREE REST ON ONE IDENTITY. The embedded chain of a MAP moves from one
19 * arrival to the next with kernel `(-D0)^-1 D1`, and the time spent doing so
20 * has, conditional on the phase, a phase-type law with generator D0. So
21 *
22 * E[X^k] = k! pie (-D0)^-k e,
23 * E[X_n^k X_n+1^l] = k! l! pie (-D0)^-k P (-D0)^-l e, P = (-D0)^-1 D1,
24 *
25 * with `pie` the EMBEDDED at-arrivals law and `P` the embedded TRANSITION
26 * KERNEL. The kernel is what separates the two intervals; dropping it entirely
27 * would give the product of two marginals, i.e. the independent case.
28 *
29 * THE REFERENCE USES `D1` WHERE THE KERNEL BELONGS, and that is a defect, not a
30 * convention. `P = (-D0)^-1 D1` carries one more resolvent than `D1` does, so
31 * the reference's joint moment is short by exactly one factor of the mean and
32 * is dimensionally wrong -- a moment of two times must scale as time squared.
33 * A POISSON PROCESS SEES IT IMMEDIATELY: interarrivals are independent there,
34 * so `E[X_n X_n+1]` must be `mean^2`, and at rate 1.5 that is 0.4444 while the
35 * reference returns 0.6667, the mean itself. This port uses the kernel. Native
36 * Python needs the same correction; it is recorded rather than made here,
37 * because it changes a reference.
38 *
39 * `(-D0)^-1` IS APPLIED BY SOLVING, NOT BY INVERTING. The reference forms the
40 * inverse explicitly and falls back to a pseudo-inverse when D0 is singular; a
41 * singular D0 is a MAP whose phase process cannot leave some state, which is a
42 * malformed input rather than something to smooth over, so this port lets the
43 * factorization report it.
44 *
45 * ARITHMETIC: field.
46 */
47
48#include <cstddef>
49#include <vector>
50
52#include "line/num/number.h"
53#include "line/util/error.h"
54#include "line/util/lu.h"
55#include "line/util/matrix.h"
56
57namespace line {
58namespace mam {
59
60namespace momextradetail {
61
62/** Apply `(-D0)^-1` on the RIGHT of a row vector, i.e. solve `x (-D0) = v`. */
63template <class T>
64std::vector<T> right_solve_negD0(const Matrix<T>& D0, const std::vector<T>& v) {
65 const std::size_t n = D0.rows();
66 Matrix<T> A(n, n, num_traits<T>::from_int(0));
67 // x (-D0) = v is (-D0)' x' = v', so the transpose is what gets factored.
68 for (std::size_t i = 0; i < n; ++i)
69 for (std::size_t j = 0; j < n; ++j) A(i, j) = -D0(j, i);
70 return solve(A, v);
71}
72
73} // namespace momextradetail
74
75/** k! pie (-D0)^-k e: the k-th factorial moment of the interarrival time. */
76template <class T>
77T map_factorial_moment(const Map<T>& m, std::size_t k) {
78 const std::size_t n = m.D0.rows();
79 if (n == 0 || m.D0.cols() != n) throw InputError("map_factorial_moment: D0 must be square");
80 std::vector<T> x = map_pie(m);
81 for (std::size_t i = 0; i < k; ++i) x = momextradetail::right_solve_negD0(m.D0, x);
83 for (std::size_t i = 0; i < n; ++i) s += x[i];
84 T fact = num_traits<T>::from_int(1);
85 for (std::size_t i = 2; i <= k; ++i) fact *= num_traits<T>::from_int(static_cast<long>(i));
86 return T(fact * s);
87}
88
89/**
90 * k! l! pie (-D0)^-k P (-D0)^-l e with P = (-D0)^-1 D1: the joint moment of
91 * CONSECUTIVE interarrival times.
92 *
93 * The EMBEDDED KERNEL P sits between the two resolvents, not `D1`; see the
94 * header for the measurement that separates the two.
95 */
96template <class T>
97T map_joint_moment(const Map<T>& m, std::size_t k, std::size_t l) {
98 const std::size_t n = m.D0.rows();
99 if (n == 0 || m.D0.cols() != n) throw InputError("map_joint_moment: D0 must be square");
100 if (m.D1.rows() != n || m.D1.cols() != n)
101 throw InputError("map_joint_moment: D1 must match D0");
102
103 std::vector<T> x = map_pie(m);
104 for (std::size_t i = 0; i < k; ++i) x = momextradetail::right_solve_negD0(m.D0, x);
105 // x P, with P = (-D0)^-1 D1: the resolvent FIRST, then the arrival. The
106 // reference applies D1 alone and is short by one factor of the mean.
107 x = momextradetail::right_solve_negD0(m.D0, x);
108 std::vector<T> y(n, num_traits<T>::from_int(0));
109 for (std::size_t j = 0; j < n; ++j) {
111 for (std::size_t i = 0; i < n; ++i) s += x[i] * m.D1(i, j);
112 y[j] = s;
113 }
114 for (std::size_t i = 0; i < l; ++i) y = momextradetail::right_solve_negD0(m.D0, y);
115
117 for (std::size_t i = 0; i < n; ++i) s += y[i];
118 T fact = num_traits<T>::from_int(1);
119 for (std::size_t i = 2; i <= k; ++i) fact *= num_traits<T>::from_int(static_cast<long>(i));
120 for (std::size_t i = 2; i <= l; ++i) fact *= num_traits<T>::from_int(static_cast<long>(i));
121 return T(fact * s);
122}
123
124/** The generator of an MMAP: D0 plus every marked arrival matrix. */
125template <class T>
126Matrix<T> mmap_infgen(const Matrix<T>& D0, const std::vector<Matrix<T>>& Dk) {
127 const std::size_t n = D0.rows();
128 if (n == 0 || D0.cols() != n) throw InputError("mmap_infgen: D0 must be square");
129 Matrix<T> Q = D0;
130 for (std::size_t c = 0; c < Dk.size(); ++c) {
131 if (Dk[c].rows() != n || Dk[c].cols() != n)
132 throw InputError("mmap_infgen: every arrival matrix must match D0");
133 for (std::size_t i = 0; i < n; ++i)
134 for (std::size_t j = 0; j < n; ++j) Q(i, j) += Dk[c](i, j);
135 }
136 return Q;
137}
138
139/**
140 * The QBD blocks of a MAP/MAP/1 queue: backward, local and forward.
141 *
142 * The level is the queue length and the phase is the PAIR (arrival phase,
143 * service phase), so every block is a Kronecker product with the identity of
144 * the other process:
145 *
146 * F = D1_arr (x) I an arrival raises the level
147 * B = I (x) D1_srv a completion lowers it
148 * L = D0_arr (x) I + I (x) D0_srv both processes move, the level does not
149 *
150 * The ORDER of the factors is the state ordering and cannot be swapped
151 * independently in the three: doing so in one alone transposes the phase index
152 * and the chain silently describes a different queue.
153 */
154template <class T>
155void qbd_blocks_mapmap1(const Matrix<T>& D0a, const Matrix<T>& D1a, const Matrix<T>& D0s,
156 const Matrix<T>& D1s, Matrix<T>* B, Matrix<T>* L, Matrix<T>* F) {
157 const std::size_t na = D0a.rows(), ns = D0s.rows();
158 if (na == 0 || ns == 0 || D0a.cols() != na || D1a.rows() != na || D1a.cols() != na ||
159 D0s.cols() != ns || D1s.rows() != ns || D1s.cols() != ns)
160 throw InputError("qbd_blocks_mapmap1: the two MAPs must be square and self-consistent");
161 const T zero = num_traits<T>::from_int(0);
162 const std::size_t m = na * ns;
163
164 *F = Matrix<T>(m, m, zero);
165 *B = Matrix<T>(m, m, zero);
166 *L = Matrix<T>(m, m, zero);
167 for (std::size_t i = 0; i < na; ++i)
168 for (std::size_t j = 0; j < na; ++j)
169 for (std::size_t k = 0; k < ns; ++k) {
170 (*F)(i * ns + k, j * ns + k) += D1a(i, j);
171 (*L)(i * ns + k, j * ns + k) += D0a(i, j);
172 }
173 for (std::size_t i = 0; i < na; ++i)
174 for (std::size_t k = 0; k < ns; ++k)
175 for (std::size_t l = 0; l < ns; ++l) {
176 (*B)(i * ns + k, i * ns + l) += D1s(k, l);
177 (*L)(i * ns + k, i * ns + l) += D0s(k, l);
178 }
179}
180
181} // namespace mam
182} // namespace line
183
184#endif // LINE_API_MAM_MAP_MOMENT_EXTRA_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 exception types the port throws.
LU factorization with partial pivoting, templated on the number type.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
Dense matrix and non-owning view.
T map_joint_moment(const Map< T > &m, std::size_t k, std::size_t l)
k!
T map_factorial_moment(const Map< T > &m, std::size_t k)
k!
std::vector< T > map_pie(const Map< T > &m)
Phase distribution seen by an arriving job, pie = pi D1 / (pi D1 e).
Definition map_moment.h:89
void qbd_blocks_mapmap1(const Matrix< T > &D0a, const Matrix< T > &D1a, const Matrix< T > &D0s, const Matrix< T > &D1s, Matrix< T > *B, Matrix< T > *L, Matrix< T > *F)
The QBD blocks of a MAP/MAP/1 queue: backward, local and forward.
Matrix< T > mmap_infgen(const Matrix< T > &D0, const std::vector< Matrix< T > > &Dk)
The generator of an MMAP: D0 plus every marked arrival matrix.
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.
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
Matrix< T > D1
Definition map_moment.h:55
Matrix< T > D0
Definition map_moment.h:54