1function [MomentStationTable, mom] = getMomentStationTable(self, order)
2% GETMOMENTSTATIONTABLE Exact higher moments of
the total queue length.
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
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.
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.
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.
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.
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
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
55% See also: getMomentTable, getAvgTable, getSensitivityTable.
57if nargin < 2 || isempty(order)
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
65method = self.getOptions.method;
67sn = self.model.getStruct();
71[~, D, Np, Z, ~, Ssrv, ~] = sn_get_product_form_params(sn);
72queueIndices = find(sn.nodetype == NodeType.Queue);
73Mq = numel(queueIndices);
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.
');
87 line_error(mfilename, 'getMomentStationTable supports closed models only. Per-
class second moments of an open or mixed model are available from getMomentTable.');
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.');
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);
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);
114QLen = []; QLenVar = []; QLenSCV = []; QLenM3 = []; QLenSkew = [];
116 if all(D(ist, :) <= 0)
117 continue; % no
class visits this station
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>
123 QLenSCV(end+1, 1) = mom.Var(ist) / mom.m(ist)^2; %#ok<AGROW>
125 QLenSCV(end+1, 1) = NaN; %#ok<AGROW>
127 QLenM3(end+1, 1) = mom.M3(ist); %#ok<AGROW>
128 QLenSkew(end+1, 1) = mom.Skew(ist); %#ok<AGROW>
134 vars{end+1} = QLen; names{end+1} =
'QLen';
137 vars{end+1} = QLenVar; names{end+1} =
'QLenVar';
138 vars{end+1} = QLenSCV; names{end+1} =
'QLenSCV';
141 vars{end+1} = QLenM3; names{end+1} =
'QLenM3';
142 vars{end+1} = QLenSkew; names{end+1} =
'QLenSkew';
144MomentStationTable = table(vars{:},
'VariableNames', names);
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
'}));
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.
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
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.
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.
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);
176qst = sn.nodeToStation(queueIndices);
177m0 = solveTotals(self, sn, qst);
178dm = zeros(M, M); d2m = zeros(M, 1);
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;
185mom = packFiniteDifference(m0, dm, d2m);
188% =========================================================================
189function sn2 = scaleDemandColumn(sn, station,
factor)
190% Scale one station
's whole demand column by FACTOR, via its service rates.
192sn2.rates(station, :) = sn.rates(station, :) / factor;
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);
202% =========================================================================
203function mom = packFiniteDifference(m, dm, d2m)
204% (3.2), applied to numerically obtained derivatives.
209Cov = (dm + dm.
') / 2;
210mom.CovAsym = max(max(abs(dm - dm.')));
212Var = zeros(M,1); M2 = zeros(M,1); M3 = zeros(M,1); Skew = zeros(M,1);
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;
219 Skew(i) = mu3 / Var(i)^1.5;
224mom.Var = Var; mom.M2 = M2; mom.M3 = M3; mom.Skew = Skew;
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'}));
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.
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.
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));
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));
258order = unique(order(:)');