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% see _kb/06-solver-catalog.md for rationale (method fixed at construction)
55method = self.getOptions.method;
56
57sn = self.model.getStruct();
58R = sn.nclasses;
59N = sn.njobs;
60
61[~, D, Np, Z, ~, Ssrv, ~] = sn_get_product_form_params(sn);
62queueIndices = find(sn.nodetype == NodeType.Queue);
63Mq = numel(queueIndices);
64Ztot = sum(Z, 1);
65
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.');
69end
70if any(isinf(N))
71 line_error(mfilename, 'getMomentChainTable supports closed models only. Per-class second moments of an open or mixed model are available from getMomentTable.');
72end
73if any(Ssrv > 1)
74 line_error(mfilename, 'getMomentChainTable supports single-server stations only: the higher-moment recursion of the reference is stated for load-independent stations.');
75end
76
77% the chain of each class; sn.chains is (nchains x nclasses)
78groups = zeros(1, R);
79for r = 1:R
80 c = find(sn.chains(:, r), 1);
81 if isempty(c)
82 line_error(mfilename, sprintf('class %s belongs to no chain', sn.classnames{r}));
83 end
84 groups(r) = c;
85end
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);
89groups = compact(:)';
90Cg = numel(used);
91
92% see _kb/06-solver-catalog.md for rationale (Linearizer cannot express per-chain grouping)
93if isLinearizerMethod(method)
94 if Cg > 1
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));
96 end
97 mom = pfqn_sens_linearizer(D, Np, Ztot);
98elseif isExactMvaMethod(method)
99 mom = pfqn_sens_mom(D, Np, Ztot, ones(1,Mq), groups);
100else
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);
104end
105
106Station = {}; Chain = {};
107QLen = []; QLenVar = []; QLenSCV = []; QLenM3 = []; QLenSkew = [];
108for ist = 1:Mq
109 for g = 1:Cg
110 classesOf = find(groups == g);
111 if all(all(D(ist, classesOf) <= 0))
112 continue; % no class of this chain visits this station
113 end
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>
118 if mom.m(ist, g) > 0
119 QLenSCV(end+1, 1) = mom.Var(ist, g) / mom.m(ist, g)^2; %#ok<AGROW>
120 else
121 QLenSCV(end+1, 1) = NaN; %#ok<AGROW>
122 end
123 QLenM3(end+1, 1) = mom.M3(ist, g); %#ok<AGROW>
124 QLenSkew(end+1, 1) = mom.Skew(ist, g); %#ok<AGROW>
125 end
126end
127
128vars = {Station, Chain};
129names = {'Station', 'Chain'};
130if any(order == 1)
131 vars{end+1} = QLen; names{end+1} = 'QLen';
132end
133if any(order == 2)
134 vars{end+1} = QLenVar; names{end+1} = 'QLenVar';
135 vars{end+1} = QLenSCV; names{end+1} = 'QLenSCV';
136end
137if any(order == 3)
138 vars{end+1} = QLenM3; names{end+1} = 'QLenM3';
139 vars{end+1} = QLenSkew; names{end+1} = 'QLenSkew';
140end
141MomentChainTable = table(vars{:}, 'VariableNames', names);
142end
143
144% =========================================================================
145function tf = isExactMvaMethod(method)
146% Methods whose means are the exact MVA recursion; see getMomentStationTable.
147tf = any(strcmpi(method, {'default', 'mva', 'exact'}));
148end
149
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.
154%
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);
163h = 1e-4;
164qst = sn.nodeToStation(queueIndices);
165P = M*Cg;
166pidx = zeros(M,Cg);
167p = 0;
168for i = 1:M
169 for g = 1:Cg
170 p = p + 1; pidx(i,g) = p;
171 end
172end
173m0 = solveGroupTotals(self, sn, qst, groups, Cg);
174dm = zeros(M,Cg,M,Cg); d2m = zeros(M,Cg);
175for hi = 1:M
176 for hg = 1: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);
181 for i = 1:M
182 for g = 1:Cg
183 dm(i,g,hi,hg) = (mp(i,g) - mm(i,g)) / (2*h);
184 end
185 end
186 d2m(hi,hg) = (mp(hi,hg) - 2*m0(hi,hg) + mm(hi,hg)) / h^2;
187 end
188end
189mom = packChainFiniteDifference(m0, dm, d2m, Cg);
190end
191
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.
195sn2 = sn;
196cls = find(groups == g);
197sn2.rates(station, cls) = sn.rates(station, cls) / factor;
198end
199
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);
204M = numel(qst);
205mg = zeros(M,Cg);
206for i = 1:M
207 for g = 1:Cg
208 mg(i,g) = sum(QN(qst(i), groups == g));
209 end
210end
211end
212
213% =========================================================================
214function mom = packChainFiniteDifference(m, dm, d2m, Cg)
215% (3.2), applied to numerically obtained derivatives, per (station,group).
216M = size(m,1);
217mom.m = m; mom.d2m = d2m;
218flat = reshape(dm, M*Cg, M*Cg);
219mom.CovAsym = max(max(abs(flat - flat.')));
220flat = (flat + flat.')/2;
221if Cg == 1
222 mom.Cov = reshape(flat, M, M); mom.dm = reshape(dm, M, M);
223else
224 mom.Cov = reshape(flat, M, Cg, M, Cg); mom.dm = dm;
225end
226Var = zeros(M,Cg); M2 = zeros(M,Cg); M3 = zeros(M,Cg); Skew = zeros(M,Cg);
227for i = 1:M
228 for g = 1:Cg
229 d1 = dm(i,g,i,g);
230 Var(i,g) = d1;
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;
234 if Var(i,g) > 0
235 Skew(i,g) = mu3 / Var(i,g)^1.5;
236 else
237 Skew(i,g) = NaN;
238 end
239 end
240end
241mom.Var = Var; mom.M2 = M2; mom.M3 = M3; mom.Skew = Skew;
242end
243
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'}));
248end
249
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.
255%
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.
260%
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));
265end
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));
268end
269if isscalar(order)
270 order = 1:order;
271 return;
272end
273order = unique(order(:)');
274end