LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
getMomentStationTable.m
1function [MomentStationTable, mom] = getMomentStationTable(self, order)
2% GETMOMENTSTATIONTABLE Exact higher moments of the total queue length.
3%
4% [MOMENTSTATIONTABLE, MOM] = GETMOMENTSTATIONTABLE(SELF) returns a table with
5% one row per Station giving the moments of the TOTAL queue length at that
6% station, Q_i = sum_r n(i,r):
7% QLen, QLenVar, QLenSCV
8%
9% [..] = GETMOMENTSTATIONTABLE(SELF, ORDER) selects which moment orders to
10% report. ORDER is a set: a scalar k is read as 1:k, "everything up to order
11% k"; an explicit vector selects exactly those orders.
12% 1 the mean only: QLen
13% 2 (default) mean and second moment: QLen, QLenVar, QLenSCV
14% 3 also adds QLenM3 and QLenSkew
15% [1 3] the mean and the third moment, without the variance
16% Order 1 contributes QLen, order 2 contributes QLenVar and QLenSCV, order 3
17% contributes QLenM3 and QLenSkew.
18%
19% Why this is a separate table from getMomentTable. The moments beyond the
20% second are generated by differentiating with respect to x_i, the reciprocal
21% of the capacity of station i, which scales the service times of ALL classes
22% at that station at once. That parameter therefore produces moments of the
23% station total, not of any one class: there is no per-class third moment to
24% report, and inventing one by splitting the total would be fiction. The
25% per-class second moments, which do exist, are in getMomentTable. The two are
26% consistent: Var[Q_i] here equals the sum of getMomentTable's per-class
27% covariances at station i over all class pairs.
28%
29% The ALGORITHM is chosen by the solver's method, set at construction, not by
30% an argument here: a method is a property of the solver object, so passing one
31% per call would let a single solver answer with two different algorithms.
32% SolverMVA(model) -> pfqn_sens_mom, exact, but it walks the
33% whole population lattice at a cost of
34% prod(N+1), so it is unusable once the
35% populations are large.
36% SolverMVA(model,'method','lin') -> pfqn_sens_linearizer, approximate and
37% polynomial-time. The reference reports
38% relative errors below 2.1% on E[Q],
39% 4.1% on E[Q^2] and 6.2% on E[Q^3].
40% Any Linearizer-family method ('lin', 'amva.lin', 'egflin', 'gflin') takes the
41% approximate path; every other method takes the exact one.
42%
43% Restricted to closed models. Mixed and open second moments are available per
44% class from getMomentTable; the higher moments of the reference are stated for
45% closed networks only.
46%
47% MOM is the underlying pfqn_sens_mom / pfqn_sens_linearizer struct, which also
48% carries the cross-station covariance matrix .Cov that this table does not
49% show.
50%
51% Reference: J. C. Strelen, "Moment Analysis for Closed Queuing Networks and
52% its Linearizer", Performance Evaluation 11:127-142, 1990, Theorem 3.1 and
53% equation (3.2).
54%
55% See also: getMomentTable, getAvgTable, getSensitivityTable.
56
57if nargin < 2 || isempty(order)
58 order = 2;
59end
60order = validateMomentOrder(order, 3);
61% The algorithm is a property of the solver, not of this call: it comes from
62% the method set at construction, e.g. SolverMVA(model,'method','lin'). Taking
63% a per-call method argument here would have given the same solver object two
64% disagreeing methods.
65method = self.getOptions.method;
66
67sn = self.model.getStruct();
68R = sn.nclasses;
69N = sn.njobs;
70
71[~, D, Np, Z, ~, Ssrv, ~] = sn_get_product_form_params(sn);
72queueIndices = find(sn.nodetype == NodeType.Queue);
73Mq = numel(queueIndices);
74Ztot = sum(Z, 1);
75
76% The moments rest on the product-form identity Cov = L dQ/dL, which is a
77% theorem about the product form: it follows from a d/da log G = E[n], which
78% needs the state distribution to be exponential-family in the demands. Outside
79% product form, L dQ/dL remains computable and is simply NOT a covariance, so
80% differentiating a non-product-form solver would return a confident wrong
81% number. Refuse rather than do that; exact moments of such a model come from
82% SolverCTMC's stationary distribution, or from LDES via setReward.
83if ~sn_has_product_form(sn)
84 line_error(mfilename, 'getMomentStationTable requires a product-form model: the moment identity Cov[n,n] = L dQ/dL holds only under product form, so no correct value exists here. Use SolverCTMC (exact distribution) or SolverLDES with setReward for the moments of a non-product-form model.');
85end
86if any(isinf(N))
87 line_error(mfilename, 'getMomentStationTable supports closed models only. Per-class second moments of an open or mixed model are available from getMomentTable.');
88end
89if any(Ssrv > 1)
90 line_error(mfilename, 'getMomentStationTable supports single-server stations only: the higher-moment recursion of the reference is stated for load-independent stations. Per-class second moments of a multiserver model are available from getMomentTable.');
91end
92
93% Two algorithms exist for these moments, so the solver's method selects
94% between them: any Linearizer-family method ('lin', 'amva.lin', 'egflin',
95% 'gflin') takes the approximate polynomial-time path, and every other method
96% takes the exact one. The solver has already rejected methods it does not
97% support, so no further validation is needed here.
98if isLinearizerMethod(method)
99 mom = pfqn_sens_linearizer(D, Np, Ztot);
100elseif isExactMvaMethod(method)
101 mom = pfqn_sens_mom(D, Np, Ztot);
102else
103 % An approximate method with no hand-differentiated counterpart. The
104 % identity does not care HOW the mean queue lengths were obtained, so the
105 % solver itself is used as a mean-value oracle and differentiated
106 % numerically. This is exactly the move the Linearizer makes analytically
107 % (Strelen Sec. 5: differentiate the approximate fixed point, then apply the
108 % exact identity), generalized to any product-form method. The moments
109 % inherit the accuracy of that method's means.
110 mom = momentsByFiniteDifference(self, sn, queueIndices);
111end
112
113Station = {};
114QLen = []; QLenVar = []; QLenSCV = []; QLenM3 = []; QLenSkew = [];
115for ist = 1:Mq
116 if all(D(ist, :) <= 0)
117 continue; % no class visits this station
118 end
119 Station{end+1, 1} = sn.nodenames{queueIndices(ist)}; %#ok<AGROW>
120 QLen(end+1, 1) = mom.m(ist); %#ok<AGROW>
121 QLenVar(end+1, 1) = mom.Var(ist); %#ok<AGROW>
122 if mom.m(ist) > 0
123 QLenSCV(end+1, 1) = mom.Var(ist) / mom.m(ist)^2; %#ok<AGROW>
124 else
125 QLenSCV(end+1, 1) = NaN; %#ok<AGROW>
126 end
127 QLenM3(end+1, 1) = mom.M3(ist); %#ok<AGROW>
128 QLenSkew(end+1, 1) = mom.Skew(ist); %#ok<AGROW>
129end
130
131vars = {Station};
132names = {'Station'};
133if any(order == 1)
134 vars{end+1} = QLen; names{end+1} = 'QLen';
135end
136if any(order == 2)
137 vars{end+1} = QLenVar; names{end+1} = 'QLenVar';
138 vars{end+1} = QLenSCV; names{end+1} = 'QLenSCV';
139end
140if any(order == 3)
141 vars{end+1} = QLenM3; names{end+1} = 'QLenM3';
142 vars{end+1} = QLenSkew; names{end+1} = 'QLenSkew';
143end
144MomentStationTable = table(vars{:}, 'VariableNames', names);
145end
146
147% =========================================================================
148function tf = isExactMvaMethod(method)
149% Methods whose means are the exact MVA recursion, so pfqn_sens_mom's analytic
150% derivatives apply directly.
151tf = any(strcmpi(method, {'default', 'mva', 'exact'}));
152end
153
154% =========================================================================
155function mom = momentsByFiniteDifference(self, sn, queueIndices)
156% Moments of the per-station totals from ANY solver and method, by central
157% differences of that method's OWN mean queue lengths.
158%
159% The identity does not care how the means were obtained, so the solver's own
160% method is used as a mean-value oracle. The parameter is y_i, a scaling of
161% station i's whole demand column, which is Strelen's x_i; at y = 1 the
162% y-derivatives are the scaled x-derivatives that (3.2) asks for. Because
163% D(i,r) = visits(i,r)/rate(i,r), scaling station i's rates by 1/f scales its
164% whole demand column by f, which is exactly the column perturbation wanted.
165% NetworkStruct is a plain struct (see lang/NetworkStruct.m, a function, not a
166% classdef), so copying it to perturb the rates cannot disturb the caller's sn.
167%
168% The oracle re-runs THIS solver, so every method of every product-form solver
169% is covered, including the normalizing-constant methods of SolverNC (comom, ca,
170% le, ...), which no hand-differentiated implementation reaches.
171%
172% Cost: 2*M extra solves. Accuracy: the moments inherit the accuracy of the
173% method's means, and the second derivative inherits the usual h^2 truncation.
174M = numel(queueIndices);
175h = 1e-4;
176qst = sn.nodeToStation(queueIndices);
177m0 = solveTotals(self, sn, qst);
178dm = zeros(M, M); d2m = zeros(M, 1);
179for hcol = 1:M
180 mp = solveTotals(self, scaleDemandColumn(sn, qst(hcol), 1+h), qst);
181 mm = solveTotals(self, scaleDemandColumn(sn, qst(hcol), 1-h), qst);
182 dm(:, hcol) = (mp - mm) / (2*h);
183 d2m(hcol) = (mp(hcol) - 2*m0(hcol) + mm(hcol)) / h^2;
184end
185mom = packFiniteDifference(m0, dm, d2m);
186end
187
188% =========================================================================
189function sn2 = scaleDemandColumn(sn, station, factor)
190% Scale one station's whole demand column by FACTOR, via its service rates.
191sn2 = sn;
192sn2.rates(station, :) = sn.rates(station, :) / factor;
193end
194
195% =========================================================================
196function m = solveTotals(self, sn, qst)
197% Station totals under the solver's OWN method, whatever that is.
198QN = solveMeansForStruct(self, sn);
199m = sum(QN(qst, :), 2);
200end
201
202% =========================================================================
203function mom = packFiniteDifference(m, dm, d2m)
204% (3.2), applied to numerically obtained derivatives.
205M = numel(m);
206mom.m = m;
207mom.dm = dm;
208mom.d2m = d2m;
209Cov = (dm + dm.') / 2;
210mom.CovAsym = max(max(abs(dm - dm.')));
211mom.Cov = Cov;
212Var = zeros(M,1); M2 = zeros(M,1); M3 = zeros(M,1); Skew = zeros(M,1);
213for i = 1:M
214 Var(i) = dm(i,i);
215 M2(i) = dm(i,i) + m(i)^2;
216 M3(i) = d2m(i) + (1 + 3*m(i))*dm(i,i) + m(i)^3;
217 mu3 = M3(i) - 3*m(i)*M2(i) + 2*m(i)^3;
218 if Var(i) > 0
219 Skew(i) = mu3 / Var(i)^1.5;
220 else
221 Skew(i) = NaN;
222 end
223end
224mom.Var = Var; mom.M2 = M2; mom.M3 = M3; mom.Skew = Skew;
225end
226
227% =========================================================================
228function tf = isLinearizerMethod(method)
229% True for the Linearizer family of solver methods. Everything else maps to the
230% exact recursion: the moment analysis has only these two algorithms, and the
231% exact one is the right default for a method with no approximate counterpart.
232tf = any(strcmpi(method, {'lin', 'amva.lin', 'egflin', 'gflin'}));
233end
234
235% =========================================================================
236function order = validateMomentOrder(order, maxorder)
237% ORDER is a set of moment orders. A scalar k is shorthand for 1:k, so that
238% getMomentStationTable(2) means "up to the second moment" and not "the second
239% moment alone"; a vector of two or more entries is taken literally.
240%
241% Consequence of MATLAB's isscalar: a one-element vector IS a scalar, so [2]
242% takes the 1:k path and yields [1 2]. "The second moment alone" is therefore
243% not expressible, which is deliberate: a variance with no mean beside it is not
244% a useful table, and [2 3] remains available for the higher orders.
245%
246% Non-integers are rejected on BOTH paths. Rounding them silently would accept
247% [1 2.5] as [1 3], i.e. answer a question that was not asked.
248if ~isnumeric(order) || isempty(order) || any(~isfinite(order(:)))
249 line_error(mfilename, sprintf('order must be an integer in 1..%d, or a vector of such integers.', maxorder));
250end
251if any(order(:) ~= round(order(:))) || any(order(:) < 1) || any(order(:) > maxorder)
252 line_error(mfilename, sprintf('order must be an integer in 1..%d, or a vector of such integers.', maxorder));
253end
254if isscalar(order)
255 order = 1:order;
256 return;
257end
258order = unique(order(:)');
259end
Definition Station.m:245