LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
lqn_ph.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_LQN_LQN_PH_H
6#define LINE_API_LQN_LQN_PH_H
7
8/**
9 * @file
10 * @ingroup api_lqn
11 * Phase-type composition of an LQN activity graph, the machinery behind
12 * SolverLN method 'srvn.ph'.
13 *
14 * An entry becomes a Workflow whose leaves are its activities and, when
15 * requested, its synchronous calls; the series-parallel reduction of that
16 * workflow is then the exact law of the entry service time. Port of the MATLAB
17 * lqn_entry_workflow.m, lqn_ph_serial_law.m and lqn_ph_moments.m, of the JAR
18 * jline.api.lqn.LqnPh and of the Python line_solver.api.lqn.lqn_ph.
19 */
20
21#include <cstddef>
22#include <string>
23#include <unordered_map>
24#include <vector>
25
29#include "line/util/error.h"
30#include "line/util/matrix.h"
31#include "line/util/lu.h"
32
33namespace line {
34namespace api {
35namespace lqn {
36
45
46/**
47 * The (alpha, S) pair of a phase-type Distrib, the form the composition rules
48 * take. For every Markovian family D1 = (-D0 e) alpha, so any row with a
49 * positive exit rate recovers alpha when the parameters do not carry it.
50 */
51template <class T>
53 const T zero = num_traits<T>::from_int(0);
54 PhLaw<T> out;
55 out.S = d.D0;
56 const std::size_t n = d.D0.rows();
59 d.params.size() == n) {
60 out.alpha.assign(d.params.begin(), d.params.begin() + static_cast<long>(n));
61 return out;
62 }
63 for (std::size_t i = 0; i < n; ++i) {
64 T tot = zero;
65 for (std::size_t j = 0; j < n; ++j) tot += d.D1(i, j);
66 if (!(tot > zero)) continue;
67 out.alpha.assign(n, zero);
68 for (std::size_t j = 0; j < n; ++j) out.alpha[j] = T(d.D1(i, j) / tot);
69 return out;
70 }
71 out.alpha.assign(n, zero);
72 if (n > 0) out.alpha[0] = num_traits<T>::from_int(1);
73 return out;
74}
75
76/** Activity graph of one entry, as a workflow plus its execution counts. */
77template <class T>
79 Workflow<T> wf{"entry"};
80 /** Workflow activity index of each LQN activity, npos when absent. */
81 std::unordered_map<std::size_t, std::size_t> act_idx_of;
82 /** Workflow activity index of each call, npos when absent. */
83 std::unordered_map<std::size_t, std::size_t> call_idx_of;
84 /** Expected executions of each LQN activity per entry invocation. */
85 std::unordered_map<std::size_t, T> execs;
86 /** Expected executions of each call per entry invocation. */
87 std::unordered_map<std::size_t, T> callexecs;
88};
89
90/**
91 * Activity graph of LQN entry EIDX as a Workflow.
92 *
93 * The precedences are read from LqnStruct::precedences rather than
94 * reconstructed from `graph`, whose loop back-edges carry probabilities and not
95 * counts.
96 *
97 * WITH_CALLS true expands every synchronous call of an activity into a leaf of
98 * its own, placed in series after the activity, so that the call response law
99 * and the host demand law stay separable across iterations. False keeps only
100 * the host demands, which is the processor-demand law of the entry: the host is
101 * released while a call is outstanding.
102 */
103template <class T>
104EntryWorkflow<T> entry_workflow(const ::line::lqn::LqnStruct<T>& lqn, std::size_t eidx, bool with_calls) {
105 const std::size_t tidx = lqn.parent[eidx];
106 const std::vector<std::size_t>& acts = lqn.actsof[eidx];
107 if (acts.empty())
108 throw InputError("Entry " + lqn.hashnames[eidx] + " binds no activity.");
109
111 out.wf = Workflow<T>(lqn.names[eidx] + ".Workflow");
112
113 std::unordered_map<std::size_t, std::string> head_name, tail_name;
114 std::unordered_map<std::size_t, std::size_t> act_of_name_idx; // by activity index
115 std::unordered_map<std::string, std::size_t> act_of_name;
116
117 for (std::size_t aidx : acts) {
118 const std::string& nm = lqn.names[aidx];
119 out.act_idx_of[aidx] = out.wf.add_activity(nm, lqn.hostdem[aidx]);
120 head_name[aidx] = nm;
121 tail_name[aidx] = nm;
122 act_of_name[nm] = aidx;
123 act_of_name_idx[aidx] = aidx;
124 }
125
126 if (with_calls) {
127 for (std::size_t aidx : acts) {
128 std::vector<std::string> chain;
129 chain.push_back(head_name[aidx]);
130 for (std::size_t cidx : lqn.callsof[aidx]) {
131 if (lqn.calltype[cidx] != line::lang::CallType::SYNC) {
132 continue; // an asynchronous call blocks the caller for no time
133 }
134 const std::string& cnm = lqn.callhashnames[cidx];
135 out.call_idx_of[cidx] = out.wf.add_activity(cnm, Distrib<T>::immediate());
136 chain.push_back(cnm);
137 }
138 if (chain.size() > 1) {
139 for (std::size_t k = 1; k < chain.size(); ++k) {
141 p.pre_acts.push_back(chain[k - 1]);
142 p.post_acts.push_back(chain[k]);
143 p.pre_type = PrecedenceType::PRE_SEQ;
144 p.post_type = PrecedenceType::POST_SEQ;
145 out.wf.add_precedence(p);
146 }
147 tail_name[aidx] = chain.back();
148 }
149 }
150 }
151
152 // Precedences of the task, restricted to the activities of this entry and
153 // rewritten so that a predecessor is entered at its head and left at its tail
154 if (tidx < lqn.precedences.size()) {
155 for (const auto& prec : lqn.precedences[tidx]) {
156 bool mine = true;
157 for (std::size_t a : prec.preacts)
158 if (act_of_name_idx.find(a) == act_of_name_idx.end()) mine = false;
159 for (std::size_t a : prec.postacts)
160 if (act_of_name_idx.find(a) == act_of_name_idx.end()) mine = false;
161 if (!mine) continue; // the precedence belongs to another entry of the same task
163 p.pre_type = prec.pretype;
164 p.post_type = prec.posttype;
165 p.pre_params = prec.preparams;
166 p.post_params = prec.postparams;
167 // A loop carries ONE count. LqnBuilder::loop repeats it once per body
168 // activity, which the arc expansion reads position 0 of and ignores the
169 // rest; the series-parallel parser instead requires exactly one.
170 if (prec.posttype == PrecedenceType::POST_LOOP && p.post_params.size() > 1)
171 p.post_params.resize(1);
172 for (std::size_t a : prec.preacts) p.pre_acts.push_back(tail_name[a]);
173 for (std::size_t a : prec.postacts) p.post_acts.push_back(head_name[a]);
174 out.wf.add_precedence(p);
175 }
176 }
177
178 // sp_tree() does not validate -- only to_ph() does -- and a quorum AND-join
179 // is exactly what validation refuses, so ask for it explicitly.
180 out.wf.validate();
181 const SPTree<T>* tree = out.wf.sp_tree();
182 if (tree == nullptr)
183 throw UnsupportedError(
184 "Entry " + lqn.hashnames[eidx] +
185 " has a precedence graph that is not series-parallel, so its activity graph has no "
186 "exact phase-type reduction. Use method='default'.");
187
188 for (std::size_t aidx : acts)
189 out.execs[aidx] = tree->execs[tree->leaf_of[out.act_idx_of[aidx]]];
190 for (const auto& kv : out.call_idx_of)
191 out.callexecs[kv.first] = tree->execs[tree->leaf_of[kv.second]];
192 return out;
193}
194
195/** Recursive body of serial_law, declared first so serial_law can call it. */
196template <class T>
198 std::size_t k) {
199 const auto& node = tree.nodes[k];
200 switch (node.type) {
201 case SPNodeType::LEAF:
202 return wf.activity_at(node.act).ph_representation();
203 case SPNodeType::SERIAL:
204 case SPNodeType::PAR: {
205 PhLaw<T> acc = detail_compose_serialized(wf, tree, node.kids[0]);
206 for (std::size_t i = 1; i < node.kids.size(); ++i)
208 detail_compose_serialized(wf, tree, node.kids[i]));
209 return acc;
210 }
211 case SPNodeType::OR: {
212 std::vector<PhLaw<T>> laws;
213 for (std::size_t kid : node.kids)
214 laws.push_back(detail_compose_serialized(wf, tree, kid));
215 return Workflow<T>::compose_mixture(laws, node.probs);
216 }
217 case SPNodeType::LOOP:
219 detail_compose_serialized(wf, tree, node.kids[0]), node.count);
220 }
221 throw InputError("Unknown series-parallel node type.");
222}
223
224/**
225 * Composed law of a workflow in which the branches of an AND fork are SERIAL
226 * rather than concurrent, that is, the total work the branches request rather
227 * than the elapsed time until the last of them finishes.
228 *
229 * This is the law of the PROCESSOR demand of an LQN entry. Two branches of an
230 * AND fork are two activity threads of the same task instance: they overlap in
231 * time, so the entry response time is the maximum of the branches, but they run
232 * on ONE processor, so the demand they place on it is the sum. Composing the
233 * host law with Workflow::to_ph would charge the processor the maximum and let
234 * the layer report a utilization below the true one, which no amount of
235 * iterating recovers.
236 *
237 * Every other node keeps its own composition rule: an OR fork is a mixture, a
238 * loop is a geometric compound, so the correlation within a branch survives.
239 */
240template <class T>
242 const SPTree<T>* tree = wf.sp_tree();
243 if (tree == nullptr)
244 throw UnsupportedError("Workflow " + wf.name() +
245 " is not series-parallel, so it has no exact phase-type reduction.");
246 return detail_compose_serialized(wf, *tree, tree->root);
247}
248
249/**
250 * First two moments of a phase-type law without building a Distrib, which is
251 * what the layered fixed point needs at every iteration for every composed
252 * entry law. A defective ALPHA carries an atom at zero and contributes nothing
253 * to either moment.
254 *
255 * Returns {mean, squared coefficient of variation}.
256 */
257template <class T>
258std::pair<T, T> ph_moments(const std::vector<T>& alpha, const Matrix<T>& S) {
259 const std::size_t n = S.rows();
260 const T zero = num_traits<T>::from_int(0);
261 Matrix<T> LU = S;
262 std::vector<std::size_t> piv;
265 try {
266 piv = lu_factor(LU);
267 } catch (const std::exception&) {
268 return bad;
269 }
270 std::vector<T> x1(n, num_traits<T>::from_int(1));
271 lu_solve(LU, piv, x1);
272 for (std::size_t i = 0; i < n; ++i) x1[i] = T(-x1[i]);
273 T m1 = zero;
274 for (std::size_t i = 0; i < n && i < alpha.size(); ++i) m1 += alpha[i] * x1[i];
275
276 std::vector<T> x2 = x1;
277 lu_solve(LU, piv, x2);
278 T m2 = zero;
279 for (std::size_t i = 0; i < n && i < alpha.size(); ++i) m2 += alpha[i] * x2[i];
280 m2 = T(num_traits<T>::from_int(-2) * m2);
281
282 const double m1d = num_traits<T>::to_double(m1);
283 if (!std::isfinite(m1d) || m1d <= GlobalConstants::FineTol) return bad;
284 T scv = T(m2 / (m1 * m1) - num_traits<T>::from_int(1));
285 const double scvd = num_traits<T>::to_double(scv);
286 if (!std::isfinite(scvd) || scvd <= GlobalConstants::FineTol)
288 return std::make_pair(m1, scv);
289}
290
291} // namespace lqn
292} // namespace api
293} // namespace line
294
295#endif // LINE_API_LQN_LQN_PH_H
InputError(const std::string &what)
Definition error.h:39
std::size_t rows() const
Definition matrix.h:89
UnsupportedError(const std::string &what)
Definition error.h:51
static PhLaw< T > compose_loop_geometric(const PhLaw< T > &body, const T &count)
Geometric repetition of a phase-type law, the POST_LOOP semantics.
Definition workflow.h:585
static PhLaw< T > compose_serial(const PhLaw< T > &a, const PhLaw< T > &b)
Serial composition: the second law starts when the first absorbs.
Definition workflow.h:463
Workflow(const std::string &name)
Definition workflow.h:227
static PhLaw< T > compose_mixture(const std::vector< PhLaw< T > > &laws, const std::vector< T > &probs)
Probabilistic mixture: a block-diagonal generator whose initial vector picks branch i with probabilit...
Definition workflow.h:548
The exception types the port throws.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
LayeredNetworkStruct, the flattened description of a layered queueing network.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
EntryWorkflow< T > entry_workflow(const ::line::lqn::LqnStruct< T > &lqn, std::size_t eidx, bool with_calls)
Activity graph of LQN entry EIDX as a Workflow.
Definition lqn_ph.h:104
PhLaw< T > ph_law_of(const Distrib< T > &d)
The (alpha, S) pair of a phase-type Distrib, the form the composition rules take.
Definition lqn_ph.h:52
PhLaw< T > detail_compose_serialized(Workflow< T > &wf, const SPTree< T > &tree, std::size_t k)
Recursive body of serial_law, declared first so serial_law can call it.
Definition lqn_ph.h:197
PhLaw< T > serial_law(Workflow< T > &wf)
Composed law of a workflow in which the branches of an AND fork are SERIAL rather than concurrent,...
Definition lqn_ph.h:241
std::pair< T, T > ph_moments(const std::vector< T > &alpha, const Matrix< T > &S)
First two moments of a phase-type law without building a Distrib, which is what the layered fixed poi...
Definition lqn_ph.h:258
PrecedenceType
Activity precedence kinds, with the values of MATLAB ActivityPrecedenceType.
Definition lang_types.h:470
SPNodeType
A node of the series-parallel tree.
Definition workflow.h:189
void lu_solve(const Matrix< T > &LU, const std::vector< std::size_t > &piv, std::vector< T > &b)
Solve LUx = Pb in place on b, using the factors from lu_factor.
Definition lu.h:94
std::vector< std::size_t > lu_factor(Matrix< T > &A)
In-place LU of A (n x n).
Definition lu.h:48
static Distrib immediate()
The Immediate singleton.
Definition lang_types.h:846
Activity graph of one entry, as a workflow plus its execution counts.
Definition lqn_ph.h:78
std::unordered_map< std::size_t, T > execs
Expected executions of each LQN activity per entry invocation.
Definition lqn_ph.h:85
std::unordered_map< std::size_t, T > callexecs
Expected executions of each call per entry invocation.
Definition lqn_ph.h:87
std::unordered_map< std::size_t, std::size_t > act_idx_of
Workflow activity index of each LQN activity, npos when absent.
Definition lqn_ph.h:81
std::unordered_map< std::size_t, std::size_t > call_idx_of
Workflow activity index of each call, npos when absent.
Definition lqn_ph.h:83
static constexpr double FineTol
Definition lang_types.h:668
Matrix< T > D0
The (D0,D1) pair when the type carries one directly.
Definition lang_types.h:759
std::vector< T > params
Constructor arguments, in MATLAB getParam order.
Definition lang_types.h:734
The MATLAB GlobalConstants, as reported by lineStart at its defaults.
Definition lang_types.h:667
A phase-type law as the composition rules pass it around.
Definition workflow.h:89
std::vector< T > alpha
Definition workflow.h:90
One precedence of the activity graph.
Definition workflow.h:78
std::vector< T > pre_params
Definition workflow.h:83
PrecedenceType pre_type
Definition workflow.h:81
PrecedenceType post_type
Definition workflow.h:82
std::vector< T > post_params
Definition workflow.h:84
std::vector< std::string > post_acts
Definition workflow.h:80
std::vector< std::string > pre_acts
Definition workflow.h:79
The flat series-parallel tree.
Definition workflow.h:214
std::vector< SPNode< T > > nodes
Definition workflow.h:215
std::vector< T > execs
Definition workflow.h:219
std::vector< std::size_t > leaf_of
Node index of each activity's leaf, npos when the activity has none.
Definition workflow.h:218
An activity workflow reduced to one phase-type law.