LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
solver_mam_basic.m
1function [QN,UN,RN,TN,CN,XN,totiter] = solver_mam_basic(sn, options)
2% [Q,U,R,T,C,X] = SOLVER_MAM_BASIC(QN, PH, OPTIONS)
3
4% This solver uses MAM to solve queues in isolation, but simplifies the
5% traffic equations by using visits to rescale the flows into the queue
6% inputs.
7%
8% Copyright (c) 2012-2026, Imperial College London
9% All rights reserved.
10
11config = options.config;
12tol = options.tol;
13
14PH = sn.proc;
15%% generate local state spaces
16I = sn.nnodes;
17M = sn.nstations;
18K = sn.nclasses;
19C = sn.nchains;
20N = sn.njobs';
21V = cellsum(sn.visits);
22S = 1./sn.rates;
23Strue = S; % service times as declared, used to report utilization below
24Lchain = sn_get_demands_chain(sn);
25
26% Interference of self-looping classes on the classes they share a station
27% with. A self-looping class holds njobs customers at its reference station in
28% perpetuity, so a job of any other class arriving there always finds those
29% njobs customers resident and, under any work-conserving discipline, must
30% share the server with them: its residence time is multiplied by
31% (1 + njobs_slc). Modelling this as an inflated service time keeps the SLC out
32% of the decomposition (it has no surrogate arrival stream; see below) while
33% still charging the other classes for the capacity it holds. On the sanity SLC
34% models this reproduces the CTMC solution exactly. Delay stations are exempt:
35% with infinite servers there is no contention to share.
36slcjobs = zeros(M,1);
37for k=1:K
38 if sn.isslc(k)
39 ist_k = sn.refstat(k);
40 if isfinite(sn.nservers(ist_k))
41 slcjobs(ist_k) = slcjobs(ist_k) + sn.njobs(k);
42 end
43 end
44end
45for ist=1:M
46 if slcjobs(ist) > 0
47 for k=1:K
48 if ~sn.isslc(k)
49 S(ist,k) = S(ist,k)*(1+slcjobs(ist));
50 end
51 end
52 % The chain demands drive the throughput fixed point and must be
53 % consistent with the inflated service times.
54 Lchain(ist,:) = Lchain(ist,:)*(1+slcjobs(ist));
55 end
56end
57
58QN = zeros(M,K);
59UN = zeros(M,K);
60RN = zeros(M,K);
61TN = zeros(M,K);
62CN = zeros(1,K);
63XN = zeros(1,K);
64
65% Track stations using exact MAP/D/c solver (skip post-processing for these)
66mapdcStations = false(M, 1);
67
68pie = {};
69D0 = {};
70
71lambda = zeros(1,C);
72chainSysArrivals = cell(1,C);
73TN_1 = TN+Inf;
74
75it = 0;
76
77% open queueing system (one node is the external world)
78% first build the joint arrival process
79for ist=1:M
80 switch sn.sched(ist)
81 case {SchedStrategy.FCFS, SchedStrategy.HOL, SchedStrategy.FCFSPRPRIO, SchedStrategy.PS}
82 for k=1:K
83 % Skip Det processes - they will be handled by qsys_mapdc
84 if sn.procid(ist, k) == ProcessType.DET
85 % For Det, we don't need MAP representation since we use exact solver
86 pie{ist}{k} = [];
87 D0{ist,k} = [];
88 continue;
89 end
90 % divide service time by number of servers and put
91 % later a surrogate delay server in tandem to compensate.
92 % map_scale normalizes through map_normalize, which clips
93 % negative entries to zero; on a matrix-exponential or
94 % rational representation that is not a repair of numerical
95 % noise but a replacement of the process by a different,
96 % Markovian one, so those are rescaled by the rate alone.
97 if sn.procid(ist, k) == ProcessType.ME || sn.procid(ist, k) == ProcessType.RAP
98 ratio = map_mean(PH{ist}{k}) / (S(ist,k)/sn.nservers(ist));
99 PH{ist}{k} = {PH{ist}{k}{1}*ratio, PH{ist}{k}{2}*ratio};
100 else
101 PH{ist}{k} = map_scale(PH{ist}{k}, S(ist,k)/sn.nservers(ist));
102 end
103 pie{ist}{k} = map_pie(PH{ist}{k});
104 D0{ist,k} = PH{ist}{k}{1};
105 if any(isnan(D0{ist,k}))
106 D0{ist,k} = -GlobalConstants.Immediate;
107 pie{ist}{k} = 1;
108 PH{ist}{k} = map_exponential(GlobalConstants.Immediate);
109 end
110 end
111 end
112end % i
113
114isOpen = false;
115isClosed = false;
116if any(isinf(sn.njobs))
117 isOpen = true;
118end
119if any(isfinite(sn.njobs))
120 isClosed = true;
121end
122isMixed = isOpen & isClosed;
123
124% Self-looping classes never leave their reference station, so a chain made
125% up of them admits no surrogate arrival stream: the flow-equivalent rate
126% that would reproduce a permanently resident customer does not exist, and
127% the regula falsi below would drive it upwards without bound. Such chains
128% are excluded from the fixed point and pinned by the SLC clamp at the end
129% of this function, mirroring solver_mna_closed and
130% solver_mam_basic_mmap_closed.
131isslcchain = false(1,C);
132for c=1:C
133 isslcchain(c) = all(sn.isslc(sn.inchain{c}));
134end
135
136lambdas_inchain = cell(1,C);
137for c=1:C
138 inchain = sn.inchain{c};
139 lambdas_inchain{c} = sn.rates(sn.refstat(inchain(1)),inchain);
140 %lambdas_inchain{c} = lambdas_inchain{c}(isfinite(lambdas_inchain{c}));
141 lambda(c) = sum(lambdas_inchain{c}(isfinite(lambdas_inchain{c})));
142 if isslcchain(c)
143 lambda(c) = 0;
144 lambdas_inchain{c} = zeros(1,length(inchain));
145 end
146 ist = sn.refstat(inchain(1)); % identical for all classes in the chain
147 if isinf(sum(sn.njobs(inchain))) % if open chain
148 % ist here is the source
149 % assemble a MMAP for the arrival process from all classes
150 for k=1:K
151 if isnan(PH{ist}{k}{1})
152 PH{ist}{k} = map_exponential(Inf); % no arrivals from this class
153 end
154 end
155 inchain = sn.inchain{c};
156 k = inchain(1);
157 chainSysArrivals{c} = {PH{ist}{k}{1},PH{ist}{k}{2},PH{ist}{k}{2}};
158 for ki=2:length(inchain)
159 k = inchain(ki);
160 if isnan(PH{ist}{k}{1})
161 PH{ist}{k} = map_exponential(Inf); % no arrivals from this class
162 end
163 chainSysArrivals{c} = mmap_super_safe({chainSysArrivals{c},{PH{ist}{k}{1},PH{ist}{k}{2},PH{ist}{k}{2}}}, config.space_max, 'default');
164 end
165 TN(ist,inchain') = lambdas_inchain{c};
166 end
167end
168
169sd = isfinite(sn.nservers);
170
171isclosedchain = false(1,C);
172isopenchain = false(1,C);
173for c=1:C
174 isopenchain(c) = isinf(sum(sn.njobs(sn.inchain{c})));
175 isclosedchain(c) = ~isopenchain(c) && ~isslcchain(c);
176end
177% A mixed network is the only case in which the uniform 1/Umax backoff below is
178% unsafe, because there it also scales the open chains, whose arrival rate is
179% exogenous data fixed by their source rather than an unknown of the
180% decomposition. Purely open and purely closed networks keep the original
181% iteration untouched: with a single chain kind the uniform backoff is exactly
182% the closed-only backoff, and a closed chain is expected to drive its
183% bottleneck to saturation as its population grows.
184ismixed = any(isclosedchain) && any(isopenchain);
185% In a mixed network the closed chains may claim no more than the capacity the
186% open traffic leaves free, and the iterate must stay strictly inside the
187% stability region: the queue-length formulas below are of M/G/1 type and
188% diverge at total utilization 1.
189Ulim = 1 - GlobalConstants.CoarseTol;
190
191% Stations already flagged as carrying a non-phase-type service process, scoped
192% to this solve so the fixed-point iteration reports each station once while
193% every new model is still warned about.
194meWarned = false(1, M);
195
196while max(max(abs(TN-TN_1))) > tol && it <= options.iter_max %#ok<max>
197 it = it + 1;
198 TN_1 = TN;
199 Umax = max(sum(UN(sd,:),2));
200 if ismixed || Umax < 1
201 for c=1:C
202 inchain = sn.inchain{c};
203 if isclosedchain(c)
204 Nc = sum(sn.njobs(inchain)); % closed population
205 QNc = max(tol, sum(sum(QN(:,inchain),2,"omitnan"))); %#ok<NANSUM>
206 TNlb = Nc./sum(Lchain(:,c));
207 if it == 1
208 lambda(c) = TNlb; % lower bound
209 else
210 lambda(c) = lambda(c) * it/options.iter_max + (Nc / QNc) * lambda(c) * (options.iter_max-it)/options.iter_max; % iteration-averaged regula falsi;
211 end
212 end
213 end
214 end
215 if ismixed
216 % Back the closed chains alone onto the residual capacity of the
217 % busiest finite-server station. Scaling every chain by 1/Umax instead
218 % drags the open chains down too, so the open classes report a
219 % throughput below their own source rate, and it parks the iterate
220 % exactly on U==1, where the queue lengths diverge and, since the
221 % backoff then becomes a no-op, the iteration stops moving.
222 % Summed rather than multiplied out: with no open chain the empty
223 % product would collapse to 1x0 instead of a zero column.
224 Uchain = Lchain(sd,:) .* lambda;
225 Uopen = sum(Uchain(:,~isclosedchain), 2);
226 Uclosed = sum(Uchain(:, isclosedchain), 2);
227 binding = Uclosed > tol;
228 if any(binding)
229 theta = min((Ulim - Uopen(binding)) ./ Uclosed(binding));
230 if theta < 1
231 lambda(isclosedchain) = lambda(isclosedchain) * max(0, theta);
232 end
233 end
234 elseif Umax >= 1
235 lambda = lambda * 1/Umax;
236 end
237
238 for c=1:C
239 inchain = sn.inchain{c};
240 if isslcchain(c)
241 % Vanishing-rate surrogate, as in solver_mam_basic_mmap_inner: a
242 % self-looping chain must not inject arrivals into the shared-queue
243 % decomposition. An exactly zero rate would give a singular MMAP.
244 chainSysArrivals{c} = mmap_exponential(GlobalConstants.Zero);
245 elseif ~isinf(sum(sn.njobs(inchain)))
246 % Closed chain: Poisson surrogate at the current throughput
247 % iterate. Open chains keep the source arrival MMAP assembled
248 % before the loop (its rate is re-imposed per station by
249 % mmap_scale), so the external arrival variability is not
250 % silently discarded.
251 chainSysArrivals{c} = mmap_exponential(lambda(c));
252 end
253 for m=1:M
254 TN(m,inchain) = V(m,inchain) .* lambda(c);
255 end
256 end
257
258 for ind=1:I
259 if sn.isstation(ind)
260 ist = sn.nodeToStation(ind);
261 switch sn.nodetype(ind)
262 case NodeType.Join
263 for c=1:C
264 inchain = sn.inchain{c};
265 for k=inchain
266 fanin = nnz(sn.rtnodes(:, (ind-1)*K+k));
267 TN(ist,k) = lambda(c)*V(ist,k)/fanin;
268 UN(ist,k) = 0;
269 QN(ist,k) = 0;
270 RN(ist,k) = 0;
271 end
272 end
273 otherwise
274 switch sn.sched(ist)
275 case SchedStrategy.INF
276 for c=1:C
277 inchain = sn.inchain{c};
278 for k=inchain
279 if V(ist,k) == 0
280 % Class k does not visit this station, so
281 % its service rate is undefined (NaN) and
282 % all of its metrics here are null. Guard
283 % explicitly: S(ist,k)*TN(ist,k) would be
284 % NaN*0 = NaN, which then propagates into
285 % the Umax saturation test below (NaN>=1 is
286 % false, so the guard would be bypassed
287 % silently). Mirrors the V(ist,r)>0 test in
288 % solver_mam_basic_mmap_inner.
289 TN(ist,k) = 0;
290 UN(ist,k) = 0;
291 QN(ist,k) = 0;
292 RN(ist,k) = 0;
293 continue;
294 end
295 TN(ist,k) = lambda(c)*V(ist,k);
296 % Utilization Law: a c-server station holds TN*S/c of
297 % its capacity. An INF station has nservers = Inf, so
298 % dividing would annihilate it (S*TN/Inf = 0); an
299 % infinite server reports U = QLen = TN*S instead, the
300 % convention every other solver follows.
301 UN(ist,k) = S(ist,k)*TN(ist,k);
302 % TN already carries the visits, so Little's law reads
303 % QN = TN*S; a second V factor undercounts the delay
304 % jobs whenever the chain switches class (V<1), which
305 % skews the closed-chain regula falsi target QNc and,
306 % through the post-loop rescale, deflates the response
307 % time of the queueing stations in the same chain. The
308 % PS and FCFS branches already use the single-V form.
309 QN(ist,k) = TN(ist,k).*S(ist,k);
310 RN(ist,k) = QN(ist,k)/TN(ist,k);
311 end
312 end
313 case SchedStrategy.PS
314 for c=1:C
315 inchain = sn.inchain{c};
316 for k=inchain
317 if V(ist,k) == 0
318 % See the INF branch: a non-visiting class
319 % has an undefined rate, and NaN*0 = NaN
320 % would poison the Uden sum below.
321 TN(ist,k) = 0;
322 UN(ist,k) = 0;
323 continue;
324 end
325 TN(ist,k) = lambda(c)*V(ist,k);
326 % Utilization Law: a c-server station holds TN*S/c of its capacity.
327 UN(ist,k) = S(ist,k)*TN(ist,k)/sn.nservers(ist);
328 end
329 %Nc = sum(sn.njobs(inchain)); % closed population
330 Uden = min([1-GlobalConstants.FineTol,sum(UN(ist,:))]);
331 for k=inchain
332 if V(ist,k) == 0
333 QN(ist,k) = 0;
334 RN(ist,k) = 0;
335 continue;
336 end
337 %QN(ist,k) = (UN(ist,k)-UN(ist,k)^(Nc+1))/(1-Uden); % geometric bound type approximation
338 QN(ist,k) = UN(ist,k)/(1-Uden);
339 RN(ist,k) = QN(ist,k)/TN(ist,k);
340 end
341 end
342 case {SchedStrategy.FCFS, SchedStrategy.HOL, SchedStrategy.FCFSPRPRIO}
343 chainArrivalAtNode = cell(1,C);
344 rates = cell(M,C);
345 mapdcUsed = false; % Flag for exact MAP/D/c solver
346 finiteCapUsed = false; % Flag for finite-capacity truncate-and-renormalize
347 finiteCapLossPerClass = []; % per-class loss, exact MMAP/G/1/K branch only
348 for c=1:C %for each chain
349 rates{ist,c} = V(ist,:) .* lambda(c); % visits of classes within the chain
350 inchain = find(sn.chains(c,:))';
351 markProb = rates{ist,c}(inchain) / sum(rates{ist,c}(inchain));
352 markProb(isnan(markProb)) = 0;
353 chainArrivalAtNode{c} = mmap_mark(chainSysArrivals{c}, markProb);
354 % mmap_normalize clips negative entries to zero and
355 % re-derives the diagonal. On a genuine MAP that
356 % repairs roundoff; on a matrix-exponential or
357 % rational arrival it substitutes a different,
358 % Markovian process, which changes the
359 % autocorrelation that the downstream MMAPPH1FCFS
360 % is there to account for. Same reasoning as the
361 % ME/RAP branch of the service rescaling above.
362 if mam_chain_arrival_is_markovian(sn, c)
363 chainArrivalAtNode{c} = mmap_normalize(chainArrivalAtNode{c});
364 end
365 % Target the per-class mean inter-arrival times of
366 % THIS chain's classes. mmap_scale reads M(1:C)
367 % where C is the number of marks in the MMAP, i.e.
368 % length(inchain) here, so passing the full 1:K
369 % vector silently targets classes 1..length(inchain)
370 % instead of the chain's own classes. The two
371 % coincide only when the chain occupies the leading
372 % class indices; otherwise every chain but the first
373 % is scaled to another chain's visit rate.
374 % A chain that offers no arrivals here (a self-looping
375 % chain, whose lambda is pinned to zero) has an
376 % infinite target mean: mmap_scale would then scale the
377 % MMAP by mean/Inf = 0 and return an all-zero, singular
378 % MAP. It is already a vanishing-rate arrival process,
379 % so leave it untouched.
380 tgtrates = rates{ist,c}(inchain);
381 if any(tgtrates > 0)
382 chainArrivalAtNode{c} = mmap_scale(chainArrivalAtNode{c}, 1./tgtrates, 0); % non-iterative approximation
383 end
384 chainIsMarkovian = mam_chain_arrival_is_markovian(sn, c);
385 if c == 1
386 if chainIsMarkovian
387 aggrArrivalAtNode = mmap_super_safe({chainArrivalAtNode{c}, mmap_exponential(0,1)}, config.space_max, 'default');
388 aggrArrivalAtNode = {aggrArrivalAtNode{1} aggrArrivalAtNode{2} aggrArrivalAtNode{2}};
389 else
390 % Superposing with a zero-rate exponential is the
391 % identity on (D0,D1) -- the Kronecker sum with an
392 % order-1 null adds nothing -- and is used here only
393 % to reach the {D0,D1,D1} marked shape. What it also
394 % does is run mmap_normalize inside mmap_super, which
395 % clips the negative off-diagonal entries a
396 % matrix-exponential or rational arrival legitimately
397 % carries and substitutes a Markovian process with a
398 % different autocorrelation. Build the shape directly
399 % instead, so the arrival reaching MMAPPH1FCFS is the
400 % process the user declared.
401 aggrArrivalAtNode = {chainArrivalAtNode{c}{1}, chainArrivalAtNode{c}{2}, chainArrivalAtNode{c}{2}};
402 end
403 lc = map_lambda(chainArrivalAtNode{c});
404 if lc>0
405 aggrArrivalAtNode = mmap_scale(aggrArrivalAtNode, 1/lc, 0); % non-iterative approximation
406 end
407 else
408 if ~chainIsMarkovian
409 % mmap_super_safe normalizes, so the ME/RAP component
410 % of a multi-chain superposition is replaced by a
411 % Markovian one and its correlation is only
412 % approximated. Say so rather than report the result
413 % as if it carried the declared process.
414 line_warning_always(mfilename, ...
415 'Chain %d has a matrix-exponential or rational arrival process, which the superposition of several chains at station %s cannot represent exactly. Its autocorrelation is approximated by the closest Markovian arrival process.', ...
416 c, sn.nodenames{sn.stationToNode(ist)});
417 end
418 aggrArrivalAtNode = mmap_super_safe({aggrArrivalAtNode, chainArrivalAtNode{c}}, config.space_max, 'default');
419 end
420 end
421 Qret = cell(1,K);
422 if (sn.sched(ist)==SchedStrategy.HOL && any(sn.classprio ~= sn.classprio(1))) % if priorities are not identical; sched holds numeric ids so == must be used (strcmp on numerics is always false)
423 [uK,iK] = unique(sn.classprio);
424 % BUTools convention: D1=lowest priority, DK=highest priority
425 % LINE convention: lower value = higher priority
426 % unique() returns ascending order, so we need to reverse for BUTools
427 iK = flipud(iK(:));
428 if length(uK) == length(sn.classprio) % if all priorities are different
429 [Qret{iK'}] = MMAPPH1NPPR({aggrArrivalAtNode{[1;2+iK]}}, {pie{ist}{iK}}, {D0{ist,iK}}, 'ncMoms', 1);
430 else
431 line_error(mfilename,'Solver MAM requires either identical priorities or all distinct priorities');
432 end
433 elseif (sn.sched(ist)==SchedStrategy.FCFSPRPRIO && any(sn.classprio ~= sn.classprio(1))) % FCFS preemptive resume priority
434 [uK,iK] = unique(sn.classprio);
435 % BUTools convention: D1=lowest priority, DK=highest priority
436 % LINE convention: lower value = higher priority
437 % unique() returns ascending order, so we need to reverse for BUTools
438 iK = flipud(iK(:));
439 if length(uK) == length(sn.classprio) % if all priorities are different
440 [Qret{iK'}] = MMAPPH1PRPR({aggrArrivalAtNode{[1;2+iK]}}, {pie{ist}{iK}}, {D0{ist,iK}}, 'ncMoms', 1);
441 else
442 line_error(mfilename,'Solver MAM requires either identical priorities or all distinct priorities');
443 end
444 else
445 aggrUtil = sum(mmap_lambda(aggrArrivalAtNode)./(GlobalConstants.FineTol+sn.rates(ist,1:K)*sn.nservers(ist)), 'omitnan'); %% to debug
446 aggrLambda = mmap_lambda(aggrArrivalAtNode);
447 if aggrUtil < 1-GlobalConstants.FineTol
448 if any(isinf(N))
449 % Check for MAP/D/c: single-class open model with deterministic service
450 isMapDc = (K == 1) && (sn.procid(ist, 1) == ProcessType.DET);
451 % Check for D/M/c: single-class open, exp service at this queue,
452 % deterministic source elsewhere.
453 isDMc = false;
454 dmcSourceIdx = -1;
455 if K == 1 && ~isMapDc && sn.procid(ist, 1) == ProcessType.EXP
456 for jst = 1:sn.nstations
457 if jst ~= ist && sn.procid(jst, 1) == ProcessType.DET
458 isDMc = true;
459 dmcSourceIdx = jst;
460 break;
461 end
462 end
463 end
464 % Check for PH/M/c: single-class open, exp service at this queue,
465 % source has PH inter-arrivals (Erlang/Coxian/MAP/PH/...).
466 % Covers both c=1 (PH/M/1) and c>1 (PH/M/c via matrix-geometric).
467 isPhM1 = false;
468 phM1SourceIdx = -1;
469 if K == 1 && ~isMapDc && ~isDMc && sn.procid(ist, 1) == ProcessType.EXP ...
470 && isfinite(sn.nservers(ist)) && sn.nservers(ist) >= 1
471 for jst = 1:sn.nstations
472 if jst == ist
473 continue;
474 end
475 srcProc = sn.procid(jst, 1);
476 if srcProc ~= ProcessType.EXP && srcProc ~= ProcessType.DET ...
477 && srcProc ~= ProcessType.IMMEDIATE && srcProc ~= ProcessType.DISABLED
478 % qsys_phmc reads the arrival only through
479 % its (pie, D0) phase-type pair, i.e. its
480 % renewal marginal. That is exact for a
481 % renewal arrival and discards the
482 % autocorrelation of any other, so the gate
483 % is RENEWAL, not merely non-exponential.
484 % Being non-exponential was the old test and
485 % it let a correlated MAP, RAP or ME arrival
486 % onto the closed form: the answer then came
487 % out near the Kingman value implied by the
488 % arrival SCV alone. An ME arrival is renewal
489 % and stays on this fast path.
490 if ~mam_srcproc_is_renewal(sn, jst)
491 continue;
492 end
493 isPhM1 = true;
494 phM1SourceIdx = jst;
495 break;
496 elseif srcProc == ProcessType.EXP && sn.nservers(ist) > 1 ...
497 && sn.nstations == 2 ...
498 && sn.nodetype(sn.stationToNode(jst)) == NodeType.Source
499 % M/M/c: the single-fast-server surrogate used by the
500 % generic path is inexact for c>1; dispatch to the exact
501 % PH/M/c matrix-geometric solver (Erlang-C result)
502 isPhM1 = true;
503 phM1SourceIdx = jst;
504 break;
505 end
506 end
507 end
508 % Check for finite-buffer FCFS. qsys_phmc, qsys_dmc
509 % and qsys_mapdc are all infinite-buffer closed
510 % forms, so letting them win here would silently
511 % discard sn.cap and report a lossless queue. A
512 % finite buffer therefore takes precedence over
513 % them rather than being disabled by them.
514 isFiniteCap = isfinite(sn.cap(ist));
515 if isFiniteCap
516 isPhM1 = false;
517 isDMc = false;
518 isMapDc = false;
519 end
520 if isPhM1
521 muQ = 1.0 / S(ist, 1);
522 try
523 phPair = sn.proc{phM1SourceIdx}{1};
524 D0_ph = phPair{1};
525 D1_ph = phPair{2};
526 pie_src = map_pie({D0_ph, D1_ph});
527 phm1Result = qsys_phmc(pie_src, D0_ph, muQ, sn.nservers(ist));
528 Qret{1} = phm1Result.meanQueueLength;
529 mapdcUsed = true;
530 mapdcStations(ist) = true;
531 line_debug(options, 'Using exact PH/M/%d solver: Q=%.4f, W=%.4f', ...
532 sn.nservers(ist), phm1Result.meanQueueLength, phm1Result.meanWaitingTime);
533 catch
534 isPhM1 = false;
535 end
536 end
537 if isDMc
538 muQ = 1.0 / S(ist, 1);
539 lamD = sn.rates(dmcSourceIdx, 1);
540 try
541 dmcResult = qsys_dmc(lamD, muQ, sn.nservers(ist));
542 Qret{1} = dmcResult.meanQueueLength;
543 mapdcUsed = true; % skip surrogate delay correction
544 mapdcStations(ist) = true;
545 line_debug(options, 'Using exact D/M/%d solver: Q=%.4f, W=%.4f', ...
546 sn.nservers(ist), dmcResult.meanQueueLength, dmcResult.meanWaitingTime);
547 catch
548 isDMc = false;
549 end
550 end
551 if isPhM1 || isDMc
552 % handled above; skip the MAP/D/c, finite-cap, and MMAPPH1FCFS branches
553 elseif isMapDc
554 % Use exact MAP/D/c solver from Q-MAM
555 D0_arr = aggrArrivalAtNode{1};
556 D1_arr = aggrArrivalAtNode{2};
557 detServiceTime = S(ist, 1); % 1/rate = service time
558 numServers = sn.nservers(ist);
559
560 mapdcResult = qsys_mapdc(D0_arr, D1_arr, detServiceTime, numServers);
561 Qret{1} = mapdcResult.meanQueueLength;
562 % Store result for later use to skip surrogate delay adjustment
563 mapdcUsed = true;
564 mapdcStations(ist) = true; % Mark for skipping post-processing
565 line_debug('Using exact MAP/D/%d solver: Q=%.4f, W=%.4f', ...
566 numServers, mapdcResult.meanQueueLength, mapdcResult.meanWaitingTime);
567 elseif isFiniteCap
568 % Finite-buffer FCFS. Detect exact M/M/c/K
569 % (Poisson arrivals + same exp service across
570 % classes); else use MMAP[K]/PH[K]/1/FCFS
571 % truncate-and-renormalize approximation.
572 mapdcUsed = false;
573 capK = sn.cap(ist);
574 finiteCapLossPerClass = [];
575 [isMmck, muMmck] = mam_detect_mmck(sn, ist, K, aggrArrivalAtNode);
576 if isMmck
577 aggrLambdaTotal = sum(aggrLambda, 'omitnan');
578 exactRes = qsys_mmck(aggrLambdaTotal, muMmck, sn.nservers(ist), capK);
579 finiteCapMeanQ = exactRes.meanQueueLength;
580 finiteCapLossProb = exactRes.lossProbability;
581 finiteCapP0 = exactRes.queueLengthDist(1);
582 line_debug('Using exact M/M/%d/%d: Q=%.4f, ploss=%.4f', ...
583 sn.nservers(ist), capK, finiteCapMeanQ, finiteCapLossProb);
584 elseif sn.nservers(ist) == 1
585 % Exact MMAP[K]/G/1/K. The embedded chain at
586 % departure epochs resolves the buffer level
587 % jointly with the arrival phase, so class k,
588 % which arrives out of phase i at rate
589 % (D1k*e)_i, gets its own loss ratio. The
590 % truncate-renorm fallback below can only
591 % return one blocking probability for every
592 % class, making loss rate-proportional and
593 % blind to interarrival variability.
594 svcMix = mam_svc_mixture({aggrArrivalAtNode{[1,3:end]}}, ...
595 {pie{ist}{:}}, {D0{ist,:}});
596 exRes = qsys_mmapg1k(aggrArrivalAtNode{1}, ...
597 aggrArrivalAtNode(3:end), svcMix, capK);
598 finiteCapMeanQ = exRes.meanQueueLength;
599 finiteCapLossProb = exRes.lossAggregate;
600 finiteCapLossPerClass = exRes.lossRatio;
601 finiteCapP0 = exRes.p0;
602 line_debug('Using exact MMAP/G/1/%d: Q=%.4f, ploss=%.4f, per-class ploss=%s', ...
603 capK, finiteCapMeanQ, finiteCapLossProb, mat2str(finiteCapLossPerClass, 4));
604 else
605 [meanQ_fc, lossProb_fc, p_norm_fc] = mam_truncate_renorm( ...
606 {aggrArrivalAtNode{[1,3:end]}}, {pie{ist}{:}}, {D0{ist,:}}, capK);
607 finiteCapMeanQ = meanQ_fc;
608 finiteCapLossProb = lossProb_fc;
609 finiteCapP0 = p_norm_fc(1);
610 line_debug('Using MMAP/PH/%d/%d truncate-renorm: Q=%.4f, ploss=%.4f', ...
611 sn.nservers(ist), capK, meanQ_fc, lossProb_fc);
612 end
613 finiteCapUsed = true;
614 for k=1:K
615 Qret{k} = NaN; % filled per class in result loop
616 end
617 mapdcStations(ist) = true; % skip surrogate delay 2nd pass
618 elseif sn.isfunction(ist)
619 % Open setup/delay-off station. qbd_setupdelayoff
620 % races the delay-off against the aggregate
621 % interarrival Exp(lambda), which is exactly the
622 % idle seen by a Poisson stream, so it is the right
623 % surrogate here (unlike the closed case below,
624 % where the idle is per instance). Without this the
625 % station fell through to the plain MMAPPH1FCFS
626 % below and the setup was silently ignored: an
627 % M/M/1 with setup returned the setup-free
628 % rho/(1-rho).
629 mapdcUsed = false;
630 % nodeparam is NODE-indexed (refreshLocalVars), while
631 % ist is a STATION index; the two coincide only when
632 % no non-station node precedes the queue. Reading
633 % nodeparam{ist} directly errored out on any model
634 % with e.g. a Router or ClassSwitch ahead of the
635 % setup station.
636 alpharate = map_lambda(sn.nodeparam{sn.stationToNode(ist)}{end}.setupTime);
637 alphascv = map_scv(sn.nodeparam{sn.stationToNode(ist)}{end}.setupTime);
638 betarate = map_lambda(sn.nodeparam{sn.stationToNode(ist)}{end}.delayoffTime);
639 betascv = map_scv(sn.nodeparam{sn.stationToNode(ist)}{end}.delayoffTime);
640 mu_k = zeros(1, K);
641 lambda_k = zeros(1, K);
642 active_k = false(1, K);
643 for k=1:K
644 pie_k = pie{ist}{k};
645 if ~isempty(pie_k) && ~isnan(pie_k(1))
646 mu_k(k) = 1 / (pie_k * inv(-D0{ist,k}) * ones(size(pie_k))');
647 c = find(sn.chains(:,k), 1);
648 lambda_k(k) = rates{ist,c}(k);
649 active_k(k) = true;
650 end
651 end
652 rho_k = lambda_k ./ mu_k;
653 rho_k(~active_k) = 0;
654 rho_total = sum(rho_k);
655 if rho_total > 0
656 % Aggregate service rate consistent with the
657 % aggregate load, then split the aggregate queue
658 % across classes in proportion to their load.
659 % aggrLambda is per class; the QBD takes the
660 % scalar total arrival rate seen by the server.
661 aggrLambdaTotal = sum(aggrLambda, 'omitnan');
662 aggrRate = aggrLambdaTotal / rho_total;
663 Q_total = qbd_setupdelayoff(aggrLambdaTotal, aggrRate, alpharate, alphascv, betarate, betascv);
664 for k=1:K
665 if active_k(k)
666 Qret{k} = Q_total * rho_k(k) / rho_total;
667 else
668 Qret{k} = 0;
669 end
670 end
671 else
672 for k=1:K
673 Qret{k} = 0;
674 end
675 end
676 else
677 mapdcUsed = false;
678 % MMAPPH1FCFS treats service as a renewal
679 % phase-type (marginal), discarding service
680 % autocorrelation. For the single-class
681 % single-server M/MAP/1 case with a genuinely
682 % correlated service, use the exact MAP/MAP/1
683 % queue (Q_CT_MAP_MAP_1), which carries the
684 % service phase across departures.
685 % A matrix-exponential or rational service
686 % process is not phase-type, so the (pie,D0)
687 % pair MMAPPH1FCFS reads does not describe it
688 % and the PH path would silently return the
689 % wrong numbers. The RAP/RAP/1 QBD applies
690 % whenever the SERVICE is ME or RAP, whatever
691 % the arrival type: the aggregate arrival
692 % stream is handed over as a (D0,D1) pair, and
693 % a Poisson stream, a MAP and a PH renewal
694 % stream are all RAPs.
695 isMEorRAPsvc = any(sn.procid(ist,:) == ProcessType.ME | ...
696 sn.procid(ist,:) == ProcessType.RAP);
697 useRapRap1 = false;
698 if isMEorRAPsvc
699 if (K == 1) && (sn.nservers(ist) == 1)
700 useRapRap1 = true;
701 else
702 % qbd_raprap1 is a single-class
703 % single-server QBD. Say so rather than
704 % let an ME model be answered by the
705 % phase-type approximation without the
706 % user knowing.
707 % line_warning_always, not
708 % line_warning: this reports that
709 % the returned numbers are not
710 % exact, so it must be raised for
711 % every model it applies to rather
712 % than be hidden as a repeat of the
713 % previous model's warning.
714 if ~meWarned(ist)
715 meWarned(ist) = true;
716 line_warning_always(mfilename, ...
717 'Station %s has a matrix-exponential or rational service process, which the RAP/RAP/1 analysis supports only with a single class at a single server (here %d classes, %g servers). Falling back to the phase-type approximation MMAPPH1FCFS, which is not exact for this service process.', ...
718 sn.nodenames{sn.stationToNode(ist)}, K, sn.nservers(ist));
719 end
720 end
721 end
722 useMapMap1 = ~useRapRap1 && (K == 1) && (sn.nservers(ist) == 1) && ...
723 (abs(map_acf(PH{ist}{1}, 1)) > GlobalConstants.CoarseTol);
724 if useRapRap1
725 % qbd_raprap1 carries no sojourn-time
726 % information, so only the queue-length
727 % mean is taken here; the response time is
728 % recovered downstream by Little's law.
729 [~, QNrap] = qbd_raprap1({aggrArrivalAtNode{1}, aggrArrivalAtNode{3}}, ...
730 {PH{ist}{1}{1}, PH{ist}{1}{2}});
731 Qret{1} = QNrap;
732 elseif useMapMap1
733 ql = Q_CT_MAP_MAP_1(aggrArrivalAtNode{1}, aggrArrivalAtNode{3}, ...
734 PH{ist}{1}{1}, PH{ist}{1}{2}, 'MaxNumComp', 100000);
735 ql = ql(:);
736 Qret{1} = sum((0:numel(ql)-1)' .* ql);
737 else
738 [Qret{1:K}] = MMAPPH1FCFS({aggrArrivalAtNode{[1,3:end]}}, {pie{ist}{:}}, {D0{ist,:}}, 'ncMoms', 1);
739 end
740 end
741 else % all closed classes
742 maxLevel = sum(N(isfinite(N)))+1;
743 D = {aggrArrivalAtNode{[1,3:end]}};
744 pdistr_k = cell(1,K);
745 if map_lambda(D)< GlobalConstants.FineTol
746 for k=1:K
747 pdistr_k = [1-GlobalConstants.FineTol, GlobalConstants.FineTol];
748 Qret{k} = GlobalConstants.FineTol / sn.rates(ist);
749 end
750 else
751 if sn.isfunction(ist)
752 alpharate = map_lambda(sn.nodeparam{sn.stationToNode(ist)}{end}.setupTime);
753 betarate = map_lambda(sn.nodeparam{sn.stationToNode(ist)}{end}.delayoffTime);
754 betascv = map_scv(sn.nodeparam{sn.stationToNode(ist)}{end}.delayoffTime);
755 lambda_k = zeros(1, K);
756 active_k = false(1, K);
757 for k=1:K
758 pie_k = pie{ist}{k};
759 if ~isnan(pie_k(1))
760 c = find(sn.chains(:,k), 1);
761 lambda_k(k) = rates{ist,c}(k);
762 active_k(k) = true;
763 end
764 end
765 if any(active_k)
766 % Per-instance cold-start race. In a closed layer the
767 % function instances are the chain's own tokens: each
768 % token's idle period between two consecutive requests
769 % is the client think time ZT of its chain (the
770 % exponential surrogate at the infinite-server client
771 % delay), so an arriving request pays the setup iff the
772 % delay-off timer expires within Exp(1/ZT):
773 % p_cold = P(delayoff < Exp(1/ZT)) = LST_delayoff(1/ZT).
774 % The response time then carries the full setup latency,
775 % R_k = p_cold*E[setup] + S_k,
776 % and QN follows by Little's law, which keeps the
777 % regula falsi above linear in lambda. The open-arrival
778 % surrogate qbd_setupdelayoff(aggrLambda,...) is not
779 % used here: it races the delay-off against the
780 % aggregate interarrival Exp(lambda) instead of the
781 % per-instance idle, and its Poisson clumping creates
782 % queueing that cannot occur among closed tokens.
783 setupMean = 1/alpharate;
784 if betascv == 1.0
785 % rate taken directly, as in qbd_setupdelayoff
786 betaProc = {-betarate, betarate};
787 else
788 betaProc = APH.fitMeanAndSCV(1/betarate, betascv).getProcess;
789 end
790 Tb = betaProc{1};
791 pie_b = map_pie(betaProc);
792 infstat = isinf(sn.nservers);
793 for k=1:K
794 if active_k(k) && lambda_k(k) > 0 && isfinite(S(ist,k))
795 c = find(sn.chains(:,k), 1);
796 inchain_k = find(sn.chains(c,:));
797 % Lchain and V are per chain cycle; the token
798 % visits this station sum(V(ist,inchain)) times
799 % per cycle, so the idle between two consecutive
800 % requests to an instance is the chain think
801 % demand per visit.
802 Vtot = sum(V(ist, inchain_k));
803 ZT = sum(Lchain(infstat, c), 'omitnan') / max(Vtot, GlobalConstants.FineTol);
804 nu = 1 / max(ZT, GlobalConstants.FineTol);
805 pcold = pie_b * ((nu*eye(size(Tb,1)) - Tb) \ (-Tb*ones(size(Tb,1),1)));
806 % service part uses S/nservers: the surrogate
807 % delay correction below adds the remaining
808 % S*(nservers-1)/nservers, restoring R_k =
809 % p_cold*E[setup] + S_k.
810 Qret{k} = lambda_k(k) * (pcold*setupMean + S(ist,k)/sn.nservers(ist));
811 elseif active_k(k)
812 % A class that offers no load here (zero visits,
813 % or no service process: the chain's client-delay
814 % token class) holds no jobs; 0*NaN would stamp
815 % NaN into QN, void the unguarded chain sum QNc
816 % of the second pass, and the NaN-ignoring max()
817 % in the response-time fallback would silently
818 % report the bare service time for the served
819 % classes.
820 Qret{k} = 0;
821 else
822 % An inactive class holds no jobs here. NaN
823 % would propagate into the chain sum used by
824 % the closed-population renormalization below
825 % (QNc = sum(sum(QN(:,inchain)))), turning the
826 % whole row NaN and discarding the queue length;
827 % the NaN-ignoring max() in the response-time
828 % fallback would then silently report the bare
829 % service time. The MMAPPH1FCFS path likewise
830 % yields 0 for a zero-population class.
831 Qret{k} = 0;
832 end
833 end
834 else
835 for k=1:K
836 Qret{k} = NaN;
837 end
838 end
839 else
840 % MMAPPH1FCFS returns one queue-length
841 % distribution per class (see the for k=1:K loop
842 % around its 'ncDistr' branch), so all K outputs
843 % must be captured. Requesting a single output
844 % yields class 1's distribution alone, which was
845 % then reused for every class: Qret depended on
846 % the class only through N(k), so two classes
847 % with equal populations but different visit
848 % ratios were handed the same queue length, and
849 % the throughput fixed point resolved that by
850 % driving every chain to the same lambda.
851 pdistr_all = cell(1,K);
852 [pdistr_all{1:K}] = MMAPPH1FCFS(D, {pie{ist}{:}}, {D0{ist,:}}, 'ncDistr', maxLevel);
853 for k=1:K
854 pdistr = pdistr_all{k};
855 pdistr_k = abs(pdistr(1:(N(k)+1)));
856 % Truncating at the class population folds
857 % the tail beyond level N(k) into that level,
858 % so the mean below is E[min(Q_k,N(k))]. The
859 % complement must therefore be taken over the
860 % truncated vector: reading it from the full
861 % distribution instead assigns only the mass
862 % above N(k), i.e. P(Q>N(k)) rather than
863 % P(Q>=N(k)), losing the levels in between.
864 pdistr_k(end) = abs(1-sum(pdistr_k(1:end-1)));
865 pdistr_k = pdistr_k / sum(pdistr_k(1:(N(k)+1)));
866 Qret{k} = max(0,min(N(k),(0:N(k))*pdistr_k(1:(N(k)+1))'));
867 end
868 end
869 end
870 end
871 else
872 for k=1:K
873 Qret{k} = sn.njobs(k);
874 end
875 end
876 end
877 if finiteCapUsed
878 % Per-class decomposition under FCFS: wait time in queue is
879 % uniform across classes, so per-class response time is
880 % R_k = W_q + S_k. From E[N_total] (truncate-renorm) and
881 % per-class effective rates lambda_k_eff = lambda_k*(1-ploss):
882 % W_q = E[N_total]/lambda_total_eff - sum(lambda_k_eff*S_k)/lambda_total_eff
883 % N_k = lambda_k_eff * R_k
884 lambdaInflow = zeros(1, K);
885 for k=1:K
886 cidx = find(sn.chains(:,k),1);
887 lambdaInflow(k) = rates{ist,cidx}(k);
888 end
889 lambdaInflow(isnan(lambdaInflow)) = 0;
890 if ~isempty(finiteCapLossPerClass)
891 % Exact MMAP/G/1/K branch: loss differs by class
892 TN_eff = lambdaInflow .* (1 - finiteCapLossPerClass(1:K));
893 else
894 TN_eff = lambdaInflow * (1 - finiteCapLossProb);
895 end
896 sumTN = sum(TN_eff);
897 if sumTN > 0
898 Savg_eff = sum(TN_eff .* S(ist,1:K), 'omitnan') / sumTN;
899 Wq = max(0, finiteCapMeanQ / sumTN - Savg_eff);
900 else
901 Wq = 0;
902 end
903 for k=1:K
904 TN(ist,k) = TN_eff(k);
905 UN(ist,k) = TN(ist,k) * S(ist,k) / sn.nservers(ist);
906 if TN(ist,k) > 0
907 RN(ist,k) = Wq + S(ist,k);
908 QN(ist,k) = TN(ist,k) * RN(ist,k);
909 else
910 RN(ist,k) = 0;
911 QN(ist,k) = 0;
912 end
913 end
914 else
915 QN(ist,:) = cell2mat(Qret);
916 for k=1:K
917 c = find(sn.chains(:,k),1);
918 TN(ist,k) = rates{ist,c}(k);
919 UN(ist,k) = TN(ist,k) * S(ist,k) / sn.nservers(ist);
920 if sn.isfunction(ist) && ~isfinite(UN(ist,k))
921 % A class with no service process here (the chain's
922 % client-delay token class) has S = NaN, and NaN*0
923 % poisons the Umax saturation gate above, freezing
924 % the closed-chain regula falsi at the no-contention
925 % bound TNlb from it=2. At a setup-bearing function
926 % station the iteration must keep moving so lambda
927 % reaches QNc = Nc with the cold-start queue length
928 % included; the class contributes no utilization.
929 % Elsewhere the NaN freeze is load-bearing (it pins
930 % LN class-switching host layers) and is preserved.
931 UN(ist,k) = 0;
932 end
933 QN(ist,k) = Qret{k};
934 if mapdcUsed
935 % For MAP/D/c, D/M/c, or PH/M/1, use exact results
936 % (no surrogate delay adjustment).
937 if isPhM1
938 RN(ist,k) = phm1Result.meanSojournTime;
939 elseif isDMc
940 RN(ist,k) = dmcResult.meanSojournTime;
941 else
942 RN(ist,k) = mapdcResult.meanSojournTime;
943 end
944 else
945 % add number of jobs at the surrogate delay server.
946 % A class with no service process here has S=NaN, and
947 % the correction is nil for it (TN=0) and for a single
948 % server ((nservers-1)/nservers=0), but IEEE makes
949 % NaN*0 = NaN, which would propagate into the chain sum
950 % QNc below, void the whole row and, via the NaN-ignoring
951 % max() in the response-time fallback, silently degrade
952 % the result to the bare service time.
953 if isfinite(S(ist,k))
954 QN(ist,k) = QN(ist,k) + TN(ist,k)*S(ist,k) * (sn.nservers(ist)-1)/sn.nservers(ist);
955 end
956 RN(ist,k) = QN(ist,k) ./ TN(ist,k);
957 end
958 end
959 end
960 end
961 end
962 else % not a station
963 switch sn.nodetype(ind)
964 case NodeType.Fork
965 % line_error(mfilename,'Fork nodes not supported yet by MAM solver.');
966 end
967 end
968 end
969 %it
970 %max(max(abs(TN-TN_1)))
971end
972totiter = it + 2;
973CN = sum(RN,1);
974QN = abs(QN);
975for it=1:2 % second pass to rescale again QN based on RN correction
976 for c=1:C
977 inchain = sn.inchain{c};
978 Nc = sum(sn.njobs(inchain));
979 if isfinite(Nc)
980 QNc = sum(sum(QN(:,inchain)));
981 QN(:,inchain) = QN(:,inchain) * (Nc / QNc);
982 end
983 for ind=1:I
984 for k=inchain
985 if sn.isstation(ind)
986 ist = sn.nodeToStation(ind);
987 % Skip stations using exact MAP/D/c solver (already have exact values)
988 if mapdcStations(ist)
989 continue;
990 end
991 if V(ist,k)>0
992 if isinf(sn.nservers(ist))
993 RN(ist,k) = S(ist,k);
994 else
995 RN(ist,k) = max([S(ist,k), QN(ist,k) ./ TN(ist,k)]);
996 end
997 else
998 RN(ist,k) = 0;
999 end
1000 QN(ist,k) = RN(ist,k) .* TN(ist,k);
1001 end
1002 end
1003 end
1004 Nc = sum(sn.njobs(inchain)); % closed population
1005 if Nc == 0 % if closed chain
1006 QN(:,c)=0;
1007 UN(:,c)=0;
1008 RN(:,c)=0;
1009 TN(:,c)=0;
1010 CN(c)=0;
1011 XN(c)=0;
1012 end
1013 end
1014end
1015
1016% SLC clamp: a self-looping class holds its njobs customers at its reference
1017% station in perpetuity, so its metrics follow in closed form and supersede
1018% whatever the decomposition produced. Applied last, after the queue-length
1019% rescaling above, so that it is not undone.
1020%
1021% At a queueing station the class is permanently backlogged, hence a
1022% work-conserving server never idles and the class takes exactly the capacity
1023% the other classes leave free: U_slc = 1 - sum_j U_j. This is exact whenever
1024% the other classes' utilizations are, and it reproduces the CTMC solution of
1025% the sanity SLC models to 5 significant digits. Note that solver_mna_closed
1026% and solver_mam_basic_mmap_closed instead pin U_slc = S*njobs*rate, which
1027% assumes each of the njobs customers owns a server and returns U>1 as soon as
1028% njobs exceeds nservers; that form is deliberately not reproduced here.
1029slcAll = find(sn.isslc);
1030if ~isempty(slcAll)
1031 % Utilization is the fraction of server time a class actually consumes, so
1032 % it follows from the declared service time. The inflation applied above
1033 % models the interference the self-looping customers cause in the queueing
1034 % delay, not extra work performed by the server, and must not be reported as
1035 % utilization (nor fed to the leftover-capacity identity below).
1036 for ist=1:M
1037 if slcjobs(ist) > 0
1038 for k=1:K
1039 if ~sn.isslc(k)
1040 UN(ist,k) = Strue(ist,k)*TN(ist,k);
1041 end
1042 end
1043 end
1044 end
1045 QN(:,slcAll) = 0;
1046 UN(:,slcAll) = 0;
1047 RN(:,slcAll) = 0;
1048 TN(:,slcAll) = 0;
1049 for ist=1:M
1050 slck = slcAll(sn.refstat(slcAll) == ist);
1051 if isempty(slck)
1052 continue;
1053 end
1054 if isinf(sn.nservers(ist))
1055 % Delay station: no contention, every customer is always in
1056 % service, so the class completes at its full aggregate rate.
1057 for k=slck(:)'
1058 QN(ist,k) = sn.njobs(k);
1059 TN(ist,k) = sn.njobs(k)*sn.rates(ist,k);
1060 RN(ist,k) = Strue(ist,k);
1061 UN(ist,k) = Strue(ist,k)*TN(ist,k);
1062 end
1063 else
1064 nsrv = sn.nservers(ist);
1065 Uleft = max(0, 1 - sum(UN(ist,~sn.isslc)));
1066 % Several self-looping classes at one station share the free
1067 % capacity in proportion to the service rate they offer.
1068 w = sn.njobs(slck(:)').*sn.rates(ist,slck(:)');
1069 if sum(w) <= 0
1070 continue;
1071 end
1072 for i=1:numel(slck)
1073 k = slck(i);
1074 UN(ist,k) = min(Uleft*w(i)/sum(w), min(sn.njobs(k),nsrv)/nsrv);
1075 TN(ist,k) = sn.rates(ist,k)*UN(ist,k)*nsrv;
1076 QN(ist,k) = sn.njobs(k);
1077 if TN(ist,k) > 0
1078 RN(ist,k) = QN(ist,k) ./ TN(ist,k);
1079 else
1080 RN(ist,k) = 0;
1081 end
1082 end
1083 end
1084 end
1085 CN = sum(RN,1);
1086end
1087end
Definition fjtag.m:157
Definition Station.m:265