1function [MomentTable, mom] = getMomentTable(self, order)
2% GETMOMENTTABLE Exact higher moments of
the per-
class performance measures.
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
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
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.
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.
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
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.
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
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.
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.
56% See also: getAvgTable, getSensitivityTable, getMomentStationTable.
58if nargin < 2 || isempty(order)
61order = validateMomentOrder(order, 3);
63sn = self.model.getStruct();
67[lambda, D, Np, Z, mu, Ssrv, ~] = sn_get_product_form_params(sn);
68queueIndices = find(sn.nodetype == NodeType.Queue);
69Mq = numel(queueIndices);
71isOpen = any(isinf(N));
72isClosed = any(isfinite(N) & N > 0);
73isMixed = isOpen && isClosed;
75mom = struct('qlen
', [], 'respt
', [], 'qlenmom
', []);
77QLenVar = zeros(Mq, R);
79% ---- queue-length moments -------------------------------------------------
82 mom.qlen = pfqn_sens_mva(D, Np, Ztot);
84 mom.qlen = pfqn_sens_mvaldmx(zeros(1,R), D, Np, Ztot, mu, Ssrv);
87 QLenVar = mom.qlen.QVar;
89 mom.qlen = pfqn_sens_mvaldmx(lambda, D, N, Ztot, mu, Ssrv);
91 QLenVar = mom.qlen.QVar;
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
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]
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.
');
109 rho(ist, r) = lambda(r) * D(ist, r);
113 rhoTot = sum(rho, 2);
117 line_error(mfilename, sprintf('Station %s
is unstable (utilization %.4f >= 1); its queue-length moments
do not exist.
', sn.nodenames{queueIndices(ist)}, ri));
123 Vn = ri / (1 - ri)^2;
125 pr = rho(ist, r) / ri;
126 QLen(ist, r) = pr * En;
127 QLenVar(ist, r) = En * (pr - pr^2) + pr^2 * Vn;
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);
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;
143% ---- response-time moments (FCFS only) ------------------------------------
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
154respAvail = ~isOpen && fcfsRatesAreClassIndependent(sn, queueIndices, R);
159 sIdx = sn.nodeToStation(queueIndices(ist));
160 if sn.sched(sIdx) == SchedStrategy.FCFS
161 st = serviceTimeOf(sn, sIdx, R);
163 Ssvc(ist) = st; %
the true per-visit rate; (4.5) needs it
167 Vq(ist, r) = D(ist, r) / Ssvc(ist);
170 mom.respt = pfqn_sens_respt(Ssvc, Vq, Np, Ztot, Ssrv(:), max(order));
173 sIdx = sn.nodeToStation(queueIndices(ist));
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);
185 RespT(ist, r) = QLen(ist, r) / (xr * Vir);
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
193 RespT(ist, r) = mom.respt.W(ist, r);
195 RespTVar(ist, r) = mom.respt.WVar(ist, r);
198 RespTSkew(ist, r) = mom.respt.WSkew(ist, r);
205% ---- assemble -------------------------------------------------------------
207QLenv = []; QLenVarv = []; QLenSCVv = [];
208RespTv = []; RespTVarv = []; RespTSCVv = []; RespTSkewv = []; QLenSkewv = [];
210 node = queueIndices(ist);
213 continue; %
class r does not visit this station
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>
229names = {
'Station',
'JobClass'};
231 vars{end+1} = QLenv; names{end+1} =
'QLen';
234 vars{end+1} = QLenVarv; names{end+1} =
'QLenVar';
235 vars{end+1} = QLenSCVv; names{end+1} =
'QLenSCV';
238 vars{end+1} = QLenSkewv; names{end+1} =
'QLenSkew';
241 vars{end+1} = RespTv; names{end+1} =
'RespT';
244 vars{end+1} = RespTVarv; names{end+1} =
'RespTVar';
245 vars{end+1} = RespTSCVv; names{end+1} =
'RespTSCV';
248 vars{end+1} = RespTSkewv; names{end+1} =
'RespTSkew';
250MomentTable = table(vars{:},
'VariableNames', names);
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.
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.
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));
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));
277order = unique(order(:)');
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
287 v = variance / meanv^2;
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.
297for ist = 1:numel(queueIndices)
298 sIdx = sn.nodeToStation(queueIndices(ist));
299 if sn.sched(sIdx) ~= SchedStrategy.FCFS
304 rate = sn.rates(sIdx, r);
305 if ~isfinite(rate) || rate <= 0
310 elseif abs(rate - ref) > GlobalConstants.FineTol * max(1, ref)
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.
324 rate = sn.rates(sIdx, r);
325 if isfinite(rate) && rate > 0
332% =========================================================================
333function x = throughputOf(mom, lambda, N, r)
336elseif ~isempty(mom.qlen)