1function [MomentChainTable, mom] = getMomentChainTable(self, order)
2% GETMOMENTCHAINTABLE Exact higher moments of the per-chain queue length.
4% [MOMENTCHAINTABLE, MOM] = GETMOMENTCHAINTABLE(SELF) returns a table with one
5% row per (
Station, Chain) giving the moments of the queue length of that
6% chain at that station, Q_(i,c) =
sum_(r in chain c) n(i,r):
7% QLen, QLenVar, QLenSCV
9% This
is the chain-level analogue of getAvgChainTable, and it sits between the
10% two other moment tables: getMomentTable
is per class, getMomentStationTable
11%
is per station total, and this one
is per chain, i.e. per group of classes
12% that circulate together.
14% [..] = GETMOMENTCHAINTABLE(SELF, ORDER) selects which moment orders to
15% report. ORDER
is a set: a scalar k
is read as 1:k,
"everything up to order
16% k"; an
explicit vector selects exactly those orders.
17% 1 the mean only: QLen
18% 2 (
default) mean and second moment: QLen, QLenVar, QLenSCV
19% 3 also adds QLenM3 and QLenSkew
21% Unlike the per-class table, order 3 IS available here. All three tables are
22% the same recursion under different groupings of the classes: the generating
23% parameter scales the service times of a class subset T at a station, and the
24% moments it produces are those of
sum_(r in T) n(i,r). T = {r} gives
25% getMomentTable, T = chain gives
this table, T = all classes gives
26% getMomentStationTable. That
is Theorem 1 of Akyildiz and Strelen; Strelen
's
27% own x_i is the last case.
29% The ALGORITHM is chosen by the solver's method, set at construction, not by
30% an argument here; see getMomentStationTable. A Linearizer-family method
31% approximates the per-station totals only, so it cannot express a per-chain
32% grouping and
is rejected here unless every
class already sits in one chain, in
33% which
case the chain IS the station total.
35% Restricted to closed, single-server models, which
is the scope of
38% MOM
is the underlying pfqn_sens_mom
struct. Its .Cov
is (M x C x M x C) and
39% carries the cross-chain and cross-station covariances
this table does not
42% Reference: I. F. Akyildiz and J. C. Strelen,
"Moment Analysis for
43% Load-Dependent Mixed Product Form Queueing Networks", IEEE Trans.
44% Communications 39(6):828-832, 1991, Theorem 1; J. C. Strelen,
"Moment
45% Analysis for Closed Queuing Networks and its Linearizer", Performance
46% Evaluation 11:127-142, 1990, equation (3.2).
48% See also: getMomentTable, getMomentStationTable, getAvgChainTable.
50if nargin < 2 || isempty(order)
53order = validateMomentOrder(order, 3);
54% see _kb/06-solver-catalog.md
for rationale (method fixed at construction)
55method = self.getOptions.method;
57sn = self.model.getStruct();
61[~, D, Np, Z, ~, Ssrv, ~] = sn_get_product_form_params(sn);
62queueIndices = find(sn.nodetype == NodeType.Queue);
63Mq = numel(queueIndices);
66% see _kb/06-solver-catalog.md
for rationale (product-
form identity Cov=L dQ/dL)
67if ~sn_has_product_form(sn)
68 line_error(mfilename,
'getMomentChainTable 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.');
71 line_error(mfilename,
'getMomentChainTable supports closed models only. Per-class second moments of an open or mixed model are available from getMomentTable.');
74 line_error(mfilename, 'getMomentChainTable supports single-server stations only: the higher-moment recursion of the reference
is stated for load-independent stations.');
77% the chain of each class; sn.chains
is (nchains x nclasses)
80 c = find(sn.chains(:, r), 1);
82 line_error(mfilename, sprintf('class %s belongs to no chain', sn.classnames{r}));
86% pfqn_sens_mom
requires the group labels to be consecutive from 1, so drop any
87% chain that holds no
class rather than leaving a hole in the numbering
88[used, ~, compact] = unique(groups);
92% see _kb/06-solver-catalog.md for rationale (Linearizer cannot express per-chain grouping)
93if isLinearizerMethod(method)
95 line_error(mfilename, sprintf('the solver method
''%s
'' approximates the per-station totals, so it cannot produce a per-chain grouping of %d chains. Use an exact method, or getMomentStationTable.
', method, Cg));
97 mom = pfqn_sens_linearizer(D, Np, Ztot);
98elseif isExactMvaMethod(method)
99 mom = pfqn_sens_mom(D, Np, Ztot, ones(1,Mq), groups);
101 % Finite-difference oracle over one chain's classes (Akyildiz-Strelen Thm 1
102 % class subset T). see _kb/06-solver-catalog.md for rationale
103 mom = chainMomentsByFiniteDifference(self, sn, queueIndices, groups, Cg);
107QLen = []; QLenVar = []; QLenSCV = []; QLenM3 = []; QLenSkew = [];
110 classesOf = find(groups == g);
111 if all(all(D(ist, classesOf) <= 0))
112 continue; % no class of this chain
visits this station
114 Station{end+1, 1} = sn.nodenames{queueIndices(ist)}; %#ok<AGROW>
115 Chain{end+1, 1} = sprintf(
'Chain%d', used(g)); %#ok<AGROW>
116 QLen(end+1, 1) = mom.m(ist, g); %#ok<AGROW>
117 QLenVar(end+1, 1) = mom.Var(ist, g); %#ok<AGROW>
119 QLenSCV(end+1, 1) = mom.Var(ist, g) / mom.m(ist, g)^2; %#ok<AGROW>
121 QLenSCV(end+1, 1) = NaN; %#ok<AGROW>
123 QLenM3(end+1, 1) = mom.M3(ist, g); %#ok<AGROW>
124 QLenSkew(end+1, 1) = mom.Skew(ist, g); %#ok<AGROW>
129names = {
'Station',
'Chain'};
131 vars{end+1} = QLen; names{end+1} =
'QLen';
134 vars{end+1} = QLenVar; names{end+1} =
'QLenVar';
135 vars{end+1} = QLenSCV; names{end+1} =
'QLenSCV';
138 vars{end+1} = QLenM3; names{end+1} =
'QLenM3';
139 vars{end+1} = QLenSkew; names{end+1} =
'QLenSkew';
141MomentChainTable = table(vars{:},
'VariableNames', names);
144% =========================================================================
145function tf = isExactMvaMethod(method)
146% Methods whose means are the exact MVA recursion; see getMomentStationTable.
147tf = any(strcmpi(method, {
'default',
'mva',
'exact'}));
150% =========================================================================
151function mom = chainMomentsByFiniteDifference(self, sn, queueIndices, groups, Cg)
152% Per-chain moments from ANY solver and method, by central differences of that
153% method
's OWN mean queue lengths; see solveMeansForStruct.
155% The parameter y_(i,g) scales the demands of group g's classes at station i,
156% which
is the
class-subset parameter T of Akyildiz-Strelen Theorem 1; the
157% moments it generates are those of Q_(i,g) =
sum_(r in g) n(i,r). Since
158% D(i,r) =
visits(i,r)/rate(i,r), the perturbation
is applied to the rates of
159% that group
's classes at that station alone, leaving the other classes' demands
160% at the same station untouched. That per-
class granularity
is what separates
161%
this from getMomentStationTable, which scales a whole
column.
162M = numel(queueIndices);
164qst = sn.nodeToStation(queueIndices);
170 p = p + 1; pidx(i,g) = p;
173m0 = solveGroupTotals(self, sn, qst, groups, Cg);
174dm = zeros(M,Cg,M,Cg); d2m = zeros(M,Cg);
177 snp = scaleGroupDemands(sn, qst(hi), groups, hg, 1+h);
178 snm = scaleGroupDemands(sn, qst(hi), groups, hg, 1-h);
179 mp = solveGroupTotals(self, snp, qst, groups, Cg);
180 mm = solveGroupTotals(self, snm, qst, groups, Cg);
183 dm(i,g,hi,hg) = (mp(i,g) - mm(i,g)) / (2*h);
186 d2m(hi,hg) = (mp(hi,hg) - 2*m0(hi,hg) + mm(hi,hg)) / h^2;
189mom = packChainFiniteDifference(m0, dm, d2m, Cg);
192% =========================================================================
193function sn2 = scaleGroupDemands(sn, station, groups, g,
factor)
194% Scale the demands of group g
's classes at one station by FACTOR, via rates.
196cls = find(groups == g);
197sn2.rates(station, cls) = sn.rates(station, cls) / factor;
200% =========================================================================
201function mg = solveGroupTotals(self, sn, qst, groups, Cg)
202% Per-(station,group) mean queue lengths under the solver's own method.
203QN = solveMeansForStruct(self, sn);
208 mg(i,g) = sum(QN(qst(i), groups == g));
213% =========================================================================
214function mom = packChainFiniteDifference(m, dm, d2m, Cg)
215% (3.2), applied to numerically obtained derivatives, per (station,group).
217mom.m = m; mom.d2m = d2m;
218flat = reshape(dm, M*Cg, M*Cg);
219mom.CovAsym = max(max(abs(flat - flat.
')));
220flat = (flat + flat.')/2;
222 mom.Cov = reshape(flat, M, M); mom.dm = reshape(dm, M, M);
224 mom.Cov = reshape(flat, M, Cg, M, Cg); mom.dm = dm;
226Var = zeros(M,Cg); M2 = zeros(M,Cg); M3 = zeros(M,Cg); Skew = zeros(M,Cg);
231 M2(i,g) = d1 + m(i,g)^2;
232 M3(i,g) = d2m(i,g) + (1 + 3*m(i,g))*d1 + m(i,g)^3;
233 mu3 = M3(i,g) - 3*m(i,g)*M2(i,g) + 2*m(i,g)^3;
235 Skew(i,g) = mu3 / Var(i,g)^1.5;
241mom.Var = Var; mom.M2 = M2; mom.M3 = M3; mom.Skew = Skew;
244% =========================================================================
245function tf = isLinearizerMethod(method)
246% True
for the Linearizer family of solver methods; see getMomentStationTable.
247tf = any(strcmpi(method, {
'lin',
'amva.lin',
'egflin',
'gflin'}));
250% =========================================================================
251function order = validateMomentOrder(order, maxorder)
252% ORDER
is a set of moment orders. A scalar k
is shorthand
for 1:k, so that
253% getMomentChainTable(2) means "up to the second moment" and not "the second
254% moment alone"; a vector of two or more entries
is taken literally.
256% Consequence of MATLAB's isscalar: a one-element vector IS a scalar, so [2]
257% takes the 1:k path and yields [1 2]. "The second moment alone"
is therefore
258% not expressible, which
is deliberate: a variance with no mean beside it
is not
259% a useful table, and [2 3] remains available for the higher orders.
261% Non-integers are rejected on BOTH paths. Rounding them silently would accept
262% [1 2.5] as [1 3], i.e. answer a question that was not asked.
263if ~isnumeric(order) || isempty(order) || any(~isfinite(order(:)))
264 line_error(mfilename, sprintf('order must be an integer in 1..%d, or a vector of such integers.', maxorder));
266if any(order(:) ~= round(order(:))) || any(order(:) < 1) || any(order(:) > maxorder)
267 line_error(mfilename, sprintf('order must be an integer in 1..%d, or a vector of such integers.', maxorder));
273order = unique(order(:)');