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 BCMP single-server: closed-form geometric/multinomial joint
94 % law, no lattice recursion. see _kb/06-solver-catalog.md for rationale
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.
');
102 rho(ist, r) = lambda(r) * D(ist, r);
106 rhoTot = sum(rho, 2);
110 line_error(mfilename, sprintf('Station %s
is unstable (utilization %.4f >= 1); its queue-length moments
do not exist.
', sn.nodenames{queueIndices(ist)}, ri));
116 Vn = ri / (1 - ri)^2;
118 pr = rho(ist, r) / ri;
119 QLen(ist, r) = pr * En;
120 QLenVar(ist, r) = En * (pr - pr^2) + pr^2 * Vn;
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);
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;
136% ---- response-time moments (FCFS only) ------------------------------------
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);
146 sIdx = sn.nodeToStation(queueIndices(ist));
147 if sn.sched(sIdx) == SchedStrategy.FCFS
148 st = serviceTimeOf(sn, sIdx, R);
150 Ssvc(ist) = st; % the true per-visit rate; (4.5) needs it
154 Vq(ist, r) = D(ist, r) / Ssvc(ist);
157 mom.respt = pfqn_sens_respt(Ssvc, Vq, Np, Ztot, Ssrv(:), max(order));
160 sIdx = sn.nodeToStation(queueIndices(ist));
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);
170 RespT(ist, r) = QLen(ist, r) / (xr * Vir);
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
178 RespT(ist, r) = mom.respt.W(ist, r);
180 RespTVar(ist, r) = mom.respt.WVar(ist, r);
183 RespTSkew(ist, r) = mom.respt.WSkew(ist, r);
190% ---- assemble -------------------------------------------------------------
192QLenv = []; QLenVarv = []; QLenSCVv = [];
193RespTv = []; RespTVarv = []; RespTSCVv = []; RespTSkewv = []; QLenSkewv = [];
195 node = queueIndices(ist);
198 continue; %
class r does not visit this station
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>
214names = {
'Station',
'JobClass'};
216 vars{end+1} = QLenv; names{end+1} =
'QLen';
219 vars{end+1} = QLenVarv; names{end+1} =
'QLenVar';
220 vars{end+1} = QLenSCVv; names{end+1} =
'QLenSCV';
223 vars{end+1} = QLenSkewv; names{end+1} =
'QLenSkew';
226 vars{end+1} = RespTv; names{end+1} =
'RespT';
229 vars{end+1} = RespTVarv; names{end+1} =
'RespTVar';
230 vars{end+1} = RespTSCVv; names{end+1} =
'RespTSCV';
233 vars{end+1} = RespTSkewv; names{end+1} =
'RespTSkew';
235MomentTable = table(vars{:},
'VariableNames', names);
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.
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.
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));
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));
262order = unique(order(:)');
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
272 v = variance / meanv^2;
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.
282for ist = 1:numel(queueIndices)
283 sIdx = sn.nodeToStation(queueIndices(ist));
284 if sn.sched(sIdx) ~= SchedStrategy.FCFS
289 rate = sn.rates(sIdx, r);
290 if ~isfinite(rate) || rate <= 0
295 elseif abs(rate - ref) > GlobalConstants.FineTol * max(1, ref)
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.
309 rate = sn.rates(sIdx, r);
310 if isfinite(rate) && rate > 0
317% =========================================================================
318function x = throughputOf(mom, lambda, N, r)
321elseif ~isempty(mom.qlen)