LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
aph_simplify.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_APH_SIMPLIFY_H
6#define LINE_API_MAM_APH_SIMPLIFY_H
7
8/**
9 * @file
10 * @ingroup api_mam
11 * Composition of two matrix-exponential distributions given in (alpha, T) form.
12 *
13 * Port of `matlab/lib/kpctoolbox/aph/aph_simplify.m`. The three patterns are
14 * the three ways two activities can be composed in an LQN activity graph, which
15 * is the caller this exists for (`updateMetricsMomentBased`):
16 *
17 * Sequence X1 + X2 the convolution
18 * Parallel max(X1, X2) both run, the composite ends with the last
19 * Branch X1 w.p. p1, X2 w.p. p2 the mixture
20 *
21 * WHY (alpha, T) AND NOT A `Map`. A MAP pair (D0, D1) fixes the RESTART law as
22 * well as the absorption law, and every one of these patterns is a statement
23 * about a single passage: what happens after absorption is the caller's, not
24 * the composition's. `aph_fit.h` converts in the other direction
25 * (`aph_canonical`) precisely once, when a fitted APH has to become a process.
26 *
27 * THE ALPHA VECTORS ARE SUB-STOCHASTIC ON PURPOSE. `1 - alpha*e` is the mass
28 * that skips the phase process entirely, i.e. an atom at zero, and each pattern
29 * routes it explicitly -- in the sequence, the mass of X1 that starts already
30 * absorbed enters X2's initial vector directly. Renormalizing alpha would drop
31 * that atom and shorten the composite.
32 */
33
34#include <cstddef>
35#include <vector>
36
37#include "line/num/number.h"
38#include "line/util/error.h"
39#include "line/util/matrix.h"
40
41namespace line {
42namespace mam {
43
44/** A matrix-exponential law in (alpha, S) form: initial vector and subgenerator. */
45template <class T>
46struct AphPair {
47 std::vector<T> alpha; ///< (n) initial vector, possibly sub-stochastic
48 Matrix<T> S; ///< (n x n) subgenerator
49
50 std::size_t order() const { return alpha.size(); }
51};
52
53/** The `pattern` argument of aph_simplify.m, by name. */
54enum class AphPattern { Sequence = 1, Parallel = 2, Branch = 3, Loop = 4 };
55
56namespace detail {
57
58/** Exit-rate column of a subgenerator, -S*e, one entry per phase. */
59template <class T>
60std::vector<T> aph_exit_rates(const Matrix<T>& S) {
61 const T zero = num_traits<T>::from_int(0);
62 std::vector<T> out(S.rows(), zero);
63 for (std::size_t i = 0; i < S.rows(); ++i) {
64 T s = zero;
65 for (std::size_t j = 0; j < S.cols(); ++j) s -= S(i, j);
66 out[i] = s;
67 }
68 return out;
69}
70
71/** The atom at zero, 1 - alpha*e. */
72template <class T>
73T aph_zero_atom(const std::vector<T>& alpha) {
74 T s = num_traits<T>::from_int(1);
75 for (const T& a : alpha) s -= a;
76 return s;
77}
78
79} // namespace detail
80
81/**
82 * Compose two matrix-exponential laws, as aph_simplify.m does.
83 *
84 * `p1` and `p2` are read only by `Branch`, where they are the branch
85 * probabilities; the other patterns take them as the reference does, i.e. they
86 * are present in the signature and unused.
87 */
88template <class T>
89AphPair<T> aph_simplify(const AphPair<T>& d1, const AphPair<T>& d2, const T& p1, const T& p2,
90 AphPattern pattern) {
91 const T zero = num_traits<T>::from_int(0);
92 const std::size_t n1 = d1.order(), n2 = d2.order();
93 if (d1.S.rows() != n1 || d1.S.cols() != n1 || d2.S.rows() != n2 || d2.S.cols() != n2)
94 throw InputError("aph_simplify: an (alpha, S) pair has mismatched dimensions");
95
96 AphPair<T> out;
97 switch (pattern) {
99 // alpha = [a1, (1 - a1*e) a2]; the second block is entered directly
100 // by the mass of X1 that was already absorbed at time zero.
101 const T atom1 = detail::aph_zero_atom(d1.alpha);
102 const std::vector<T> exit1 = detail::aph_exit_rates(d1.S);
103 out.alpha.assign(n1 + n2, zero);
104 for (std::size_t i = 0; i < n1; ++i) out.alpha[i] = d1.alpha[i];
105 for (std::size_t j = 0; j < n2; ++j) out.alpha[n1 + j] = T(atom1 * d2.alpha[j]);
106 out.S = Matrix<T>(n1 + n2, n1 + n2, zero);
107 for (std::size_t i = 0; i < n1; ++i) {
108 for (std::size_t j = 0; j < n1; ++j) out.S(i, j) = d1.S(i, j);
109 for (std::size_t j = 0; j < n2; ++j)
110 out.S(i, n1 + j) = T(exit1[i] * d2.alpha[j]);
111 }
112 for (std::size_t i = 0; i < n2; ++i)
113 for (std::size_t j = 0; j < n2; ++j) out.S(n1 + i, n1 + j) = d2.S(i, j);
114 return out;
115 }
117 // States: the n1*n2 product block (both still running), then the n1
118 // block (X2 already done) and the n2 block (X1 already done).
119 const T atom1 = detail::aph_zero_atom(d1.alpha);
120 const T atom2 = detail::aph_zero_atom(d2.alpha);
121 const std::vector<T> exit1 = detail::aph_exit_rates(d1.S);
122 const std::vector<T> exit2 = detail::aph_exit_rates(d2.S);
123 const std::size_t np = n1 * n2, n = np + n1 + n2;
124 out.alpha.assign(n, zero);
125 for (std::size_t i = 0; i < n1; ++i)
126 for (std::size_t j = 0; j < n2; ++j) out.alpha[i * n2 + j] = T(d1.alpha[i] * d2.alpha[j]);
127 for (std::size_t i = 0; i < n1; ++i) out.alpha[np + i] = T(atom2 * d1.alpha[i]);
128 for (std::size_t j = 0; j < n2; ++j) out.alpha[np + n1 + j] = T(atom1 * d2.alpha[j]);
129 out.S = Matrix<T>(n, n, zero);
130 // kron(T1, I2) + kron(I1, T2) on the product block
131 for (std::size_t i = 0; i < n1; ++i)
132 for (std::size_t j = 0; j < n2; ++j) {
133 const std::size_t r = i * n2 + j;
134 for (std::size_t k = 0; k < n1; ++k) out.S(r, k * n2 + j) += d1.S(i, k);
135 for (std::size_t k = 0; k < n2; ++k) out.S(r, i * n2 + k) += d2.S(j, k);
136 // kron(I1, -T2*e): X2 absorbs, X1 continues alone
137 out.S(r, np + i) = exit2[j];
138 // kron(-T1*e, I2): X1 absorbs, X2 continues alone
139 out.S(r, np + n1 + j) = exit1[i];
140 }
141 for (std::size_t i = 0; i < n1; ++i)
142 for (std::size_t j = 0; j < n1; ++j) out.S(np + i, np + j) = d1.S(i, j);
143 for (std::size_t i = 0; i < n2; ++i)
144 for (std::size_t j = 0; j < n2; ++j)
145 out.S(np + n1 + i, np + n1 + j) = d2.S(i, j);
146 return out;
147 }
148 case AphPattern::Branch: {
149 out.alpha.assign(n1 + n2, zero);
150 for (std::size_t i = 0; i < n1; ++i) out.alpha[i] = T(p1 * d1.alpha[i]);
151 for (std::size_t j = 0; j < n2; ++j) out.alpha[n1 + j] = T(p2 * d2.alpha[j]);
152 out.S = Matrix<T>(n1 + n2, n1 + n2, zero);
153 for (std::size_t i = 0; i < n1; ++i)
154 for (std::size_t j = 0; j < n1; ++j) out.S(i, j) = d1.S(i, j);
155 for (std::size_t i = 0; i < n2; ++i)
156 for (std::size_t j = 0; j < n2; ++j) out.S(n1 + i, n1 + j) = d2.S(i, j);
157 return out;
158 }
159 default:
160 break;
161 }
162 // The reference's fourth arm is commented out in aph_simplify.m and returns
163 // nothing at all, so a caller asking for it in MATLAB gets an undefined
164 // output rather than a loop composition. Refusing is the same statement.
165 throw UnsupportedError(
166 "aph_simplify: the loop pattern is not implemented in the reference either "
167 "(aph_simplify.m leaves its fourth arm commented out)");
168}
169
170} // namespace mam
171} // namespace line
172
173#endif // LINE_API_MAM_APH_SIMPLIFY_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
UnsupportedError(const std::string &what)
Definition error.h:51
The exception types the port throws.
Dense matrix and non-owning view.
AphPair< T > aph_simplify(const AphPair< T > &d1, const AphPair< T > &d2, const T &p1, const T &p2, AphPattern pattern)
Compose two matrix-exponential laws, as aph_simplify.m does.
AphPattern
The pattern argument of aph_simplify.m, by name.
Number-type abstraction for the templated API port.
A matrix-exponential law in (alpha, S) form: initial vector and subgenerator.
std::vector< T > alpha
(n) initial vector, possibly sub-stochastic
std::size_t order() const
Matrix< T > S
(n x n) subgenerator