LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
getMomentChainTable.m
1function [MomentChainTable, mom] = getMomentChainTable(self, order)
2% GETMOMENTCHAINTABLE Exact higher moments of the per-chain queue length.
3%
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
8%
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.
13%
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
20%
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.
28%
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.
34%
35% Restricted to closed, single-server models, which is the scope of
36% pfqn_sens_mom.
37%
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
40% show.
41%
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).
47%
48% See also: getMomentTable, getMomentStationTable, getAvgChainTable.
49
50if nargin < 2 || isempty(order)
51 order = 2;
52end
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
57% disagreeing methods.
58method = self.getOptions.method;
59
60sn = self.model.getStruct();
61R = sn.nclasses;
62N = sn.njobs;
63
64[~, D, Np, Z, ~, Ssrv, ~] = sn_get_product_form_params(sn);
65queueIndices = find(sn.nodetype == NodeType.Queue);
66Mq = numel(queueIndices);
67Ztot = sum(Z, 1);
68
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.');
74end
75if any(isinf(N))
76 line_error(mfilename, 'getMomentChainTable supports closed models only. Per-class second moments of an open or mixed model are available from getMomentTable.');
77end
78if any(Ssrv > 1)
79 line_error(mfilename, 'getMomentChainTable supports single-server stations only: the higher-moment recursion of the reference is stated for load-independent stations.');
80end
81
82% the chain of each class; sn.chains is (nchains x nclasses)
83groups = zeros(1, R);
84for r = 1:R
85 c = find(sn.chains(:, r), 1);
86 if isempty(c)
87 line_error(mfilename, sprintf('class %s belongs to no chain', sn.classnames{r}));
88 end
89 groups(r) = c;
90end
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);
94groups = compact(:)';
95Cg = numel(used);
96
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)
102 if Cg > 1
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));
104 end
105 mom = pfqn_sens_linearizer(D, Np, Ztot);
106elseif isExactMvaMethod(method)
107 mom = pfqn_sens_mom(D, Np, Ztot, ones(1,Mq), groups);
108else
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);
115end
116
117Station = {}; Chain = {};
118QLen = []; QLenVar = []; QLenSCV = []; QLenM3 = []; QLenSkew = [];
119for ist = 1:Mq
120 for g = 1:Cg
121 classesOf = find(groups == g);
122 if all(all(D(ist, classesOf) <= 0))
123 continue; % no class of this chain visits this station
124 end
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>
129 if mom.m(ist, g) > 0
130 QLenSCV(end+1, 1) = mom.Var(ist, g) / mom.m(ist, g)^2; %#ok<AGROW>
131 else
132 QLenSCV(end+1, 1) = NaN; %#ok<AGROW>
133 end
134 QLenM3(end+1, 1) = mom.M3(ist, g); %#ok<AGROW>
135 QLenSkew(end+1, 1) = mom.Skew(ist, g); %#ok<AGROW>
136 end
137end
138
139vars = {Station, Chain};
140names = {'Station', 'Chain'};
141if any(order == 1)
142 vars{end+1} = QLen; names{end+1} = 'QLen';
143end
144if any(order == 2)
145 vars{end+1} = QLenVar; names{end+1} = 'QLenVar';
146 vars{end+1} = QLenSCV; names{end+1} = 'QLenSCV';
147end
148if any(order == 3)
149 vars{end+1} = QLenM3; names{end+1} = 'QLenM3';
150 vars{end+1} = QLenSkew; names{end+1} = 'QLenSkew';
151end
152MomentChainTable = table(vars{:}, 'VariableNames', names);
153end
154
155% =========================================================================
156function tf = isExactMvaMethod(method)
157% Methods whose means are the exact MVA recursion; see getMomentStationTable.
158tf = any(strcmpi(method, {'default', 'mva', 'exact'}));
159end
160
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.
165%
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);
174h = 1e-4;
175qst = sn.nodeToStation(queueIndices);
176P = M*Cg;
177pidx = zeros(M,Cg);
178p = 0;
179for i = 1:M
180 for g = 1:Cg
181 p = p + 1; pidx(i,g) = p;
182 end
183end
184m0 = solveGroupTotals(self, sn, qst, groups, Cg);
185dm = zeros(M,Cg,M,Cg); d2m = zeros(M,Cg);
186for hi = 1:M
187 for hg = 1: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);
192 for i = 1:M
193 for g = 1:Cg
194 dm(i,g,hi,hg) = (mp(i,g) - mm(i,g)) / (2*h);
195 end
196 end
197 d2m(hi,hg) = (mp(hi,hg) - 2*m0(hi,hg) + mm(hi,hg)) / h^2;
198 end
199end
200mom = packChainFiniteDifference(m0, dm, d2m, Cg);
201end
202
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.
206sn2 = sn;
207cls = find(groups == g);
208sn2.rates(station, cls) = sn.rates(station, cls) / factor;
209end
210
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);
215M = numel(qst);
216mg = zeros(M,Cg);
217for i = 1:M
218 for g = 1:Cg
219 mg(i,g) = sum(QN(qst(i), groups == g));
220 end
221end
222end
223
224% =========================================================================
225function mom = packChainFiniteDifference(m, dm, d2m, Cg)
226% (3.2), applied to numerically obtained derivatives, per (station,group).
227M = size(m,1);
228mom.m = m; mom.d2m = d2m;
229flat = reshape(dm, M*Cg, M*Cg);
230mom.CovAsym = max(max(abs(flat - flat.')));
231flat = (flat + flat.')/2;
232if Cg == 1
233 mom.Cov = reshape(flat, M, M); mom.dm = reshape(dm, M, M);
234else
235 mom.Cov = reshape(flat, M, Cg, M, Cg); mom.dm = dm;
236end
237Var = zeros(M,Cg); M2 = zeros(M,Cg); M3 = zeros(M,Cg); Skew = zeros(M,Cg);
238for i = 1:M
239 for g = 1:Cg
240 d1 = dm(i,g,i,g);
241 Var(i,g) = d1;
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;
245 if Var(i,g) > 0
246 Skew(i,g) = mu3 / Var(i,g)^1.5;
247 else
248 Skew(i,g) = NaN;
249 end
250 end
251end
252mom.Var = Var; mom.M2 = M2; mom.M3 = M3; mom.Skew = Skew;
253end
254
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'}));
259end
260
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.
266%
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.
271%
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));
276end
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));
279end
280if isscalar(order)
281 order = 1:order;
282 return;
283end
284order = unique(order(:)');
285end
Definition Station.m:245