LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
pfqn_qlen_joint_moments.m
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)
3%
4% Joint moments of the queue-length vector of a closed product-form network,
5% obtained from normalizing constants.
6%
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:
14%
15% - SINGLE CLASS (R = 1), route 'tail'. The survival probabilities are ratios
16% of normalizing constants of the network itself,
17%
18% P(n_i >= k_i for all i) = (prod_i L_i^k_i) * G(N - sum_i k_i) / G(N)
19%
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.
23%
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,
29%
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)
31%
32% The survival array is the reverse cumulative sum of that array, exactly,
33% since the box covers the support.
34%
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.
41%
42% Input:
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
61%
62% Output:
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
66% and the pairs used
67%
68% Example:
69% out = pfqn_qlen_joint_moments([2 1], [4 3], [0.5 0.8], [1 1; 1 2]);
70% cov12 = out.cov(1,2);
71%
72% Reference:
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.
75
76[M,R] = size(L);
77N = N(:).';
78if numel(N) ~= R
79 line_error(mfilename,'The population vector N must have one entry per class.');
80end
81if nargin < 3 || isempty(Z)
82 Z = zeros(1,R);
83end
84Z = Z(:).';
85if numel(Z) ~= R
86 line_error(mfilename,'The think time vector Z must have one entry per class.');
87end
88if nargin < 4 || isempty(pairs)
89 pairs = zeros(M*R,2);
90 t = 0;
91 for i = 1:M
92 for r = 1:R
93 t = t + 1;
94 pairs(t,:) = [i, r];
95 end
96 end
97end
98if size(pairs,2) ~= 2 || isempty(pairs)
99 line_error(mfilename,'The pairs must be given as a Px2 matrix of (station,class) indices.');
100end
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.');
103end
104if size(unique(pairs,'rows'),1) ~= size(pairs,1)
105 line_error(mfilename,'The (station,class) pairs must be distinct.');
106end
107if nargin < 5 || isempty(route)
108 route = 'auto';
109end
110if nargin < 6
111 lGsrc = [];
112end
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';
120end
121if strcmp(route,'auto')
122 if R == 1
123 route = 'tail';
124 else
125 route = 'pmf';
126 end
127end
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.');
130end
131if ~any(strcmp(route,{'tail','pmf'}))
132 line_error(mfilename,'The route must be auto, tail or pmf.');
133end
134
135d = size(pairs,1);
136dims = zeros(1,d);
137for j = 1:d
138 dims(j) = N(pairs(j,2)) + 1;
139end
140
141if strcmp(route,'tail')
142 % the whole population set is known up front: N minus the total order
143 nel = prod(dims);
144 need = [];
145 a = ones(1,d);
146 for ia = 1:nel
147 s = sum(a-1);
148 if N - s >= 0
149 need(end+1,:) = N - s; %#ok<AGROW>
150 end
151 a = local_odometer(a, dims);
152 end
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]);
157 a = ones(1,d);
158 for ia = 1:nel
159 s = sum(a-1);
160 if N - s >= 0
161 acc = 0;
162 ok = true;
163 for j = 1:d
164 if a(j) > 1
165 if L(pairs(j,1),pairs(j,2)) <= 0
166 ok = false;
167 break
168 end
169 acc = acc + (a(j)-1)*log(L(pairs(j,1),pairs(j,2)));
170 end
171 end
172 if ok
173 tail(ia) = exp(acc + lg(local_findrow(need, N - s)) - lgN);
174 end
175 end
176 a = local_odometer(a, dims);
177 end
178else
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);
185 t = 0;
186 for i = stations
187 for r = 1:R
188 t = t + 1;
189 coords(t,:) = [i, r];
190 end
191 end
192 dc = size(coords,1);
193 cdims = zeros(1,dc);
194 for j = 1:dc
195 cdims(j) = N(coords(j,2)) + 1;
196 end
197 Lsub = L;
198 Lsub(stations,:) = [];
199 ncel = prod(cdims);
200 need = [];
201 a = ones(1,dc);
202 for ia = 1:ncel
203 n = N;
204 for j = 1:dc
205 n(coords(j,2)) = n(coords(j,2)) - (a(j)-1);
206 end
207 if all(n >= 0)
208 need(end+1,:) = n; %#ok<AGROW>
209 end
210 a = local_odometer(a, cdims);
211 end
212 need = unique(need, 'rows');
213 if isempty(Lsub)
214 lgc = zeros(size(need,1),1);
215 for p = 1:size(need,1)
216 lgc(p) = local_delay_lg(Z, need(p,:));
217 end
218 served = size(need,1);
219 evals = 0;
220 else
221 [lgc, served, evals] = local_batch_lg(Lsub, need, Z, lGsrc, options);
222 end
223 [lgNv, ~, evals0] = local_batch_lg(L, N, Z, [], options);
224 lgN = lgNv(1);
225 evals = evals + evals0;
226
227 marg = zeros([dims 1]);
228 a = ones(1,dc);
229 for ia = 1:ncel
230 n = N;
231 for j = 1:dc
232 n(coords(j,2)) = n(coords(j,2)) - (a(j)-1);
233 end
234 if all(n >= 0)
235 gc = lgc(local_findrow(need, n));
236 if isfinite(gc)
237 acc = gc - lgN;
238 ok = true;
239 for i = stations
240 tot = 0;
241 for j = 1:dc
242 if coords(j,1) == i
243 tot = tot + (a(j)-1);
244 end
245 end
246 acc = acc + gammaln(tot+1);
247 for j = 1:dc
248 if coords(j,1) == i && a(j) > 1
249 if L(i,coords(j,2)) <= 0
250 ok = false;
251 break
252 end
253 acc = acc + (a(j)-1)*log(L(i,coords(j,2))) - gammaln(a(j));
254 end
255 end
256 if ~ok
257 break
258 end
259 end
260 if ok
261 sub = ones(1,d);
262 for j = 1:d
263 for jc = 1:dc
264 if coords(jc,1) == pairs(j,1) && coords(jc,2) == pairs(j,2)
265 sub(j) = a(jc);
266 end
267 end
268 end
269 subc = num2cell(sub);
270 marg(subc{:}) = marg(subc{:}) + exp(acc);
271 end
272 end
273 end
274 a = local_odometer(a, cdims);
275 end
276 tail = marg;
277 for mode = 1:d
278 tail = flip(cumsum(flip(tail,mode),mode),mode);
279 end
280end
281
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);
287
288meanv = zeros(1,d);
289covm = zeros(d,d);
290for j = 1:d
291 e = ones(1,d);
292 e(j) = 2;
293 ec = num2cell(e);
294 meanv(j) = m(ec{:});
295 for l = 1:d
296 aa = ones(1,d);
297 aa(j) = aa(j) + 1;
298 aa(l) = aa(l) + 1;
299 ac = num2cell(aa);
300 covm(j,l) = kap(ac{:});
301 end
302end
303
304out = struct();
305out.tail = tail;
306out.binomial = b;
307out.factorial = f;
308out.raw = m;
309out.central = mc;
310out.cumulant = kap;
311out.mean = meanv;
312out.cov = covm;
313out.info = struct('route', route, 'points', size(need,1), 'served', served, ...
314 'evals', evals, 'exact', true, 'pairs', pairs, 'dims', dims);
315end
316
317function a = local_odometer(a, dims)
318% Advance a 1-based multi-index, first dimension fastest.
319for l = 1:numel(dims)
320 a(l) = a(l) + 1;
321 if a(l) <= dims(l)
322 return
323 end
324 a(l) = 1;
325end
326end
327
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);
331end
332
333function lg = local_delay_lg(Z, n)
334% Log normalizing constant of a pure-delay network, prod_r Z_r^n_r / n_r!.
335lg = 0;
336for r = 1:numel(n)
337 if n(r) == 0
338 continue
339 end
340 if Z(r) <= 0
341 lg = -Inf;
342 return
343 end
344 lg = lg + n(r)*log(Z(r)) - gammaln(n(r)+1);
345end
346end
347
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.
350P = size(pops,1);
351lg = nan(P,1);
352served = 0;
353if ~isempty(lGsrc)
354 if isa(lGsrc,'function_handle')
355 got = lGsrc(Lsub, pops);
356 got = got(:);
357 if numel(got) ~= P
358 line_error(mfilename,'The lGsrc handle must return one value per requested population.');
359 end
360 lg = got;
361 served = sum(isfinite(lg));
362 elseif isnumeric(lGsrc)
363 for p = 1:P
364 sub = num2cell(pops(p,:) + 1);
365 lg(p) = lGsrc(sub{:});
366 end
367 served = sum(isfinite(lg));
368 else
369 line_error(mfilename,'lGsrc must be empty, a function handle or a numeric table.');
370 end
371end
372evals = 0;
373R = size(pops,2);
374for p = 1:P
375 if isfinite(lg(p))
376 continue
377 end
378 lg(p) = pfqn_nc(zeros(1,R), Lsub, pops(p,:), Z, options);
379 evals = evals + 1;
380end
381end