LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
fluid_matrix.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_SOLVERS_FLUID_FLUID_MATRIX_H
6#define LINE_SOLVERS_FLUID_FLUID_MATRIX_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The `matrix` fluid method: a port of `solver_fluid_matrix.m`, the
12 * formulation of Ruuskanen, Berg, Lehtinen et al., PEVA 151 (2021).
13 *
14 * WHY THIS METHOD EXISTS ALONGSIDE `closing`. Both integrate the same fluid
15 * limit; they differ in how the drift is written. `closing` enumerates events
16 * and sums their jumps. This one writes the whole drift as one matrix,
17 *
18 * dx/dt = W' theta(x) + A_lambda, W = Psi + B P A'
19 *
20 * where Psi is the block diagonal of the phase generators D0, B the column of
21 * completion rates (the row sums of D1), P the station-to-station routing and
22 * A the block diagonal of entry-phase vectors. `theta(x)` is the only
23 * nonlinear part: it is the mass actually IN SERVICE,
24 *
25 * theta = x / (sum of x at the station) * min(servers, sum of x)
26 *
27 * so it equals x while the station is under-loaded and saturates at the server
28 * count above it. Writing the drift this way is what lets the p-norm smoothing
29 * be applied in one place, and it is why MATLAB and native Python make this
30 * the DEFAULT method.
31 *
32 * IT INTEGRATES ONCE, NOT ITERATIVELY. `closing` restarts from its own end
33 * state repeatedly; this method picks a single horizon,
34 * 10 * iter_max / min|W|, and integrates straight to it. Same fixed point,
35 * different route.
36 *
37 * THE SOURCE IS NOT IN THE DYNAMICS. An EXT station's states are held at zero
38 * with `theta = 0`, and its arrivals are injected directly into the phases of
39 * the queues they route to, through `A_lambda`. The reference also strips the
40 * routing back INTO a Source first, so an open class cannot cycle through it.
41 * That is why the fluid state of an open model carries no Source mass here,
42 * unlike the `closing` drift, which keeps a unit pool.
43 *
44 * DISABLED PAIRS. A (station, class) with no service still contributes one
45 * placeholder column so the block structure lines up; the reference marks it
46 * with NaN in A and then drops every NaN column from W. Reproduced, because
47 * the surviving index order is what every result map is aligned to.
48 */
49
50#include <algorithm>
51#include <cmath>
52#include <cstddef>
53#include <limits>
54#include <vector>
55
60#include "line/util/error.h"
61#include "line/util/matrix.h"
62
63namespace line {
64namespace fluid {
65
66/** The assembled matrix-form drift and the maps that read metrics off it. */
68 Matrix<double> W; ///< (n x n) drift generator
69 std::vector<double> alambda; ///< (n) external arrivals per state
70 std::vector<double> x0; ///< (n) initial state
71 std::vector<std::size_t> qa; ///< (n) 0-based station of each state
72 std::vector<double> sa; ///< (n) server count of that station
73 std::vector<bool> is_source; ///< (n) state belongs to an EXT station
74 std::vector<bool> is_inf; ///< (n) state belongs to an INF station
75 Matrix<double> sqc, suc, stc; ///< (M*K x n) queue, utilization, throughput maps
76 Matrix<double> src_arrival; ///< (M x K) per-class arrival rate of each EXT station
77 std::size_t nstates = 0;
78 double min_rate = 1.0; ///< smallest nonzero |W|, sets the horizon
79 double pstar = 0.0; ///< >0 selects the p-norm smoothing
80 /**
81 * When true, min(E[n], c) is replaced by E[min(n, c)] under the station's
82 * equilibrium geometric marginal. Set ONLY by the degeneracy repair in
83 * `solver_fluid`: min() is FLAT above the server count, so a network of
84 * saturated stations has a CONTINUUM of fixed points and the integrator
85 * returns whichever one it stopped at. See BUGS.md, _kb/06-solver-catalog.md.
86 */
87 bool var_closure = false;
88};
89
90namespace detail {
91
92/**
93 * Port of `compute_theta`: the mass in service.
94 *
95 * The FineTol in the denominator is the reference's, and is load bearing -- an
96 * empty station would otherwise divide zero by zero and poison the drift.
97 */
98inline void fluid_matrix_theta(const FluidMatrixSystem& s, const double* x,
99 std::vector<double>& theta) {
100 const std::size_t n = s.nstates;
101 // The total mass at each station, which every state of it shares.
102 std::vector<double> station_sum(s.sa.size(), 0.0);
103 std::vector<double> sums(n, 0.0);
104 std::size_t nst = 0;
105 for (std::size_t i = 0; i < n; ++i) nst = std::max(nst, s.qa[i] + 1);
106 station_sum.assign(nst, 0.0);
107 for (std::size_t i = 0; i < n; ++i) station_sum[s.qa[i]] += x[i];
108 for (std::size_t i = 0; i < n; ++i) {
109 const double tot = lang::GlobalConstants::FineTol + station_sum[s.qa[i]];
110 if (s.pstar > 0.0) {
111 // p-norm smoothing: ghat stands in for min(1, c/tot) smoothly. An
112 // INF station has k = infinity and so no min() to smooth; sa holds
113 // the population there, which would read it as a k = N queue.
114 const double c = s.sa[i];
115 double g = 1.0;
116 if (!s.is_inf[i] && tot > 0.0 && c > 0.0)
117 g = 1.0 / std::pow(1.0 + std::pow(tot / c, s.pstar), 1.0 / s.pstar);
118 if (std::isnan(g)) g = 0.0;
119 theta[i] = x[i] * g;
120 } else if (s.var_closure) {
121 // E[min(n, c)] UNDER A GEOMETRIC MARGINAL, not min(E[n], c):
122 // n ~ Geometric(mean m) => E[min(n,c)] = sum_{k=1..c} p^k
123 // = m * (1 - p^c), p = m/(1+m).
124 // Strictly increasing in m (slope 1/(1+m)^2 at c = 1, still 1e-2 at
125 // m = 9, a restoring force the integrator can follow inside its
126 // horizon) and with the same asymptotes, -> c as m -> inf and -> m
127 // as m -> 0. An INF station has a server per job, so there is no
128 // min() to close and sa holds the whole population there.
129 double emin;
130 if (s.is_inf[i]) {
131 emin = tot;
132 } else {
133 const double p = tot > 0.0 ? tot / (1.0 + tot) : 0.0;
134 emin = tot * (1.0 - std::pow(p, std::max(s.sa[i], 0.0)));
135 if (!std::isfinite(emin)) emin = 0.0;
136 emin = std::min(emin, tot);
137 }
138 theta[i] = x[i] / tot * emin;
139 } else {
140 theta[i] = x[i] / tot * std::min(s.sa[i], tot);
141 }
142 if (s.is_source[i]) theta[i] = 0.0; // the Source is out of the dynamics
143 }
144}
145
146} // namespace detail
147
148/** Assemble the matrix-form drift of `sn`. */
149template <class T>
151 const std::vector<double>& init_sol, double pstar) {
152 const std::size_t M = sn.nstations, K = sn.nclasses;
153 const FluidLayout L = fluid_layout(sn);
155 s.pstar = pstar;
156
157 // ---- station-to-station routing, via the stochastic complement ---------
158 // sn.rt is over stateful nodes; a Router is stateful but not a station, so
159 // the reference complements it out rather than indexing around it.
160 const std::size_t S = sn.nof_stateful();
161 std::vector<std::size_t> keep_idx;
162 keep_idx.reserve(M * K);
163 for (std::size_t i = 0; i < M; ++i) {
164 const std::size_t isf = sn.stateful_of_station(i + 1) - 1;
165 for (std::size_t r = 0; r < K; ++r) keep_idx.push_back(isf * K + r);
166 }
167 Matrix<double> rt_full(S * K, S * K, 0.0);
168 if (sn.rt.rows() == S * K)
169 for (std::size_t a = 0; a < S * K; ++a)
170 for (std::size_t b = 0; b < S * K; ++b)
171 rt_full(a, b) = num_traits<T>::to_double(sn.rt(a, b));
172 Matrix<double> P = mc::dtmc_stochcomp(rt_full, keep_idx);
173
174 // Per-class arrival rate of each EXT station, and the removal of any
175 // routing back INTO it: an open class leaves the Source once.
176 Matrix<double> src_arrival(M, K, 0.0);
177 for (std::size_t i = 0; i < M; ++i) {
178 if (sn.stations[i].sched != lang::SchedStrategy::EXT) continue;
179 for (std::size_t r = 0; r < K; ++r) {
180 if (!L.enabled[i][r]) continue;
182 const std::size_t nn = sn.service[i][r].D0.rows();
183 mp.D0 = Matrix<double>(nn, nn, 0.0);
184 mp.D1 = Matrix<double>(nn, nn, 0.0);
185 for (std::size_t a = 0; a < nn; ++a)
186 for (std::size_t b = 0; b < nn; ++b) {
187 mp.D0(a, b) = num_traits<T>::to_double(sn.service[i][r].D0(a, b));
188 mp.D1(a, b) = num_traits<T>::to_double(sn.service[i][r].D1(a, b));
189 }
190 const double mean = mam::map_mean(mp);
191 if (mean > 0.0) src_arrival(i, r) = 1.0 / mean;
192 if (src_arrival(i, r) > 0.0)
193 for (std::size_t j = 0; j < M; ++j) {
194 if (j == i) continue;
195 for (std::size_t q = 0; q < K; ++q) P(j * K + q, i * K + r) = 0.0;
196 }
197 }
198 }
199
200 // ---- the block-structured state space ---------------------------------
201 // One column per phase, or a single placeholder for a disabled pair. The
202 // placeholder is dropped below; it exists so the blocks line up first.
203 std::vector<std::size_t> blk_state; // first state index of block (i,r)
204 std::vector<std::size_t> blk_len; // phases in block (i,r), 0 when disabled
205 std::size_t nfull = 0;
206 for (std::size_t i = 0; i < M; ++i)
207 for (std::size_t r = 0; r < K; ++r) {
208 blk_state.push_back(nfull);
209 const std::size_t p = L.kic[i][r];
210 blk_len.push_back(p);
211 nfull += (p == 0) ? 1 : p;
212 }
213
214 Matrix<double> Wfull(nfull, nfull, 0.0);
215 std::vector<double> pie_full(nfull, 0.0), brate(nfull, 0.0);
216 std::vector<bool> disabled_state(nfull, false);
217 for (std::size_t i = 0; i < M; ++i)
218 for (std::size_t r = 0; r < K; ++r) {
219 const std::size_t b = blk_state[i * K + r], p = blk_len[i * K + r];
220 if (p == 0) {
221 disabled_state[b] = true; // the reference's NaN column
222 continue;
223 }
224 const lang::Distrib<T>& d = sn.service[i][r];
225 const std::vector<double> pie = detail::fluid_pie(d);
226 for (std::size_t a = 0; a < p; ++a) {
227 pie_full[b + a] = a < pie.size() ? pie[a] : 0.0;
228 for (std::size_t c = 0; c < p; ++c)
229 Wfull(b + a, b + c) = num_traits<T>::to_double(d.D0(a, c)); // Psi
230 double row = 0.0;
231 for (std::size_t c = 0; c < d.D1.cols(); ++c)
232 row += num_traits<T>::to_double(d.D1(a, c));
233 brate[b + a] = row; // B
234 }
235 }
236 // W += B P A': a completion at (i,r,a) routes to (j,l) and enters phase c.
237 for (std::size_t ir = 0; ir < M * K; ++ir) {
238 const std::size_t bi = blk_state[ir], pi = blk_len[ir];
239 if (pi == 0) continue;
240 for (std::size_t jl = 0; jl < M * K; ++jl) {
241 const double p = P(ir, jl);
242 if (!(p > 0.0)) continue;
243 const std::size_t bj = blk_state[jl], pj = blk_len[jl];
244 if (pj == 0) continue;
245 for (std::size_t a = 0; a < pi; ++a)
246 for (std::size_t c = 0; c < pj; ++c)
247 Wfull(bi + a, bj + c) += brate[bi + a] * p * pie_full[bj + c];
248 }
249 }
250
251 // ---- external arrivals, injected at the queues, not the Source --------
252 std::vector<double> alam_full(nfull, 0.0);
253 for (std::size_t i = 0; i < M; ++i) {
254 if (sn.stations[i].sched == lang::SchedStrategy::EXT) continue;
255 for (std::size_t r = 0; r < K; ++r) {
256 const std::size_t b = blk_state[i * K + r], p = blk_len[i * K + r];
257 if (p == 0) continue;
258 double rate = 0.0;
259 for (std::size_t sidx = 0; sidx < M; ++sidx)
260 if (src_arrival(sidx, r) > 0.0)
261 rate += src_arrival(sidx, r) * P(sidx * K + r, i * K + r);
262 if (!(rate > 0.0)) continue;
263 for (std::size_t a = 0; a < p; ++a) alam_full[b + a] = pie_full[b + a] * rate;
264 }
265 }
266
267 // ---- drop the disabled placeholders and build the result maps ---------
268 std::vector<std::size_t> keep;
269 for (std::size_t i = 0; i < nfull; ++i)
270 if (!disabled_state[i]) keep.push_back(i);
271 const std::size_t n = keep.size();
272 s.nstates = n;
273 s.W = Matrix<double>(n, n, 0.0);
274 for (std::size_t a = 0; a < n; ++a)
275 for (std::size_t b = 0; b < n; ++b) s.W(a, b) = Wfull(keep[a], keep[b]);
276 s.alambda.assign(n, 0.0);
277 s.x0.assign(n, 0.0);
278 s.qa.assign(n, 0);
279 s.sa.assign(n, 1.0);
280 s.is_source.assign(n, false);
281 s.is_inf.assign(n, false);
282 s.sqc = Matrix<double>(M * K, n, 0.0);
283 s.suc = Matrix<double>(M * K, n, 0.0);
284 s.stc = Matrix<double>(M * K, n, 0.0);
285
286 double closed_pop = 0.0;
287 for (std::size_t r = 0; r < K; ++r)
288 if (std::isfinite(sn.classes[r].population)) closed_pop += sn.classes[r].population;
289
290 // Map every surviving state back to its (station, class, phase).
291 std::vector<std::size_t> st_of(nfull, 0), cl_of(nfull, 0), ph_of(nfull, 0);
292 for (std::size_t i = 0; i < M; ++i)
293 for (std::size_t r = 0; r < K; ++r) {
294 const std::size_t b = blk_state[i * K + r], p = blk_len[i * K + r];
295 for (std::size_t a = 0; a < (p == 0 ? 1u : p); ++a) {
296 st_of[b + a] = i;
297 cl_of[b + a] = r;
298 ph_of[b + a] = a;
299 }
300 }
301
302 for (std::size_t a = 0; a < n; ++a) {
303 const std::size_t f = keep[a], i = st_of[f], r = cl_of[f], k = ph_of[f];
304 const double c = sn.stations[i].nservers;
305 const double servers = std::isfinite(c) ? c : closed_pop;
306 s.qa[a] = i;
307 s.sa[a] = servers;
308 s.alambda[a] = alam_full[f];
309 s.is_source[a] = (sn.stations[i].sched == lang::SchedStrategy::EXT);
310 s.is_inf[a] = !std::isfinite(c);
311 s.sqc(i * K + r, a) = 1.0;
312 s.suc(i * K + r, a) = (servers > 0.0) ? 1.0 / servers : 0.0;
313 double row = 0.0;
314 for (std::size_t cc = 0; cc < sn.service[i][r].D1.cols(); ++cc)
315 row += num_traits<T>::to_double(sn.service[i][r].D1(k, cc));
316 s.stc(i * K + r, a) = row;
317 // The Source carries no mass: its arrivals enter downstream.
318 s.x0[a] = s.is_source[a] ? 0.0 : (f < init_sol.size() ? 0.0 : 0.0);
319 }
320
321 // The initial state is given in the fluid layout's order (one entry per
322 // enabled phase); map it onto the surviving states.
323 if (!init_sol.empty()) {
324 for (std::size_t a = 0; a < n; ++a) {
325 const std::size_t f = keep[a], i = st_of[f], r = cl_of[f], k = ph_of[f];
326 if (s.is_source[a] || !L.enabled[i][r]) continue;
327 const std::size_t src = L.qidx[i][r] + k;
328 if (src < init_sol.size()) s.x0[a] = init_sol[src];
329 }
330 }
331
332 s.src_arrival = src_arrival;
333 double mr = std::numeric_limits<double>::infinity();
334 for (std::size_t a = 0; a < n; ++a)
335 for (std::size_t b = 0; b < n; ++b) {
336 const double v = std::fabs(s.W(a, b));
337 if (v > 0.0) mr = std::min(mr, v);
338 }
339 s.min_rate = std::isfinite(mr) ? mr : 1.0;
340 return s;
341}
342
343/** The drift dx/dt = W' theta(x) + A_lambda. */
344inline std::function<void(double, const double*, double*)> fluid_matrix_drift(
345 const FluidMatrixSystem& s) {
346 const std::size_t n = s.nstates;
347 return [s, n](double, const double* x, double* dx) {
348 std::vector<double> theta(n, 0.0);
349 detail::fluid_matrix_theta(s, x, theta);
350 for (std::size_t i = 0; i < n; ++i) {
351 double acc = s.alambda[i];
352 for (std::size_t j = 0; j < n; ++j) acc += s.W(j, i) * theta[j]; // W' theta
353 dx[i] = acc;
354 }
355 };
356}
357
358/**
359 * Is the returned point one of a CONTINUUM of fixed points?
360 *
361 * A station whose queue exceeds its server count has theta pinned at the server
362 * count, so the drift cannot tell one split of the mass between two such
363 * stations from another. The test is direct rather than structural: move a
364 * little mass from one station to another along a POPULATION-CONSERVING
365 * direction and see whether the drift moves at all. Both directions are tried,
366 * because the integrator typically stops on the BOUNDARY of the degenerate set,
367 * where one of the two does change the drift.
368 *
369 * The DIRECTIONAL DERIVATIVE is the scale-free quantity to threshold: a live
370 * direction moves the drift at the station's own service rate (measured
371 * 9.99e-01) and a null one only by the FineTol the share carries (measured
372 * 1.00e-08), four orders apart. Source and INF states are excluded: neither can
373 * be the pinned coordinate. Returns false whenever the point is not a fixed
374 * point at all, so a transient run is never repaired.
375 */
377 const FluidMatrixSystem& s,
378 const std::function<void(double, const double*, double*)>& drift,
379 const std::vector<double>& x, std::size_t K) {
380 const std::size_t n = s.nstates;
381 if (x.size() != n || n == 0 || K == 0) return false;
382 double max_abs_x = 1.0;
383 for (std::size_t i = 0; i < n; ++i) max_abs_x = std::max(max_abs_x, std::fabs(x[i]));
384 std::vector<double> d0(n, 0.0);
385 drift(0.0, x.data(), d0.data());
386 double max_d0 = 0.0;
387 for (std::size_t i = 0; i < n; ++i) max_d0 = std::max(max_d0, std::fabs(d0[i]));
388 if (max_d0 > 1e-6 * max_abs_x) return false;
389 if (s.sqc.rows() % K != 0) return false;
390 const std::size_t M = s.sqc.rows() / K;
391
392 double rate_scale = 0.0;
393 for (std::size_t a = 0; a < n; ++a)
394 for (std::size_t b = 0; b < n; ++b)
395 rate_scale = std::max(rate_scale, std::fabs(s.W(a, b)));
396 rate_scale = std::max(rate_scale, 1e-12);
397
398 const double step = 1e-3 * max_abs_x;
399 std::vector<double> xp(n, 0.0), dp(n, 0.0);
400 for (std::size_t r = 0; r < K; ++r) {
401 // PER CLASS, NOT PER STATION: a direction that moves a station's mass
402 // across ALL its classes is infeasible where a SelfLoopingClass is
403 // pinned at one station, and a well-posed model then reads as
404 // degenerate. Source and INF states carry no min() to pin.
405 std::vector<std::vector<std::size_t> > groups;
406 for (std::size_t i = 0; i < M; ++i) {
407 std::vector<std::size_t> members;
408 for (std::size_t a = 0; a < n; ++a)
409 if (!s.is_source[a] && !s.is_inf[a] && s.sqc(i * K + r, a) > 0.0)
410 members.push_back(a);
411 if (!members.empty()) groups.push_back(members);
412 }
413 if (groups.size() < 2) continue;
414 std::vector<double> mass(groups.size(), 0.0);
415 for (std::size_t a = 0; a < groups.size(); ++a)
416 for (std::size_t idx : groups[a]) mass[a] += x[idx];
417 for (std::size_t a = 0; a < groups.size(); ++a) {
418 if (mass[a] <= step) continue;
419 for (std::size_t b = 0; b < groups.size(); ++b) {
420 if (a == b) continue;
421 xp = x;
422 for (std::size_t idx : groups[a]) xp[idx] -= step * x[idx] / mass[a];
423 for (std::size_t idx : groups[b])
424 xp[idx] += (mass[b] > 0.0) ? step * x[idx] / mass[b]
425 : step / static_cast<double>(groups[b].size());
426 drift(0.0, xp.data(), dp.data());
427 double max_dd = 0.0;
428 for (std::size_t i = 0; i < n; ++i)
429 max_dd = std::max(max_dd, std::fabs(dp[i] - d0[i]));
430 if (max_dd / step <= 1e-4 * rate_scale) return true;
431 }
432 }
433 }
434 return false;
435}
436
437} // namespace fluid
438} // namespace line
439
440#endif // LINE_SOLVERS_FLUID_FLUID_MATRIX_H
std::size_t rows() const
Definition matrix.h:89
A network plus its refreshed NetworkStruct.
Stochastic complement of a DTMC partition, a port of matlab/lib/kpctoolbox/mc/dtmc_stochcomp....
The exception types the port throws.
The fluid drift: a port of solver_fluid_odes.m and the ode_jumps_new / ode_rate_base / ode_rates_clos...
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
Dense matrix and non-owning view.
FluidLayout fluid_layout(const qn::NetworkStruct< T > &sn)
Port of the layout half of solver_fluid_odes.m.
Definition fluid_odes.h:282
bool fluid_matrix_degenerate(const FluidMatrixSystem &s, const std::function< void(double, const double *, double *)> &drift, const std::vector< double > &x, std::size_t K)
Is the returned point one of a CONTINUUM of fixed points?
FluidMatrixSystem fluid_matrix_system(const qn::NetworkStruct< T > &sn, const std::vector< double > &init_sol, double pstar)
Assemble the matrix-form drift of sn.
std::function< void(double, const double *, double *)> fluid_matrix_drift(const FluidMatrixSystem &s)
The drift dx/dt = W' theta(x) + A_lambda.
T map_mean(const Map< T > &m)
Mean inter-arrival time, 1/lambda.
Definition map_moment.h:101
Matrix< T > dtmc_stochcomp(const Matrix< T > &P, const std::vector< std::size_t > &keep)
Stochastic complement of a DTMC partition, a port of matlab/lib/kpctoolbox/mc/dtmc_stochcomp....
A queueing network and its refreshed NetworkStruct.
Where each (station, class) block sits in the state vector.
Definition fluid_odes.h:86
std::vector< std::vector< std::size_t > > qidx
0-based first index of (i,r)
Definition fluid_odes.h:88
std::vector< std::vector< bool > > enabled
whether (i,r) is served at all
Definition fluid_odes.h:90
std::vector< std::vector< std::size_t > > kic
phases held by (i,r); 0 when disabled
Definition fluid_odes.h:89
The assembled matrix-form drift and the maps that read metrics off it.
double min_rate
smallest nonzero |W|, sets the horizon
bool var_closure
When true, min(E[n], c) is replaced by E[min(n, c)] under the station's equilibrium geometric margina...
Matrix< double > stc
(M*K x n) queue, utilization, throughput maps
Matrix< double > src_arrival
(M x K) per-class arrival rate of each EXT station
std::vector< double > alambda
(n) external arrivals per state
double pstar
>0 selects the p-norm smoothing
Matrix< double > W
(n x n) drift generator
std::vector< bool > is_inf
(n) state belongs to an INF station
std::vector< double > sa
(n) server count of that station
std::vector< double > x0
(n) initial state
std::vector< bool > is_source
(n) state belongs to an EXT station
std::vector< std::size_t > qa
(n) 0-based station of each state
Matrix< T > D0
The (D0,D1) pair when the type carries one directly.
Definition lang_types.h:759
static constexpr double FineTol
Definition lang_types.h:668
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