1function out = pfqn_qlen_joint_moments(L, N, Z, pairs, route, lGsrc, options)
2% out = pfqn_qlen_joint_moments(L, N, Z, pairs, route, lGsrc, options)
4% Joint moments of the queue-length vector of a closed product-
form network,
5% obtained from normalizing constants.
7% The coordinates are (station,
class) pairs. Two pairs sharing a class give the
8% cross-station covariance of that class; two pairs sharing a station give the
9% cross-class covariance at that station, which
is what a class-oriented method
10% of moments (pfqn_comomrm and its relatives)
is positioned to deliver. Two
11% exact routes reach the joint survival array, and both end in the same
12% conversion, the tail edge of the house of moments (api/moment) followed by
13% the joint central-moment and cumulant conversions:
15% - SINGLE CLASS (R = 1), route 'tail'. The survival probabilities are ratios
16% of normalizing constants of the network itself,
18%
P(n_i >= k_i for all i) = (prod_i L_i^k_i) * G(N - sum_i k_i) / G(N)
20% which holds because a load-independent single-class station has the
21% geometric occupancy L_i^n. Only N+1 constants of the ORIGINAL model are
22% needed, which
is why any normalizing-constant algorithm serves it.
24% - MULTICLASS, route 'pmf'. The geometric factorization fails, since a
25% multiclass load-independent station carries the multinomial occupancy
26% f_i(n_i) = (|n_i|)! prod_r L_ir^n_ir / n_ir!. What holds instead
is the
27% joint law of the selected stations in terms of the COMPLEMENTARY network,
28% the model with those stations deleted and the think times kept,
30%
P(n_i = m_i, i in S) = prod_i f_i(m_i) * G_(S^c)(N - sum_i m_i) / G(N)
32% The survival array
is the reverse cumulative sum of that array, exactly,
33% since the box covers the support.
35% Neither the factorial nor the raw moments have a one-constant closed
form;
36% the survival array
is the queue-length functional that does. The
37% normalizing-constant algorithm
is INJECTED rather than called at a fixed
38% site: the whole set of populations
is known before any evaluation, so it
is
39% emitted in one batch and an algorithm that produces several constants in one
40% pass serves it without recomputation.
43% L: service demand matrix of the QUEUEING stations (MxR). Delay stations
44% belong in Z, their marginals following a different law. Load-dependent
45% and multiserver stations are out of scope for both routes
46% N: population vector (1xR)
47% Z: think time vector (1xR), zeros if empty
48% pairs: Px2 matrix of 1-based (station,class) pairs, one per dimension of
49% the returned arrays. Defaults to every class of every station
50% route: 'auto' (default), 'tail' or 'pmf'
51% lGsrc: where log G comes from. Empty calls pfqn_nc. A function handle
is
52% invoked ONCE per network as lGsrc(Lsub, pops), pops being a PxR
53% matrix of populations, and must return
P values of log G with NaN
54% where it cannot serve; those are filled in by pfqn_nc. A numeric
55% array
is read as a table indexed by population, which
is what a
56% convolution sweep produces for free. The 'pmf' route queries the
57% COMPLEMENTARY network, so a table must be its table
58% options: options struct passed to pfqn_nc. Defaults to
59% SolverNC.defaultOptions with method 'exact', since an approximate
60% normalizing constant would silently make the moments approximate
63% out: struct with the joint arrays over the selected coordinates (tail,
64% binomial, factorial, raw, central, cumulant), the mean vector, the
65% covariance matrix cov, and info holding route, points, served, evals
69% out = pfqn_qlen_joint_moments([2 1], [4 3], [0.5 0.8], [1 1; 1 2]);
70% cov12 = out.cov(1,2);
73% M. Reiser and S. S. Lavenberg. Mean-value analysis of closed multichain
74% queuing networks. Journal of the ACM, 27(2):313-322, 1980.
79 line_error(mfilename,'The population vector N must have one entry per class.');
81if nargin < 3 || isempty(Z)
86 line_error(mfilename,'The think time vector Z must have one entry per class.');
88if nargin < 4 || isempty(pairs)
98if size(pairs,2) ~= 2 || isempty(pairs)
99 line_error(mfilename,'The pairs must be given as a Px2 matrix of (station,class) indices.');
101if any(pairs(:,1) < 1) || any(pairs(:,1) > M) || any(pairs(:,2) < 1) || any(pairs(:,2) > R)
102 line_error(mfilename,'A (station,class) pair
is out of range.');
104if size(unique(pairs,'rows'),1) ~= size(pairs,1)
105 line_error(mfilename,'The (station,class) pairs must be distinct.');
107if nargin < 5 || isempty(route)
113if nargin < 7 || isempty(options)
114 % the point of this routine
is an EXACT moment array, so the default
is the
115 % exact normalizing constant rather than the adaptive dispatch of pfqn_nc.
116 % parseOptions replaces the defaults wholesale, so the struct
is built from
117 % SolverNC.defaultOptions and only the method
is overridden
118 options = SolverNC.defaultOptions;
119 options.method = 'exact';
121if strcmp(route,'auto')
128if strcmp(route,'tail') && R > 1
129 line_error(mfilename,'The tail route needs the geometric occupancy of a single-class load-independent station; with several classes the multinomial
factor breaks the survival identity, so use the pmf route.');
131if ~any(strcmp(route,{
'tail',
'pmf'}))
132 line_error(mfilename,'The route must be auto, tail or pmf.');
138 dims(j) = N(pairs(j,2)) + 1;
141if strcmp(route,'tail')
142 % the whole population set
is known up front: N minus the total order
149 need(end+1,:) = N - s; %
#ok<AGROW>
151 a = local_odometer(a, dims);
153 need = unique([need; N],
'rows');
154 [lg, served, evals] = local_batch_lg(L, need, Z, lGsrc, options);
155 lgN = lg(local_findrow(need, N));
156 tail = zeros([dims 1]);
165 if L(pairs(j,1),pairs(j,2)) <= 0
169 acc = acc + (a(j)-1)*log(L(pairs(j,1),pairs(j,2)));
173 tail(ia) = exp(acc + lg(local_findrow(need, N - s)) - lgN);
176 a = local_odometer(a, dims);
179 % the joint law of the selected stations needs every
class of those
180 % stations, so the internal box runs over (station,
class) and the requested
181 % pairs are marginalized out of it afterwards
182 stations = unique(pairs(:,1)).';
183 ns = numel(stations);
184 coords = zeros(ns*R,2);
189 coords(t,:) = [i, r];
195 cdims(j) = N(coords(j,2)) + 1;
198 Lsub(stations,:) = [];
205 n(coords(j,2)) = n(coords(j,2)) - (a(j)-1);
208 need(end+1,:) = n; %
#ok<AGROW>
210 a = local_odometer(a, cdims);
212 need = unique(need,
'rows');
214 lgc = zeros(size(need,1),1);
215 for p = 1:size(need,1)
216 lgc(p) = local_delay_lg(Z, need(p,:));
218 served = size(need,1);
221 [lgc, served, evals] = local_batch_lg(Lsub, need, Z, lGsrc, options);
223 [lgNv, ~, evals0] = local_batch_lg(L, N, Z, [], options);
225 evals = evals + evals0;
227 marg = zeros([dims 1]);
232 n(coords(j,2)) = n(coords(j,2)) - (a(j)-1);
235 gc = lgc(local_findrow(need, n));
243 tot = tot + (a(j)-1);
246 acc = acc + gammaln(tot+1);
248 if coords(j,1) == i && a(j) > 1
249 if L(i,coords(j,2)) <= 0
253 acc = acc + (a(j)-1)*log(L(i,coords(j,2))) - gammaln(a(j));
264 if coords(jc,1) == pairs(j,1) && coords(jc,2) == pairs(j,2)
269 subc = num2cell(sub);
270 marg(subc{:}) = marg(subc{:}) + exp(acc);
274 a = local_odometer(a, cdims);
278 tail = flip(cumsum(flip(tail,mode),mode),mode);
282b = moment_joint_binomial_from_tail(tail);
283f = moment_joint_factorial_from_binomial(b);
284m = moment_joint_raw_from_factorial(f);
285mc = moment_joint_central_from_raw(m);
286kap = moment_joint_cumulant_from_raw(m);
300 covm(j,l) = kap(ac{:});
313out.info =
struct(
'route', route,
'points', size(need,1),
'served', served, ...
314 'evals', evals,
'exact',
true,
'pairs', pairs,
'dims', dims);
317function a = local_odometer(a, dims)
318% Advance a 1-based multi-index, first dimension fastest.
328function p = local_findrow(rows, key)
329% Index of a population vector in the deduplicated request matrix.
330p = find(all(rows == repmat(key, size(rows,1), 1), 2), 1);
333function lg = local_delay_lg(Z, n)
334% Log normalizing constant of a pure-delay network, prod_r Z_r^n_r / n_r!.
344 lg = lg + n(r)*log(Z(r)) - gammaln(n(r)+1);
348function [lg, served, evals] = local_batch_lg(Lsub, pops, Z, lGsrc, options)
349% Evaluate log G at a batch of populations, honouring the injected source.
354 if isa(lGsrc,
'function_handle')
355 got = lGsrc(Lsub, pops);
358 line_error(mfilename,'The lGsrc handle must return one value per requested population.');
361 served = sum(isfinite(lg));
362 elseif isnumeric(lGsrc)
364 sub = num2cell(pops(p,:) + 1);
365 lg(p) = lGsrc(sub{:});
367 served = sum(isfinite(lg));
369 line_error(mfilename,
'lGsrc must be empty, a function handle or a numeric table.');
378 lg(p) = pfqn_nc(zeros(1,R), Lsub, pops(p,:), Z, options);