LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
getMomentTable.m
1function [MomentTable, mom] = getMomentTable(self, order)
2% GETMOMENTTABLE Exact higher moments of the per-class performance measures.
3%
4% [MOMENTTABLE, MOM] = GETMOMENTTABLE(SELF) returns a table with one row per
5% (Station, JobClass) giving, in addition to the means that getAvgTable
6% reports, the second moments of that row's queue length and response time:
7% QLen, QLenVar, QLenSCV, RespT, RespTVar, RespTSCV
8%
9% [..] = GETMOMENTTABLE(SELF, ORDER) selects which moment orders to report.
10% ORDER is a set. A scalar k is read as 1:k, "everything up to order k"; an
11% explicit vector selects exactly those orders:
12% 1 the means only: QLen, RespT
13% 2 (default) means and second moments, i.e. the columns above
14% 3 also adds RespTSkew
15% [1 2] the same as 2
16% [2 3] second moments and skewness, without the means
17% Order 1 contributes QLen and RespT, order 2 contributes the Var and SCV
18% columns, order 3 contributes RespTSkew.
19%
20% ORDER = 3 also adds QLenSkew, the skewness of the per-class queue length.
21% That quantity is reachable because the generating parameter need not scale a
22% whole demand column: scaling L(i,r) alone is Theorem 1 of Akyildiz and Strelen
23% with the class subset T = {r}, and it generates the moments of n(i,r) itself.
24% QLenSkew is available only for closed single-server models, which is the scope
25% of pfqn_sens_mom; it is NaN otherwise.
26%
27% All of it is exact, not simulated and not approximated. The queue-length
28% moments come from the product-form identity Cov[n(i,r),n(j,s)] =
29% L(j,s) dQ(i,r)/dL(j,s), evaluated by the pfqn_sens_* family; see
30% _kb/03-api-layer.md.
31%
32% RESPONSE-TIME MOMENTS ARE FCFS-ONLY. RespTVar and RespTSCV are NaN at any
33% station that is not FCFS, and in open or mixed models. This is a limitation
34% of the theory, not of the implementation: the sojourn-time distribution at a
35% processor-sharing or LCFS center is not known in general (Strelen 1990,
36% Section 4), so there is no correct value to report and a wrong one is worse
37% than a blank. The mean RespT is always reported, since it needs no
38% distributional result.
39%
40% Scope by model type:
41% closed, single-server -> pfqn_sens_mva
42% closed, multiserver -> pfqn_sens_mvaldmx
43% mixed open and closed -> pfqn_sens_mvaldmx
44% purely open, single-server -> exact BCMP closed form (below)
45% purely open, multiserver -> not supported, see the error
46%
47% MOM is a struct carrying the raw results: .qlen is the underlying
48% pfqn_sens_mva / pfqn_sens_mvaldmx struct (with the full covariance matrices,
49% not just the diagonal this table shows), .respt is the pfqn_sens_respt struct
50% or empty. Use it when the per-pair covariances are needed.
51%
52% Per-station TOTAL moments, including the third moment and the skewness, are
53% in getMomentStationTable: they are only defined for a station total, because
54% the parameter that generates them scales a whole demand column.
55%
56% See also: getAvgTable, getSensitivityTable, getMomentStationTable.
57
58if nargin < 2 || isempty(order)
59 order = 2;
60end
61order = validateMomentOrder(order, 3);
62
63sn = self.model.getStruct();
64R = sn.nclasses;
65N = sn.njobs;
66
67[lambda, D, Np, Z, mu, Ssrv, ~] = sn_get_product_form_params(sn);
68queueIndices = find(sn.nodetype == NodeType.Queue);
69Mq = numel(queueIndices);
70Ztot = sum(Z, 1);
71isOpen = any(isinf(N));
72isClosed = any(isfinite(N) & N > 0);
73isMixed = isOpen && isClosed;
74
75mom = struct('qlen', [], 'respt', [], 'qlenmom', []);
76QLen = zeros(Mq, R);
77QLenVar = zeros(Mq, R);
78
79% ---- queue-length moments -------------------------------------------------
80if ~isOpen
81 if all(Ssrv == 1)
82 mom.qlen = pfqn_sens_mva(D, Np, Ztot);
83 else
84 mom.qlen = pfqn_sens_mvaldmx(zeros(1,R), D, Np, Ztot, mu, Ssrv);
85 end
86 QLen = mom.qlen.Q;
87 QLenVar = mom.qlen.QVar;
88elseif isMixed
89 mom.qlen = pfqn_sens_mvaldmx(lambda, D, N, Ztot, mu, Ssrv);
90 QLen = mom.qlen.Q;
91 QLenVar = mom.qlen.QVar;
92else
93 % Purely open BCMP single-server: closed-form geometric/multinomial joint
94 % law, no lattice recursion. see _kb/06-solver-catalog.md for rationale
95 if any(Ssrv > 1)
96 line_error(mfilename, 'getMomentTable does not support multiserver stations in a purely open model: the queue-length law is not geometric there. Add a closed class, or use a single-server model.');
97 end
98 rho = zeros(Mq, R);
99 for ist = 1:Mq
100 for r = 1:R
101 if isinf(N(r))
102 rho(ist, r) = lambda(r) * D(ist, r);
103 end
104 end
105 end
106 rhoTot = sum(rho, 2);
107 for ist = 1:Mq
108 ri = rhoTot(ist);
109 if ri >= 1
110 line_error(mfilename, sprintf('Station %s is unstable (utilization %.4f >= 1); its queue-length moments do not exist.', sn.nodenames{queueIndices(ist)}, ri));
111 end
112 if ri <= 0
113 continue;
114 end
115 En = ri / (1 - ri);
116 Vn = ri / (1 - ri)^2;
117 for r = 1:R
118 pr = rho(ist, r) / ri;
119 QLen(ist, r) = pr * En;
120 QLenVar(ist, r) = En * (pr - pr^2) + pr^2 * Vn;
121 end
122 end
123end
124
125% ---- per-class queue-length skewness (closed, single-server only) ---------
126% pfqn_sens_mom with groups = 1:R scales one class at a time, which is the
127% class-subset parameter T = {r} of Akyildiz and Strelen's Theorem 1, so it
128% yields the moments of n(i,r) rather than of the station total.
129QLenSkew = nan(Mq, R);
130mom.qlenmom = [];
131if any(order == 3) && ~isOpen && all(Ssrv == 1)
132 mom.qlenmom = pfqn_sens_mom(D, Np, Ztot, ones(1,Mq), 1:R);
133 QLenSkew = mom.qlenmom.Skew;
134end
135
136% ---- response-time moments (FCFS only) ------------------------------------
137RespT = zeros(Mq, R);
138RespTVar = nan(Mq, R);
139RespTSkew = nan(Mq, R);
140% see _kb/06-solver-catalog.md for rationale (pfqn_sens_respt reads mu only at FCFS)
141respAvail = ~isOpen && fcfsRatesAreClassIndependent(sn, queueIndices, R);
142if respAvail
143 Ssvc = ones(Mq, 1);
144 Vq = zeros(Mq, R);
145 for ist = 1:Mq
146 sIdx = sn.nodeToStation(queueIndices(ist));
147 if sn.sched(sIdx) == SchedStrategy.FCFS
148 st = serviceTimeOf(sn, sIdx, R);
149 if st > 0
150 Ssvc(ist) = st; % the true per-visit rate; (4.5) needs it
151 end
152 end
153 for r = 1:R
154 Vq(ist, r) = D(ist, r) / Ssvc(ist);
155 end
156 end
157 mom.respt = pfqn_sens_respt(Ssvc, Vq, Np, Ztot, Ssrv(:), max(order));
158end
159for ist = 1:Mq
160 sIdx = sn.nodeToStation(queueIndices(ist));
161 for r = 1:R
162 if D(ist, r) <= 0
163 continue;
164 end
165 % Mean response time per visit by Little's law at the station; always
166 % reported. see _kb/06-solver-catalog.md for rationale
167 xr = throughputOf(mom, lambda, N, r);
168 Vir = D(ist, r) * sn.rates(sIdx, r);
169 if xr > 0 && Vir > 0
170 RespT(ist, r) = QLen(ist, r) / (xr * Vir);
171 end
172 end
173 % The variance needs the sojourn-time distribution, which is known only at
174 % FCFS centers; elsewhere RespTVar stays NaN.
175 if ~isempty(mom.respt) && sn.sched(sIdx) == SchedStrategy.FCFS
176 for r = 1:R
177 if D(ist, r) > 0
178 RespT(ist, r) = mom.respt.W(ist, r);
179 if max(order) >= 2
180 RespTVar(ist, r) = mom.respt.WVar(ist, r);
181 end
182 if max(order) >= 3
183 RespTSkew(ist, r) = mom.respt.WSkew(ist, r);
184 end
185 end
186 end
187 end
188end
189
190% ---- assemble -------------------------------------------------------------
191Station = {}; JobClass = {};
192QLenv = []; QLenVarv = []; QLenSCVv = [];
193RespTv = []; RespTVarv = []; RespTSCVv = []; RespTSkewv = []; QLenSkewv = [];
194for ist = 1:Mq
195 node = queueIndices(ist);
196 for r = 1:R
197 if D(ist, r) <= 0
198 continue; % class r does not visit this station
199 end
200 Station{end+1, 1} = sn.nodenames{node}; %#ok<AGROW>
201 JobClass{end+1, 1} = sn.classnames{r}; %#ok<AGROW>
202 QLenv(end+1, 1) = QLen(ist, r); %#ok<AGROW>
203 QLenVarv(end+1, 1) = QLenVar(ist, r); %#ok<AGROW>
204 QLenSCVv(end+1, 1) = scv(QLenVar(ist, r), QLen(ist, r)); %#ok<AGROW>
205 RespTv(end+1, 1) = RespT(ist, r); %#ok<AGROW>
206 RespTVarv(end+1, 1) = RespTVar(ist, r); %#ok<AGROW>
207 RespTSCVv(end+1, 1) = scv(RespTVar(ist, r), RespT(ist, r)); %#ok<AGROW>
208 RespTSkewv(end+1, 1) = RespTSkew(ist, r); %#ok<AGROW>
209 QLenSkewv(end+1, 1) = QLenSkew(ist, r); %#ok<AGROW>
210 end
211end
212
213vars = {Station, JobClass};
214names = {'Station', 'JobClass'};
215if any(order == 1)
216 vars{end+1} = QLenv; names{end+1} = 'QLen';
217end
218if any(order == 2)
219 vars{end+1} = QLenVarv; names{end+1} = 'QLenVar';
220 vars{end+1} = QLenSCVv; names{end+1} = 'QLenSCV';
221end
222if any(order == 3)
223 vars{end+1} = QLenSkewv; names{end+1} = 'QLenSkew';
224end
225if any(order == 1)
226 vars{end+1} = RespTv; names{end+1} = 'RespT';
227end
228if any(order == 2)
229 vars{end+1} = RespTVarv; names{end+1} = 'RespTVar';
230 vars{end+1} = RespTSCVv; names{end+1} = 'RespTSCV';
231end
232if any(order == 3)
233 vars{end+1} = RespTSkewv; names{end+1} = 'RespTSkew';
234end
235MomentTable = table(vars{:}, 'VariableNames', names);
236end
237
238% =========================================================================
239function order = validateMomentOrder(order, maxorder)
240% ORDER is a set of moment orders. A scalar k is shorthand for 1:k, so that
241% getMomentTable(2) means "up to the second moment" and not "the second moment
242% alone"; a vector of two or more entries is taken literally.
243%
244% Consequence of MATLAB's isscalar: a one-element vector IS a scalar, so [2]
245% takes the 1:k path and yields [1 2]. "The second moment alone" is therefore
246% not expressible, which is deliberate rather than overlooked: a variance with
247% no mean beside it is not a useful table, and [2 3] remains available for the
248% higher orders without the means.
249%
250% Non-integers are rejected on BOTH paths. Rounding them silently would accept
251% [1 2.5] as [1 3], i.e. answer a question that was not asked.
252if ~isnumeric(order) || isempty(order) || any(~isfinite(order(:)))
253 line_error(mfilename, sprintf('order must be an integer in 1..%d, or a vector of such integers.', maxorder));
254end
255if any(order(:) ~= round(order(:))) || any(order(:) < 1) || any(order(:) > maxorder)
256 line_error(mfilename, sprintf('order must be an integer in 1..%d, or a vector of such integers.', maxorder));
257end
258if isscalar(order)
259 order = 1:order;
260 return;
261end
262order = unique(order(:)');
263end
264
265% =========================================================================
266function v = scv(variance, meanv)
267% Squared coefficient of variation. NaN when the mean is zero, since the SCV is
268% then undefined rather than infinite in any useful sense.
269if isnan(variance) || meanv <= 0
270 v = NaN;
271else
272 v = variance / meanv^2;
273end
274end
275
276% =========================================================================
277function ok = fcfsRatesAreClassIndependent(sn, queueIndices, R)
278% An FCFS station in a BCMP network must serve every class at the same
279% exponential rate; that is the precondition for reading a per-visit rate off
280% it. Non-FCFS stations are unconstrained here, see the caller.
281ok = true;
282for ist = 1:numel(queueIndices)
283 sIdx = sn.nodeToStation(queueIndices(ist));
284 if sn.sched(sIdx) ~= SchedStrategy.FCFS
285 continue;
286 end
287 ref = -1;
288 for r = 1:R
289 rate = sn.rates(sIdx, r);
290 if ~isfinite(rate) || rate <= 0
291 continue;
292 end
293 if ref < 0
294 ref = rate;
295 elseif abs(rate - ref) > GlobalConstants.FineTol * max(1, ref)
296 ok = false;
297 return;
298 end
299 end
300end
301end
302
303% =========================================================================
304function s = serviceTimeOf(sn, sIdx, R)
305% The common service time of a station, i.e. the reciprocal of the class-
306% independent rate. Zero if no class is served here.
307s = 0;
308for r = 1:R
309 rate = sn.rates(sIdx, r);
310 if isfinite(rate) && rate > 0
311 s = 1 / rate;
312 return;
313 end
314end
315end
316
317% =========================================================================
318function x = throughputOf(mom, lambda, N, r)
319if isinf(N(r))
320 x = lambda(r);
321elseif ~isempty(mom.qlen)
322 x = mom.qlen.X(r);
323else
324 x = 0;
325end
326end