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% see _kb/06-solver-catalog.md for rationale (method fixed at construction)
62method = self.getOptions.method;
63
64sn = self.model.getStruct();
65R = sn.nclasses;
66N = sn.njobs;
67
68[~, D, Np, Z, ~, Ssrv, ~] = sn_get_product_form_params(sn);
69queueIndices = find(sn.nodetype == NodeType.Queue);
70Mq = numel(queueIndices);
71Ztot = sum(Z, 1);
72
73% see _kb/06-solver-catalog.md for rationale (product-form identity Cov=L dQ/dL)
74if ~sn_has_product_form(sn)
75 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.');
76end
77if any(isinf(N))
78 line_error(mfilename, 'getMomentStationTable supports closed models only. Per-class second moments of an open or mixed model are available from getMomentTable.');
79end
80if any(Ssrv > 1)
81 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.');
82end
83
84% see _kb/06-solver-catalog.md for rationale (Linearizer vs exact vs FD oracle)
85if isLinearizerMethod(method)
86 mom = pfqn_sens_linearizer(D, Np, Ztot);
87elseif isExactMvaMethod(method)
88 mom = pfqn_sens_mom(D, Np, Ztot);
89else
90 % Finite-difference oracle: differentiate the solver's own means.
91 % see _kb/06-solver-catalog.md for rationale
92 mom = momentsByFiniteDifference(self, sn, queueIndices);
93end
94
95Station = {};
96QLen = []; QLenVar = []; QLenSCV = []; QLenM3 = []; QLenSkew = [];
97for ist = 1:Mq
98 if all(D(ist, :) <= 0)
99 continue; % no class visits this station
100 end
101 Station{end+1, 1} = sn.nodenames{queueIndices(ist)}; %#ok<AGROW>
102 QLen(end+1, 1) = mom.m(ist); %#ok<AGROW>
103 QLenVar(end+1, 1) = mom.Var(ist); %#ok<AGROW>
104 if mom.m(ist) > 0
105 QLenSCV(end+1, 1) = mom.Var(ist) / mom.m(ist)^2; %#ok<AGROW>
106 else
107 QLenSCV(end+1, 1) = NaN; %#ok<AGROW>
108 end
109 QLenM3(end+1, 1) = mom.M3(ist); %#ok<AGROW>
110 QLenSkew(end+1, 1) = mom.Skew(ist); %#ok<AGROW>
111end
112
113vars = {Station};
114names = {'Station'};
115if any(order == 1)
116 vars{end+1} = QLen; names{end+1} = 'QLen';
117end
118if any(order == 2)
119 vars{end+1} = QLenVar; names{end+1} = 'QLenVar';
120 vars{end+1} = QLenSCV; names{end+1} = 'QLenSCV';
121end
122if any(order == 3)
123 vars{end+1} = QLenM3; names{end+1} = 'QLenM3';
124 vars{end+1} = QLenSkew; names{end+1} = 'QLenSkew';
125end
126MomentStationTable = table(vars{:}, 'VariableNames', names);
127end
128
129% =========================================================================
130function tf = isExactMvaMethod(method)
131% Methods whose means are the exact MVA recursion, so pfqn_sens_mom's analytic
132% derivatives apply directly.
133tf = any(strcmpi(method, {'default', 'mva', 'exact'}));
134end
135
136% =========================================================================
137function mom = momentsByFiniteDifference(self, sn, queueIndices)
138% Moments of the per-station totals from ANY solver and method, by central
139% differences of that method's OWN mean queue lengths.
140%
141% The identity does not care how the means were obtained, so the solver's own
142% method is used as a mean-value oracle. The parameter is y_i, a scaling of
143% station i's whole demand column, which is Strelen's x_i; at y = 1 the
144% y-derivatives are the scaled x-derivatives that (3.2) asks for. Because
145% D(i,r) = visits(i,r)/rate(i,r), scaling station i's rates by 1/f scales its
146% whole demand column by f, which is exactly the column perturbation wanted.
147% NetworkStruct is a plain struct (see lang/NetworkStruct.m, a function, not a
148% classdef), so copying it to perturb the rates cannot disturb the caller's sn.
149%
150% The oracle re-runs THIS solver, so every method of every product-form solver
151% is covered, including the normalizing-constant methods of SolverNC (comom, ca,
152% le, ...), which no hand-differentiated implementation reaches.
153%
154% Cost: 2*M extra solves. Accuracy: the moments inherit the accuracy of the
155% method's means, and the second derivative inherits the usual h^2 truncation.
156M = numel(queueIndices);
157h = 1e-4;
158qst = sn.nodeToStation(queueIndices);
159m0 = solveTotals(self, sn, qst);
160dm = zeros(M, M); d2m = zeros(M, 1);
161for hcol = 1:M
162 mp = solveTotals(self, scaleDemandColumn(sn, qst(hcol), 1+h), qst);
163 mm = solveTotals(self, scaleDemandColumn(sn, qst(hcol), 1-h), qst);
164 dm(:, hcol) = (mp - mm) / (2*h);
165 d2m(hcol) = (mp(hcol) - 2*m0(hcol) + mm(hcol)) / h^2;
166end
167mom = packFiniteDifference(m0, dm, d2m);
168end
169
170% =========================================================================
171function sn2 = scaleDemandColumn(sn, station, factor)
172% Scale one station's whole demand column by FACTOR, via its service rates.
173sn2 = sn;
174sn2.rates(station, :) = sn.rates(station, :) / factor;
175end
176
177% =========================================================================
178function m = solveTotals(self, sn, qst)
179% Station totals under the solver's OWN method, whatever that is.
180QN = solveMeansForStruct(self, sn);
181m = sum(QN(qst, :), 2);
182end
183
184% =========================================================================
185function mom = packFiniteDifference(m, dm, d2m)
186% (3.2), applied to numerically obtained derivatives.
187M = numel(m);
188mom.m = m;
189mom.dm = dm;
190mom.d2m = d2m;
191Cov = (dm + dm.') / 2;
192mom.CovAsym = max(max(abs(dm - dm.')));
193mom.Cov = Cov;
194Var = zeros(M,1); M2 = zeros(M,1); M3 = zeros(M,1); Skew = zeros(M,1);
195for i = 1:M
196 Var(i) = dm(i,i);
197 M2(i) = dm(i,i) + m(i)^2;
198 M3(i) = d2m(i) + (1 + 3*m(i))*dm(i,i) + m(i)^3;
199 mu3 = M3(i) - 3*m(i)*M2(i) + 2*m(i)^3;
200 if Var(i) > 0
201 Skew(i) = mu3 / Var(i)^1.5;
202 else
203 Skew(i) = NaN;
204 end
205end
206mom.Var = Var; mom.M2 = M2; mom.M3 = M3; mom.Skew = Skew;
207end
208
209% =========================================================================
210function tf = isLinearizerMethod(method)
211% True for the Linearizer family of solver methods. Everything else maps to the
212% exact recursion: the moment analysis has only these two algorithms, and the
213% exact one is the right default for a method with no approximate counterpart.
214tf = any(strcmpi(method, {'lin', 'amva.lin', 'egflin', 'gflin'}));
215end
216
217% =========================================================================
218function order = validateMomentOrder(order, maxorder)
219% ORDER is a set of moment orders. A scalar k is shorthand for 1:k, so that
220% getMomentStationTable(2) means "up to the second moment" and not "the second
221% moment alone"; a vector of two or more entries is taken literally.
222%
223% Consequence of MATLAB's isscalar: a one-element vector IS a scalar, so [2]
224% takes the 1:k path and yields [1 2]. "The second moment alone" is therefore
225% not expressible, which is deliberate: a variance with no mean beside it is not
226% a useful table, and [2 3] remains available for the higher orders.
227%
228% Non-integers are rejected on BOTH paths. Rounding them silently would accept
229% [1 2.5] as [1 3], i.e. answer a question that was not asked.
230if ~isnumeric(order) || isempty(order) || any(~isfinite(order(:)))
231 line_error(mfilename, sprintf('order must be an integer in 1..%d, or a vector of such integers.', maxorder));
232end
233if any(order(:) ~= round(order(:))) || any(order(:) < 1) || any(order(:) > maxorder)
234 line_error(mfilename, sprintf('order must be an integer in 1..%d, or a vector of such integers.', maxorder));
235end
236if isscalar(order)
237 order = 1:order;
238 return;
239end
240order = unique(order(:)');
241end