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% see _kb/06-solver-catalog.md
for rationale (method fixed at construction)
62method = self.getOptions.method;
64sn = self.model.getStruct();
68[~, D, Np, Z, ~, Ssrv, ~] = sn_get_product_form_params(sn);
69queueIndices = find(sn.nodetype == NodeType.Queue);
70Mq = numel(queueIndices);
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.');
78 line_error(mfilename,
'getMomentStationTable supports closed models only. Per-class second moments of an open or mixed model are available from getMomentTable.');
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.');
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);
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);
96QLen = []; QLenVar = []; QLenSCV = []; QLenM3 = []; QLenSkew = [];
98 if all(D(ist, :) <= 0)
99 continue; % no
class visits this station
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>
105 QLenSCV(end+1, 1) = mom.Var(ist) / mom.m(ist)^2; %#ok<AGROW>
107 QLenSCV(end+1, 1) = NaN; %#ok<AGROW>
109 QLenM3(end+1, 1) = mom.M3(ist); %#ok<AGROW>
110 QLenSkew(end+1, 1) = mom.Skew(ist); %#ok<AGROW>
116 vars{end+1} = QLen; names{end+1} =
'QLen';
119 vars{end+1} = QLenVar; names{end+1} =
'QLenVar';
120 vars{end+1} = QLenSCV; names{end+1} =
'QLenSCV';
123 vars{end+1} = QLenM3; names{end+1} =
'QLenM3';
124 vars{end+1} = QLenSkew; names{end+1} =
'QLenSkew';
126MomentStationTable = table(vars{:},
'VariableNames', names);
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
'}));
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.
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.
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.
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);
158qst = sn.nodeToStation(queueIndices);
159m0 = solveTotals(self, sn, qst);
160dm = zeros(M, M); d2m = zeros(M, 1);
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;
167mom = packFiniteDifference(m0, dm, d2m);
170% =========================================================================
171function sn2 = scaleDemandColumn(sn, station,
factor)
172% Scale one station
's whole demand column by FACTOR, via its service rates.
174sn2.rates(station, :) = sn.rates(station, :) / factor;
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);
184% =========================================================================
185function mom = packFiniteDifference(m, dm, d2m)
186% (3.2), applied to numerically obtained derivatives.
191Cov = (dm + dm.
') / 2;
192mom.CovAsym = max(max(abs(dm - dm.')));
194Var = zeros(M,1); M2 = zeros(M,1); M3 = zeros(M,1); Skew = zeros(M,1);
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;
201 Skew(i) = mu3 / Var(i)^1.5;
206mom.Var = Var; mom.M2 = M2; mom.M3 = M3; mom.Skew = Skew;
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'}));
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.
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.
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));
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));
240order = unique(order(:)');