LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ldqbd_mphc.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_LDQBD_MPHC_H
6#define LINE_API_MAM_LDQBD_MPHC_H
7
8/**
9 * @file
10 * @ingroup api_mam
11 * Port of `ldqbd_mphc.m` and `ph_multisets.m`: the exact level-dependent QBD
12 * blocks of an M/PH/c queue.
13 *
14 * THE POINT OF THE MULTISET. The level is the number of jobs at the station,
15 * and the coordinate INSIDE a level is the multiset of the phases the min(n,c)
16 * busy servers sit in. The collapsed alternative -- one PH process run at
17 * min(n,c) times its speed -- gets the aggregate service rate right but forgets
18 * which phase each busy server is in, which turns c servers into one fast
19 * server whose remaining work is a single phase-type variable. Counting rather
20 * than ordering the phases costs nchoosek(min(n,c)+p-1, p-1) states per level
21 * instead of p^min(n,c), because identical servers are exchangeable.
22 *
23 * LEVEL SIZES GROW over the boundary levels 0..c and repeat above them, so the
24 * blocks joining differently sized neighbours are rectangular. `ldqbd`, its
25 * rate matrices and its stationary vector all accept that heterogeneity; level
26 * 0 is the single empty configuration.
27 *
28 * References: S. Asmussen and J.R. Moller, "Calculation of the steady state
29 * waiting time distribution in GI/PH/c and MAP/PH/c queues", Queueing Systems
30 * 37(1):9-29, 2001; M. F. Neuts, "Matrix-geometric solutions in stochastic
31 * models", Johns Hopkins University Press, 1981.
32 */
33
34#include <algorithm>
35#include <cmath>
36#include <cstddef>
37#include <map>
38#include <string>
39#include <utility>
40#include <vector>
41
42#include "line/util/error.h"
43#include "line/util/linalg.h"
44#include "line/util/matrix.h"
45
46namespace line {
47namespace mam {
48
49/**
50 * The widest level is the repeating one, and the LD-QBD recursion inverts one
51 * matrix of that order per level, so that is the size worth guarding.
52 */
53inline constexpr std::size_t LDQBD_MPHC_MAX_CONFIGS = 2000;
54
55/**
56 * Configurations of k identical servers over p service phases.
57 *
58 * Rows are the compositions of k into p nonnegative parts: entry (r,i) is the
59 * number of the k busy servers sitting in phase i. The order is fixed and
60 * shared by every caller, so a configuration index means the same thing in each
61 * of them: the first part descends, hence k = 1 yields the identity rows
62 * e_1 ... e_p in phase order, which is what makes the c = 1 case coincide with
63 * plain phase indexing.
64 */
65inline std::vector<std::vector<int> > ph_multisets(std::size_t p, std::size_t k) {
66 std::vector<std::vector<int> > out;
67 if (k == 0) {
68 out.push_back(std::vector<int>(p, 0));
69 return out;
70 }
71 if (p == 1) {
72 out.push_back(std::vector<int>(1, static_cast<int>(k)));
73 return out;
74 }
75 for (int first = static_cast<int>(k); first >= 0; --first) {
76 const std::vector<std::vector<int> > rest =
77 ph_multisets(p - 1, k - static_cast<std::size_t>(first));
78 for (std::size_t i = 0; i < rest.size(); ++i) {
79 std::vector<int> row;
80 row.push_back(first);
81 row.insert(row.end(), rest[i].begin(), rest[i].end());
82 out.push_back(row);
83 }
84 }
85 return out;
86}
87
88/** The three block lists of a level-dependent QBD, as `ldqbd` takes them. */
89template <class T>
91 std::vector<Matrix<T> > Q0; // size Nlev : upward, level n -> n+1
92 std::vector<Matrix<T> > Q1; // size Nlev+1 : local
93 std::vector<Matrix<T> > Q2; // size Nlev+1 : downward, Q2[n] leaves level n (Q2[0] unused)
94};
95
96/**
97 * Carried-phase MAP/c blocks, the `carry` form of `ldqbd_mphc`. The station draws service
98 * times from ONE MAP in start order: a job's service begins in phase h, the phase the previous
99 * job's service ended in. V = (-D0)^-1 D1: a job starting in h ends in j w.p. V(h,j), so each
100 * busy server is a pair (i,j) run under the Doob transform of D0 conditioned on ending in j
101 * (i -> k at D0(i,k)V(k,j)/V(i,j), completion at D1(i,j)/V(i,j)). A level is the multiset of
102 * busy pairs times h, the end phase of the job started last (index row*p + h); level 0 is h
103 * alone. At c = 1 this is the frozen-phase MAP/MAP/1 chain; a renewal MAP gives the PH/c law.
104 */
105template <class T>
106LdqbdMphcBlocks<T> ldqbd_mapc_carry(const Matrix<T>& D0, const Matrix<T>& D1, std::size_t cmax,
107 const std::vector<T>& arrRate, const std::vector<T>& sf) {
108 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
109 const std::size_t p = D0.rows();
110 const std::size_t Nlev = arrRate.size() - 1;
111 Matrix<T> negD0(p, p, zero);
112 for (std::size_t i = 0; i < p; ++i)
113 for (std::size_t j = 0; j < p; ++j) negD0(i, j) = T(-D0(i, j));
114 const Matrix<T> Ninv = inverse(negD0);
115 Matrix<T> V(p, p, zero);
116 std::vector<std::vector<int> > tp(p, std::vector<int>(p, -1));
117 std::vector<std::pair<std::size_t, std::size_t> > pairs;
118 for (std::size_t i = 0; i < p; ++i)
119 for (std::size_t j = 0; j < p; ++j) {
120 T v = zero;
121 for (std::size_t k = 0; k < p; ++k) v += T(Ninv(i, k) * D1(k, j));
122 if (std::fabs(num_traits<T>::to_double(v)) < 1e-14) v = zero;
123 V(i, j) = v;
124 if (num_traits<T>::to_double(v) > 0.0) {
125 tp[i][j] = static_cast<int>(pairs.size());
126 pairs.push_back(std::make_pair(i, j));
127 }
128 }
129 const std::size_t NT = pairs.size();
130 std::vector<T> done(NT, zero);
131 for (std::size_t t = 0; t < NT; ++t)
132 done[t] = T(D1(pairs[t].first, pairs[t].second) / V(pairs[t].first, pairs[t].second));
133
134 std::vector<std::vector<std::vector<int> > > cfg(cmax + 1);
135 std::vector<std::map<std::vector<int>, std::size_t> > pos(cmax + 1);
136 std::vector<std::size_t> nCfg(cmax + 1, 0), nk(cmax + 1, 0);
137 for (std::size_t k = 0; k <= cmax; ++k) {
138 cfg[k] = ph_multisets(NT, k);
139 for (std::size_t r = 0; r < cfg[k].size(); ++r) pos[k][cfg[k][r]] = r;
140 nCfg[k] = cfg[k].size();
141 nk[k] = nCfg[k] * p;
142 }
143 if (nk[cmax] > LDQBD_MPHC_MAX_CONFIGS)
144 throw UnsupportedError(
145 "ldqbd_mphc: the exact MAP/c chain with a carried service phase needs " +
146 std::to_string(nk[cmax]) + " configurations per level for " + std::to_string(cmax) +
147 " servers and " + std::to_string(p) + " service phases, above the " +
148 std::to_string(LDQBD_MPHC_MAX_CONFIGS) +
149 " the level-by-level inverses can carry. Use fewer servers or SolverLDES on this model");
150
151 std::vector<Matrix<T> > LOC(cmax + 1), UP(cmax + 1), DN(cmax + 1);
152 for (std::size_t k = 0; k <= cmax; ++k) {
153 LOC[k] = Matrix<T>(nk[k], nk[k], zero);
154 if (k < cmax) UP[k] = Matrix<T>(nk[k], nk[k + 1], zero);
155 if (k > 0) DN[k] = Matrix<T>(nk[k], nk[k - 1], zero);
156 for (std::size_t row = 0; row < nCfg[k]; ++row) {
157 const std::vector<int>& m = cfg[k][row];
158 for (std::size_t h = 0; h < p; ++h) {
159 const std::size_t a = row * p + h;
160 for (std::size_t t = 0; t < NT; ++t) {
161 if (m[t] == 0) continue;
162 const T mult = num_traits<T>::from_int(m[t]);
163 const std::size_t i = pairs[t].first, j = pairs[t].second;
164 LOC[k](a, a) += T(mult * D0(i, i));
165 for (std::size_t kk = 0; kk < p; ++kk) {
166 if (kk == i || tp[kk][j] < 0 || num_traits<T>::to_double(D0(i, kk)) == 0.0)
167 continue;
168 std::vector<int> mm = m;
169 --mm[t];
170 ++mm[tp[kk][j]];
171 LOC[k](a, pos[k][mm] * p + h) += T(mult * D0(i, kk) * V(kk, j) / V(i, j));
172 }
173 if (k > 0) {
174 std::vector<int> mm = m;
175 --mm[t];
176 DN[k](a, pos[k - 1][mm] * p + h) += T(mult * done[t]);
177 }
178 }
179 if (k < cmax) {
180 for (std::size_t j = 0; j < p; ++j) {
181 if (tp[h][j] < 0) continue;
182 std::vector<int> mm = m;
183 ++mm[tp[h][j]];
184 UP[k](a, pos[k + 1][mm] * p + j) += V(h, j);
185 }
186 }
187 }
188 }
189 }
190 // Full bank: a completion hands the server to the next job, which starts in h.
191 Matrix<T> CDEP(nk[cmax], nk[cmax], zero);
192 for (std::size_t row = 0; row < nCfg[cmax]; ++row) {
193 const std::vector<int>& m = cfg[cmax][row];
194 for (std::size_t h = 0; h < p; ++h)
195 for (std::size_t t = 0; t < NT; ++t) {
196 if (m[t] == 0) continue;
197 const T mult = num_traits<T>::from_int(m[t]);
198 for (std::size_t j = 0; j < p; ++j) {
199 if (tp[h][j] < 0) continue;
200 std::vector<int> mm = m;
201 --mm[t];
202 ++mm[tp[h][j]];
203 CDEP(row * p + h, pos[cmax][mm] * p + j) += T(mult * done[t] * V(h, j));
204 }
205 }
206 }
207 std::vector<T> speed(Nlev + 1, one);
208 if (!sf.empty()) {
209 for (std::size_t n = 1; n <= Nlev; ++n) {
210 const std::size_t b = std::min(n, cmax);
211 const T bt = num_traits<T>::from_int(static_cast<int>(b));
212 if (!(sf[n - 1] == bt)) speed[n] = T(sf[n - 1] / bt);
213 }
214 }
216 out.Q0.assign(Nlev, Matrix<T>(1, 1, zero));
217 out.Q1.assign(Nlev + 1, Matrix<T>(1, 1, zero));
218 out.Q2.assign(Nlev + 1, Matrix<T>(1, 1, zero));
219 out.Q1[0] = Matrix<T>(p, p, zero); // level 0: arrivals only, h frozen
220 for (std::size_t i = 0; i < p; ++i) out.Q1[0](i, i) = T(-arrRate[0]);
221 for (std::size_t n = 1; n <= Nlev; ++n) {
222 const std::size_t b = std::min(n, cmax);
223 Matrix<T> B(nk[b], nk[b], zero);
224 for (std::size_t i = 0; i < nk[b]; ++i) {
225 for (std::size_t j = 0; j < nk[b]; ++j) B(i, j) = T(speed[n] * LOC[b](i, j));
226 B(i, i) -= arrRate[n];
227 }
228 out.Q1[n] = B;
229 }
230 for (std::size_t n = 0; n + 1 <= Nlev; ++n) {
231 if (n < cmax) {
232 Matrix<T> B(nk[n], nk[n + 1], zero);
233 for (std::size_t i = 0; i < nk[n]; ++i)
234 for (std::size_t j = 0; j < nk[n + 1]; ++j) B(i, j) = T(arrRate[n] * UP[n](i, j));
235 out.Q0[n] = B;
236 } else {
237 Matrix<T> B(nk[cmax], nk[cmax], zero);
238 for (std::size_t i = 0; i < nk[cmax]; ++i) B(i, i) = arrRate[n];
239 out.Q0[n] = B;
240 }
241 }
242 for (std::size_t n = 1; n <= Nlev; ++n) {
243 const Matrix<T>& S = (n <= cmax) ? DN[n] : CDEP;
244 Matrix<T> B(S.rows(), S.cols(), zero);
245 for (std::size_t i = 0; i < S.rows(); ++i)
246 for (std::size_t j = 0; j < S.cols(); ++j) B(i, j) = T(speed[n] * S(i, j));
247 out.Q2[n] = B;
248 }
249 return out;
250}
251
252/**
253 * Block-tridiagonal generator of an M/PH/c queue with level-dependent arrivals.
254 *
255 * @param D0 service sub-generator (p x p), phase changes without completion
256 * @param D1 service completion block (p x p); D1 = (-D0*1)*alpha for a PH
257 * @param alpha length-p vector a server starts each new job in
258 * @param c number of identical servers (>= 1; capped at the top level)
259 * @param arrRate length Nlev+1; arrRate[n] is the arrival rate out of level n
260 * @param sf empty, or length >= Nlev: a multiplier on the station's TOTAL
261 * service rate at level n (load dependence), so each busy server
262 * runs at sf[n-1]/min(n,c) of nominal and sf[n-1] = min(n,c)
263 * reproduces the unscaled queue exactly
264 * @param carry true for a correlated (non-renewal) MAP service, carried across services in
265 * start order, idle periods included (how JMT and LDES sample it); alpha is
266 * then unused. See `ldqbd_mapc_carry`
267 *
268 * Q2 carries an unused entry at index 0 so the three lists line up by level,
269 * matching what the C++ `ldqbd` expects.
270 */
271template <class T>
273 const std::vector<T>& alpha, double c,
274 const std::vector<T>& arrRate, const std::vector<T>& sf,
275 bool carry = false) {
276 const T zero = num_traits<T>::from_int(0);
277 const std::size_t p = D0.rows();
278 if (arrRate.empty())
279 throw InputError("ldqbd_mphc: needs at least one level above the empty one");
280 const std::size_t Nlev = arrRate.size() - 1;
281 if (Nlev < 1)
282 throw InputError("ldqbd_mphc: needs at least one level above the empty one");
283 if (D0.cols() != p || D1.rows() != p || D1.cols() != p || alpha.size() != p)
284 throw InputError("ldqbd_mphc: D0, D1 and alpha must all have the same order");
285 if (!sf.empty() && sf.size() < Nlev)
286 throw InputError("ldqbd_mphc: sf must give one total-service-rate factor per level");
287
288 // Servers that can never be busy do not need a coordinate: above the top
289 // level there is nothing left to serve.
290 const std::size_t cmax =
291 !std::isfinite(c) ? Nlev
292 : std::min(std::max<std::size_t>(1, static_cast<std::size_t>(
293 std::llround(c))),
294 Nlev);
295 if (carry) return ldqbd_mapc_carry(D0, D1, cmax, arrRate, sf);
296
297 std::vector<std::vector<std::vector<int> > > cfg(cmax + 1);
298 std::vector<std::map<std::vector<int>, std::size_t> > pos(cmax + 1);
299 std::vector<std::size_t> nCfg(cmax + 1, 0);
300 for (std::size_t k = 0; k <= cmax; ++k) {
301 cfg[k] = ph_multisets(p, k);
302 for (std::size_t r = 0; r < cfg[k].size(); ++r) pos[k][cfg[k][r]] = r;
303 nCfg[k] = cfg[k].size();
304 }
305
306 if (nCfg[cmax] > LDQBD_MPHC_MAX_CONFIGS)
307 throw UnsupportedError(
308 "ldqbd_mphc: the exact M/PH/c chain needs " + std::to_string(nCfg[cmax]) +
309 " configurations per level for " + std::to_string(cmax) + " servers and " +
310 std::to_string(p) +
311 " service phases, above the " + std::to_string(LDQBD_MPHC_MAX_CONFIGS) +
312 " the level-by-level inverses can carry. Use fewer phases (a lower-order fit), "
313 "fewer servers, or SolverCTMC/SolverLDES on this model");
314
315 // Completion rate out of each phase, summed over targets.
316 std::vector<T> t(p, zero);
317 for (std::size_t i = 0; i < p; ++i)
318 for (std::size_t j = 0; j < p; ++j) t[i] += D1(i, j);
319
320 // Structural blocks per busy-server count: LOC the within-level phase
321 // changes (with the full outflow on its diagonal), UP the entry of a newly
322 // busy server, DN a completion that leaves a server idle.
323 std::vector<Matrix<T> > LOC(cmax + 1), UP(cmax + 1), DN(cmax + 1);
324 for (std::size_t k = 0; k <= cmax; ++k) {
325 const std::vector<std::vector<int> >& Ck = cfg[k];
326 LOC[k] = Matrix<T>(nCfg[k], nCfg[k], zero);
327 for (std::size_t row = 0; row < nCfg[k]; ++row) {
328 const std::vector<int>& m = Ck[row];
329 for (std::size_t i = 0; i < p; ++i) {
330 if (m[i] == 0) continue;
331 const T mult = num_traits<T>::from_int(m[i]);
332 for (std::size_t j = 0; j < p; ++j) {
333 if (j == i) continue;
334 std::vector<int> mm = m;
335 --mm[i];
336 ++mm[j];
337 LOC[k](row, pos[k][mm]) += T(mult * D0(i, j));
338 }
339 // D0(i,i) is the total outflow of phase i, completions included
340 LOC[k](row, row) += T(mult * D0(i, i));
341 }
342 }
343
344 if (k < cmax) {
345 UP[k] = Matrix<T>(nCfg[k], nCfg[k + 1], zero);
346 for (std::size_t row = 0; row < nCfg[k]; ++row) {
347 const std::vector<int>& m = Ck[row];
348 for (std::size_t j = 0; j < p; ++j) {
349 std::vector<int> mm = m;
350 ++mm[j];
351 UP[k](row, pos[k + 1][mm]) += alpha[j];
352 }
353 }
354 }
355
356 if (k > 0) {
357 DN[k] = Matrix<T>(nCfg[k], nCfg[k - 1], zero);
358 for (std::size_t row = 0; row < nCfg[k]; ++row) {
359 const std::vector<int>& m = Ck[row];
360 for (std::size_t i = 0; i < p; ++i) {
361 if (m[i] == 0) continue;
362 std::vector<int> mm = m;
363 --mm[i];
364 DN[k](row, pos[k - 1][mm]) += T(num_traits<T>::from_int(m[i]) * t[i]);
365 }
366 }
367 }
368 }
369
370 // A completion at a full server bank takes the next waiting job at once, so
371 // the server stays busy and only its phase moves: the repeating down block.
372 Matrix<T> CDEP(nCfg[cmax], nCfg[cmax], zero);
373 for (std::size_t row = 0; row < nCfg[cmax]; ++row) {
374 const std::vector<int>& m = cfg[cmax][row];
375 for (std::size_t i = 0; i < p; ++i) {
376 if (m[i] == 0) continue;
377 const T mult = num_traits<T>::from_int(m[i]);
378 for (std::size_t j = 0; j < p; ++j) {
379 std::vector<int> mm = m;
380 --mm[i];
381 ++mm[j];
382 CDEP(row, pos[cmax][mm]) += T(mult * D1(i, j));
383 }
384 }
385 }
386
387 // Per-server speed. Without load dependence every busy server runs at its
388 // nominal rate; with it, the aggregate sf(n) is shared over the busy
389 // servers. sf(n) == min(n,c) is passed through as exactly one so the
390 // unscaled chain is reproduced bit for bit.
391 const T one = num_traits<T>::from_int(1);
392 std::vector<T> speed(Nlev + 1, one);
393 if (!sf.empty()) {
394 for (std::size_t n = 1; n <= Nlev; ++n) {
395 const std::size_t b = std::min(n, cmax);
396 const T bt = num_traits<T>::from_int(static_cast<int>(b));
397 if (!(sf[n - 1] == bt)) speed[n] = T(sf[n - 1] / bt);
398 }
399 }
400
402 out.Q0.assign(Nlev, Matrix<T>(1, 1, zero));
403 out.Q1.assign(Nlev + 1, Matrix<T>(1, 1, zero));
404 out.Q2.assign(Nlev + 1, Matrix<T>(1, 1, zero));
405
406 out.Q1[0] = Matrix<T>(1, 1, T(-arrRate[0])); // level 0: arrivals only
407 for (std::size_t n = 1; n <= Nlev; ++n) {
408 const std::size_t b = std::min(n, cmax);
409 Matrix<T> B(nCfg[b], nCfg[b], zero);
410 for (std::size_t i = 0; i < nCfg[b]; ++i) {
411 for (std::size_t j = 0; j < nCfg[b]; ++j) B(i, j) = T(speed[n] * LOC[b](i, j));
412 B(i, i) -= arrRate[n];
413 }
414 out.Q1[n] = B;
415 }
416
417 for (std::size_t n = 0; n + 1 <= Nlev; ++n) {
418 if (n < cmax) {
419 // a free server takes the job, starting it in a phase drawn from alpha
420 Matrix<T> B(nCfg[n], nCfg[n + 1], zero);
421 for (std::size_t i = 0; i < nCfg[n]; ++i)
422 for (std::size_t j = 0; j < nCfg[n + 1]; ++j) B(i, j) = T(arrRate[n] * UP[n](i, j));
423 out.Q0[n] = B;
424 } else {
425 // the job waits, so every busy phase is unchanged
426 Matrix<T> B(nCfg[cmax], nCfg[cmax], zero);
427 for (std::size_t i = 0; i < nCfg[cmax]; ++i) B(i, i) = arrRate[n];
428 out.Q0[n] = B;
429 }
430 }
431
432 for (std::size_t n = 1; n <= Nlev; ++n) {
433 if (n <= cmax) {
434 Matrix<T> B(nCfg[n], nCfg[n - 1], zero); // the server falls idle
435 for (std::size_t i = 0; i < nCfg[n]; ++i)
436 for (std::size_t j = 0; j < nCfg[n - 1]; ++j) B(i, j) = T(speed[n] * DN[n](i, j));
437 out.Q2[n] = B;
438 } else {
439 Matrix<T> B(nCfg[cmax], nCfg[cmax], zero); // it takes the next job
440 for (std::size_t i = 0; i < nCfg[cmax]; ++i)
441 for (std::size_t j = 0; j < nCfg[cmax]; ++j) B(i, j) = T(speed[n] * CDEP(i, j));
442 out.Q2[n] = B;
443 }
444 }
445
446 return out;
447}
448
449} // namespace mam
450} // namespace line
451
452#endif // LINE_API_MAM_LDQBD_MPHC_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 linear algebra over the templated number type: products, identity, inverse, and powers.
Dense matrix and non-owning view.
std::vector< std::vector< int > > ph_multisets(std::size_t p, std::size_t k)
Configurations of k identical servers over p service phases.
Definition ldqbd_mphc.h:65
LdqbdMphcBlocks< T > ldqbd_mphc(const Matrix< T > &D0, const Matrix< T > &D1, const std::vector< T > &alpha, double c, const std::vector< T > &arrRate, const std::vector< T > &sf, bool carry=false)
Block-tridiagonal generator of an M/PH/c queue with level-dependent arrivals.
Definition ldqbd_mphc.h:272
LdqbdMphcBlocks< T > ldqbd_mapc_carry(const Matrix< T > &D0, const Matrix< T > &D1, std::size_t cmax, const std::vector< T > &arrRate, const std::vector< T > &sf)
Carried-phase MAP/c blocks, the carry form of ldqbd_mphc.
Definition ldqbd_mphc.h:106
constexpr std::size_t LDQBD_MPHC_MAX_CONFIGS
The widest level is the repeating one, and the LD-QBD recursion inverts one matrix of that order per ...
Definition ldqbd_mphc.h:53
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
Matrix< T > inverse(const Matrix< T > &A)
Inverse by LU with one factorization and n back substitutions.
Definition linalg.h:72
The three block lists of a level-dependent QBD, as ldqbd takes them.
Definition ldqbd_mphc.h:90
std::vector< Matrix< T > > Q0
Definition ldqbd_mphc.h:91
std::vector< Matrix< T > > Q1
Definition ldqbd_mphc.h:92
std::vector< Matrix< T > > Q2
Definition ldqbd_mphc.h:93