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% The algorithm
is a
property of
the solver, not of
this call: it comes from
55%
the method set at construction, e.g. SolverMVA(model,
'method',
'lin'). Taking
56% a per-call method argument here would have given
the same solver
object two
58method = self.getOptions.method;
60sn = self.model.getStruct();
64[~, D, Np, Z, ~, Ssrv, ~] = sn_get_product_form_params(sn);
65queueIndices = find(sn.nodetype == NodeType.Queue);
66Mq = numel(queueIndices);
69% See getMomentStationTable:
the moment identity Cov = L dQ/dL
is a theorem
70% about
the product form, so outside it L dQ/dL
is not a covariance and no
71% correct value exists to
return.
72if ~sn_has_product_form(sn)
73 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.');
76 line_error(mfilename,
'getMomentChainTable supports closed models only. Per-class second moments of an open or mixed model are available from getMomentTable.');
79 line_error(mfilename, 'getMomentChainTable supports single-server stations only:
the higher-moment recursion of
the reference
is stated for load-independent stations.');
82%
the chain of each class; sn.chains
is (nchains x nclasses)
85 c = find(sn.chains(:, r), 1);
87 line_error(mfilename, sprintf('class %s belongs to no chain', sn.classnames{r}));
91% pfqn_sens_mom
requires the group labels to be consecutive from 1, so drop any
92% chain that holds no
class rather than leaving a hole in
the numbering
93[used, ~, compact] = unique(groups);
97% See getMomentStationTable: the solver's method selects
the algorithm. The
98% Linearizer approximates
the per-station totals only, so it cannot express a
99% per-chain grouping unless every class already sits in one chain, in which case
100%
the chain IS
the station total.
101if isLinearizerMethod(method)
103 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));
105 mom = pfqn_sens_linearizer(D, Np, Ztot);
106elseif isExactMvaMethod(method)
107 mom = pfqn_sens_mom(D, Np, Ztot, ones(1,Mq), groups);
109 % See getMomentStationTable:
the solver
's own method supplies the means and
110 % the derivatives are taken numerically. Here the parameter scales the
111 % demands of one CHAIN's classes at one station, which
is the class subset
112 % T of Akyildiz-Strelen Theorem 1, so
the moments it generates are those of
113 % that chain
's queue length.
114 mom = chainMomentsByFiniteDifference(self, sn, queueIndices, groups, Cg);
117Station = {}; Chain = {};
118QLen = []; QLenVar = []; QLenSCV = []; QLenM3 = []; QLenSkew = [];
121 classesOf = find(groups == g);
122 if all(all(D(ist, classesOf) <= 0))
123 continue; % no class of this chain visits this station
125 Station{end+1, 1} = sn.nodenames{queueIndices(ist)}; %#ok<AGROW>
126 Chain{end+1, 1} = sprintf('Chain%d
', used(g)); %#ok<AGROW>
127 QLen(end+1, 1) = mom.m(ist, g); %#ok<AGROW>
128 QLenVar(end+1, 1) = mom.Var(ist, g); %#ok<AGROW>
130 QLenSCV(end+1, 1) = mom.Var(ist, g) / mom.m(ist, g)^2; %#ok<AGROW>
132 QLenSCV(end+1, 1) = NaN; %#ok<AGROW>
134 QLenM3(end+1, 1) = mom.M3(ist, g); %#ok<AGROW>
135 QLenSkew(end+1, 1) = mom.Skew(ist, g); %#ok<AGROW>
139vars = {Station, Chain};
142 vars{end+1} = QLen; names{end+1} = 'QLen
';
145 vars{end+1} = QLenVar; names{end+1} = 'QLenVar
';
146 vars{end+1} = QLenSCV; names{end+1} = 'QLenSCV
';
149 vars{end+1} = QLenM3; names{end+1} = 'QLenM3
';
150 vars{end+1} = QLenSkew; names{end+1} = 'QLenSkew
';
152MomentChainTable = table(vars{:}, 'VariableNames
', names);
155% =========================================================================
156function tf = isExactMvaMethod(method)
157% Methods whose means are the exact MVA recursion; see getMomentStationTable.
158tf = any(strcmpi(method, {'default', 'mva
', 'exact
'}));
161% =========================================================================
162function mom = chainMomentsByFiniteDifference(self, sn, queueIndices, groups, Cg)
163% Per-chain moments from ANY solver and method, by central differences of that
164% method's OWN mean queue lengths; see solveMeansForStruct.
166% The parameter y_(i,g) scales
the demands of group g
's classes at station i,
167% which is the class-subset parameter T of Akyildiz-Strelen Theorem 1; the
168% moments it generates are those of Q_(i,g) = sum_(r in g) n(i,r). Since
169% D(i,r) = visits(i,r)/rate(i,r), the perturbation is applied to the rates of
170% that group's classes at that station alone, leaving
the other classes
' demands
171% at the same station untouched. That per-class granularity is what separates
172% this from getMomentStationTable, which scales a whole column.
173M = numel(queueIndices);
175qst = sn.nodeToStation(queueIndices);
181 p = p + 1; pidx(i,g) = p;
184m0 = solveGroupTotals(self, sn, qst, groups, Cg);
185dm = zeros(M,Cg,M,Cg); d2m = zeros(M,Cg);
188 snp = scaleGroupDemands(sn, qst(hi), groups, hg, 1+h);
189 snm = scaleGroupDemands(sn, qst(hi), groups, hg, 1-h);
190 mp = solveGroupTotals(self, snp, qst, groups, Cg);
191 mm = solveGroupTotals(self, snm, qst, groups, Cg);
194 dm(i,g,hi,hg) = (mp(i,g) - mm(i,g)) / (2*h);
197 d2m(hi,hg) = (mp(hi,hg) - 2*m0(hi,hg) + mm(hi,hg)) / h^2;
200mom = packChainFiniteDifference(m0, dm, d2m, Cg);
203% =========================================================================
204function sn2 = scaleGroupDemands(sn, station, groups, g, factor)
205% Scale the demands of group g's classes at one station by FACTOR, via rates.
207cls = find(groups == g);
208sn2.rates(station, cls) = sn.rates(station, cls) /
factor;
211% =========================================================================
212function mg = solveGroupTotals(self, sn, qst, groups, Cg)
213% Per-(station,group) mean queue lengths under
the solver
's own method.
214QN = solveMeansForStruct(self, sn);
219 mg(i,g) = sum(QN(qst(i), groups == g));
224% =========================================================================
225function mom = packChainFiniteDifference(m, dm, d2m, Cg)
226% (3.2), applied to numerically obtained derivatives, per (station,group).
228mom.m = m; mom.d2m = d2m;
229flat = reshape(dm, M*Cg, M*Cg);
230mom.CovAsym = max(max(abs(flat - flat.')));
231flat = (flat + flat.
')/2;
233 mom.Cov = reshape(flat, M, M); mom.dm = reshape(dm, M, M);
235 mom.Cov = reshape(flat, M, Cg, M, Cg); mom.dm = dm;
237Var = zeros(M,Cg); M2 = zeros(M,Cg); M3 = zeros(M,Cg); Skew = zeros(M,Cg);
242 M2(i,g) = d1 + m(i,g)^2;
243 M3(i,g) = d2m(i,g) + (1 + 3*m(i,g))*d1 + m(i,g)^3;
244 mu3 = M3(i,g) - 3*m(i,g)*M2(i,g) + 2*m(i,g)^3;
246 Skew(i,g) = mu3 / Var(i,g)^1.5;
252mom.Var = Var; mom.M2 = M2; mom.M3 = M3; mom.Skew = Skew;
255% =========================================================================
256function tf = isLinearizerMethod(method)
257% True for the Linearizer family of solver methods; see getMomentStationTable.
258tf = any(strcmpi(method, {'lin
', 'amva.lin
', 'egflin
', 'gflin
'}));
261% =========================================================================
262function order = validateMomentOrder(order, maxorder)
263% ORDER is a set of moment orders. A scalar k is shorthand for 1:k, so that
264% getMomentChainTable(2) means "up to the second moment" and not "the second
265% moment alone"; a vector of two or more entries is taken literally.
267% Consequence of MATLAB's isscalar: a one-element vector IS a scalar, so [2]
268% takes
the 1:k path and yields [1 2].
"The second moment alone" is therefore
269% not expressible, which
is deliberate: a variance with no mean beside it
is not
270% a useful table, and [2 3] remains available
for the higher orders.
272% Non-integers are rejected on BOTH paths. Rounding them silently would accept
273% [1 2.5] as [1 3], i.e. answer a question that was not asked.
274if ~isnumeric(order) || isempty(order) || any(~isfinite(order(:)))
275 line_error(mfilename, sprintf(
'order must be an integer in 1..%d, or a vector of such integers.', maxorder));
277if any(order(:) ~= round(order(:))) || any(order(:) < 1) || any(order(:) > maxorder)
278 line_error(mfilename, sprintf(
'order must be an integer in 1..%d, or a vector of such integers.', maxorder));
284order = unique(order(:)
');