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. The moment analysis of the pfqn_sens_* family needs at least
94 % one closed class to have a population lattice to recurse on, but a purely
95 % open BCMP single-server station has a closed-form joint law that needs no
96 % recursion at all: with rho(i,r) = lambda(r)*D(i,r) and rho_i = sum_r
97 % rho(i,r) < 1,
98 % P(n_i) = (1-rho_i) * (sum_r n(i,r))! / prod_r n(i,r)! * prod_r rho(i,r)^n(i,r)
99 % so the total n_i is geometric with parameter rho_i and, conditionally on
100 % n_i, the classes are multinomial with p_r = rho(i,r)/rho_i. Compounding,
101 % Cov[n(i,r),n(i,s)] = E[n_i]*(delta_rs*p_r - p_r*p_s) + p_r*p_s*Var[n_i]
102 if any(Ssrv > 1)
103 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.');
104 end
105 rho = zeros(Mq, R);
106 for ist = 1:Mq
107 for r = 1:R
108 if isinf(N(r))
109 rho(ist, r) = lambda(r) * D(ist, r);
110 end
111 end
112 end
113 rhoTot = sum(rho, 2);
114 for ist = 1:Mq
115 ri = rhoTot(ist);
116 if ri >= 1
117 line_error(mfilename, sprintf('Station %s is unstable (utilization %.4f >= 1); its queue-length moments do not exist.', sn.nodenames{queueIndices(ist)}, ri));
118 end
119 if ri <= 0
120 continue;
121 end
122 En = ri / (1 - ri);
123 Vn = ri / (1 - ri)^2;
124 for r = 1:R
125 pr = rho(ist, r) / ri;
126 QLen(ist, r) = pr * En;
127 QLenVar(ist, r) = En * (pr - pr^2) + pr^2 * Vn;
128 end
129 end
130end
131
132% ---- per-class queue-length skewness (closed, single-server only) ---------
133% pfqn_sens_mom with groups = 1:R scales one class at a time, which is the
134% class-subset parameter T = {r} of Akyildiz and Strelen's Theorem 1, so it
135% yields the moments of n(i,r) rather than of the station total.
136QLenSkew = nan(Mq, R);
137mom.qlenmom = [];
138if any(order == 3) && ~isOpen && all(Ssrv == 1)
139 mom.qlenmom = pfqn_sens_mom(D, Np, Ztot, ones(1,Mq), 1:R);
140 QLenSkew = mom.qlenmom.Skew;
141end
142
143% ---- response-time moments (FCFS only) ------------------------------------
144RespT = zeros(Mq, R);
145RespTVar = nan(Mq, R);
146RespTSkew = nan(Mq, R);
147% pfqn_sens_respt carries one service time S(i) per station, but S enters its
148% queue-length recursion ONLY through the product rho(i,r) = S(i)*V(i,r) = the
149% demand. The rate mu(i) = 1/S(i) is read directly by equation (4.5) alone, and
150% (4.5) is only evaluated at FCFS stations. So a non-FCFS station may keep
151% class-dependent demands: give it any nominal S and let V absorb the rest. Only
152% the FCFS stations must have class-independent rates, which BCMP requires of
153% them anyway.
154respAvail = ~isOpen && fcfsRatesAreClassIndependent(sn, queueIndices, R);
155if respAvail
156 Ssvc = ones(Mq, 1);
157 Vq = zeros(Mq, R);
158 for ist = 1:Mq
159 sIdx = sn.nodeToStation(queueIndices(ist));
160 if sn.sched(sIdx) == SchedStrategy.FCFS
161 st = serviceTimeOf(sn, sIdx, R);
162 if st > 0
163 Ssvc(ist) = st; % the true per-visit rate; (4.5) needs it
164 end
165 end
166 for r = 1:R
167 Vq(ist, r) = D(ist, r) / Ssvc(ist);
168 end
169 end
170 mom.respt = pfqn_sens_respt(Ssvc, Vq, Np, Ztot, Ssrv(:), max(order));
171end
172for ist = 1:Mq
173 sIdx = sn.nodeToStation(queueIndices(ist));
174 for r = 1:R
175 if D(ist, r) <= 0
176 continue;
177 end
178 % Mean response time per visit, by Little's law at the station: the
179 % arrival rate of class r to station i is X(r) times the visit ratio
180 % V(i,r) = D(i,r)/S(i,r) = D(i,r)*rate(i,r). This needs no
181 % distributional result, so it is always reported.
182 xr = throughputOf(mom, lambda, N, r);
183 Vir = D(ist, r) * sn.rates(sIdx, r);
184 if xr > 0 && Vir > 0
185 RespT(ist, r) = QLen(ist, r) / (xr * Vir);
186 end
187 end
188 % The variance needs the sojourn-time distribution, which is known only at
189 % FCFS centers; elsewhere RespTVar stays NaN.
190 if ~isempty(mom.respt) && sn.sched(sIdx) == SchedStrategy.FCFS
191 for r = 1:R
192 if D(ist, r) > 0
193 RespT(ist, r) = mom.respt.W(ist, r);
194 if max(order) >= 2
195 RespTVar(ist, r) = mom.respt.WVar(ist, r);
196 end
197 if max(order) >= 3
198 RespTSkew(ist, r) = mom.respt.WSkew(ist, r);
199 end
200 end
201 end
202 end
203end
204
205% ---- assemble -------------------------------------------------------------
206Station = {}; JobClass = {};
207QLenv = []; QLenVarv = []; QLenSCVv = [];
208RespTv = []; RespTVarv = []; RespTSCVv = []; RespTSkewv = []; QLenSkewv = [];
209for ist = 1:Mq
210 node = queueIndices(ist);
211 for r = 1:R
212 if D(ist, r) <= 0
213 continue; % class r does not visit this station
214 end
215 Station{end+1, 1} = sn.nodenames{node}; %#ok<AGROW>
216 JobClass{end+1, 1} = sn.classnames{r}; %#ok<AGROW>
217 QLenv(end+1, 1) = QLen(ist, r); %#ok<AGROW>
218 QLenVarv(end+1, 1) = QLenVar(ist, r); %#ok<AGROW>
219 QLenSCVv(end+1, 1) = scv(QLenVar(ist, r), QLen(ist, r)); %#ok<AGROW>
220 RespTv(end+1, 1) = RespT(ist, r); %#ok<AGROW>
221 RespTVarv(end+1, 1) = RespTVar(ist, r); %#ok<AGROW>
222 RespTSCVv(end+1, 1) = scv(RespTVar(ist, r), RespT(ist, r)); %#ok<AGROW>
223 RespTSkewv(end+1, 1) = RespTSkew(ist, r); %#ok<AGROW>
224 QLenSkewv(end+1, 1) = QLenSkew(ist, r); %#ok<AGROW>
225 end
226end
227
228vars = {Station, JobClass};
229names = {'Station', 'JobClass'};
230if any(order == 1)
231 vars{end+1} = QLenv; names{end+1} = 'QLen';
232end
233if any(order == 2)
234 vars{end+1} = QLenVarv; names{end+1} = 'QLenVar';
235 vars{end+1} = QLenSCVv; names{end+1} = 'QLenSCV';
236end
237if any(order == 3)
238 vars{end+1} = QLenSkewv; names{end+1} = 'QLenSkew';
239end
240if any(order == 1)
241 vars{end+1} = RespTv; names{end+1} = 'RespT';
242end
243if any(order == 2)
244 vars{end+1} = RespTVarv; names{end+1} = 'RespTVar';
245 vars{end+1} = RespTSCVv; names{end+1} = 'RespTSCV';
246end
247if any(order == 3)
248 vars{end+1} = RespTSkewv; names{end+1} = 'RespTSkew';
249end
250MomentTable = table(vars{:}, 'VariableNames', names);
251end
252
253% =========================================================================
254function order = validateMomentOrder(order, maxorder)
255% ORDER is a set of moment orders. A scalar k is shorthand for 1:k, so that
256% getMomentTable(2) means "up to the second moment" and not "the second moment
257% alone"; a vector of two or more entries is taken literally.
258%
259% Consequence of MATLAB's isscalar: a one-element vector IS a scalar, so [2]
260% takes the 1:k path and yields [1 2]. "The second moment alone" is therefore
261% not expressible, which is deliberate rather than overlooked: a variance with
262% no mean beside it is not a useful table, and [2 3] remains available for the
263% higher orders without the means.
264%
265% Non-integers are rejected on BOTH paths. Rounding them silently would accept
266% [1 2.5] as [1 3], i.e. answer a question that was not asked.
267if ~isnumeric(order) || isempty(order) || any(~isfinite(order(:)))
268 line_error(mfilename, sprintf('order must be an integer in 1..%d, or a vector of such integers.', maxorder));
269end
270if any(order(:) ~= round(order(:))) || any(order(:) < 1) || any(order(:) > maxorder)
271 line_error(mfilename, sprintf('order must be an integer in 1..%d, or a vector of such integers.', maxorder));
272end
273if isscalar(order)
274 order = 1:order;
275 return;
276end
277order = unique(order(:)');
278end
279
280% =========================================================================
281function v = scv(variance, meanv)
282% Squared coefficient of variation. NaN when the mean is zero, since the SCV is
283% then undefined rather than infinite in any useful sense.
284if isnan(variance) || meanv <= 0
285 v = NaN;
286else
287 v = variance / meanv^2;
288end
289end
290
291% =========================================================================
292function ok = fcfsRatesAreClassIndependent(sn, queueIndices, R)
293% An FCFS station in a BCMP network must serve every class at the same
294% exponential rate; that is the precondition for reading a per-visit rate off
295% it. Non-FCFS stations are unconstrained here, see the caller.
296ok = true;
297for ist = 1:numel(queueIndices)
298 sIdx = sn.nodeToStation(queueIndices(ist));
299 if sn.sched(sIdx) ~= SchedStrategy.FCFS
300 continue;
301 end
302 ref = -1;
303 for r = 1:R
304 rate = sn.rates(sIdx, r);
305 if ~isfinite(rate) || rate <= 0
306 continue;
307 end
308 if ref < 0
309 ref = rate;
310 elseif abs(rate - ref) > GlobalConstants.FineTol * max(1, ref)
311 ok = false;
312 return;
313 end
314 end
315end
316end
317
318% =========================================================================
319function s = serviceTimeOf(sn, sIdx, R)
320% The common service time of a station, i.e. the reciprocal of the class-
321% independent rate. Zero if no class is served here.
322s = 0;
323for r = 1:R
324 rate = sn.rates(sIdx, r);
325 if isfinite(rate) && rate > 0
326 s = 1 / rate;
327 return;
328 end
329end
330end
331
332% =========================================================================
333function x = throughputOf(mom, lambda, N, r)
334if isinf(N(r))
335 x = lambda(r);
336elseif ~isempty(mom.qlen)
337 x = mom.qlen.X(r);
338else
339 x = 0;
340end
341end
Definition Station.m:245