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;
23Lchain = sn_get_demands_chain(sn);
24
25QN = zeros(M,K);
26UN = zeros(M,K);
27RN = zeros(M,K);
28TN = zeros(M,K);
29CN = zeros(1,K);
30XN = zeros(1,K);
31
32% Track stations using exact MAP/D/c solver (skip post-processing for these)
33mapdcStations = false(M, 1);
34
35pie = {};
36D0 = {};
37
38lambda = zeros(1,C);
39chainSysArrivals = cell(1,C);
40TN_1 = TN+Inf;
41
42it = 0;
43
44% open queueing system (one node is the external world)
45% first build the joint arrival process
46for ist=1:M
47 switch sn.sched(ist)
48 case {SchedStrategy.FCFS, SchedStrategy.HOL, SchedStrategy.FCFSPRPRIO, SchedStrategy.PS}
49 for k=1:K
50 % Skip Det processes - they will be handled by qsys_mapdc
51 if sn.procid(ist, k) == ProcessType.DET
52 % For Det, we don't need MAP representation since we use exact solver
53 pie{ist}{k} = [];
54 D0{ist,k} = [];
55 continue;
56 end
57 % divide service time by number of servers and put
58 % later a surrogate delay server in tandem to compensate
59 PH{ist}{k} = map_scale(PH{ist}{k}, S(ist,k)/sn.nservers(ist));
60 pie{ist}{k} = map_pie(PH{ist}{k});
61 D0{ist,k} = PH{ist}{k}{1};
62 if any(isnan(D0{ist,k}))
63 D0{ist,k} = -GlobalConstants.Immediate;
64 pie{ist}{k} = 1;
65 PH{ist}{k} = map_exponential(GlobalConstants.Immediate);
66 end
67 end
68 end
69end % i
70
71isOpen = false;
72isClosed = false;
73if any(isinf(sn.njobs))
74 isOpen = true;
75end
76if any(isfinite(sn.njobs))
77 isClosed = true;
78end
79isMixed = isOpen & isClosed;
80
81lambdas_inchain = cell(1,C);
82for c=1:C
83 inchain = sn.inchain{c};
84 lambdas_inchain{c} = sn.rates(sn.refstat(inchain(1)),inchain);
85 %lambdas_inchain{c} = lambdas_inchain{c}(isfinite(lambdas_inchain{c}));
86 lambda(c) = sum(lambdas_inchain{c}(isfinite(lambdas_inchain{c})));
87 ist = sn.refstat(inchain(1)); % identical for all classes in the chain
88 if isinf(sum(sn.njobs(inchain))) % if open chain
89 % ist here is the source
90 % assemble a MMAP for the arrival process from all classes
91 for k=1:K
92 if isnan(PH{ist}{k}{1})
93 PH{ist}{k} = map_exponential(Inf); % no arrivals from this class
94 end
95 end
96 inchain = sn.inchain{c};
97 k = inchain(1);
98 chainSysArrivals{c} = {PH{ist}{k}{1},PH{ist}{k}{2},PH{ist}{k}{2}};
99 for ki=2:length(inchain)
100 k = inchain(ki);
101 if isnan(PH{ist}{k}{1})
102 PH{ist}{k} = map_exponential(Inf); % no arrivals from this class
103 end
104 chainSysArrivals{c} = mmap_super_safe({chainSysArrivals{c},{PH{ist}{k}{1},PH{ist}{k}{2},PH{ist}{k}{2}}}, config.space_max, 'default');
105 end
106 TN(ist,inchain') = lambdas_inchain{c};
107 end
108end
109
110sd = isfinite(sn.nservers);
111
112while max(max(abs(TN-TN_1))) > tol && it <= options.iter_max %#ok<max>
113 it = it + 1;
114 TN_1 = TN;
115 Umax = max(sum(UN(sd,:),2));
116 if Umax >=1
117 lambda = lambda * 1/Umax;
118 else
119 for c=1:C
120 inchain = sn.inchain{c};
121 if ~isinf(sum(sn.njobs(inchain))) % if closed chain
122 Nc = sum(sn.njobs(inchain)); % closed population
123 QNc = max(tol, sum(sum(QN(:,inchain),2,"omitnan"))); %#ok<NANSUM>
124 TNlb = Nc./sum(Lchain(:,c));
125 if it == 1
126 lambda(c) = TNlb; % lower bound
127 else
128 lambda(c) = lambda(c) * it/options.iter_max + (Nc / QNc) * lambda(c) * (options.iter_max-it)/options.iter_max; % iteration-averaged regula falsi;
129 end
130 end
131 end
132 end
133
134 for c=1:C
135 inchain = sn.inchain{c};
136 chainSysArrivals{c} = mmap_exponential(lambda(c));
137 for m=1:M
138 TN(m,inchain) = V(m,inchain) .* lambda(c);
139 end
140 end
141
142 for ind=1:I
143 if sn.isstation(ind)
144 ist = sn.nodeToStation(ind);
145 switch sn.nodetype(ind)
146 case NodeType.Join
147 for c=1:C
148 inchain = sn.inchain{c};
149 for k=inchain
150 fanin = nnz(sn.rtnodes(:, (ind-1)*K+k));
151 TN(ist,k) = lambda(c)*V(ist,k)/fanin;
152 UN(ist,k) = 0;
153 QN(ist,k) = 0;
154 RN(ist,k) = 0;
155 end
156 end
157 otherwise
158 switch sn.sched(ist)
159 case SchedStrategy.INF
160 for c=1:C
161 inchain = sn.inchain{c};
162 for k=inchain
163 TN(ist,k) = lambda(c)*V(ist,k);
164 UN(ist,k) = S(ist,k)*TN(ist,k);
165 QN(ist,k) = TN(ist,k).*S(ist,k)*V(ist,k);
166 RN(ist,k) = QN(ist,k)/TN(ist,k);
167 end
168 end
169 case SchedStrategy.PS
170 for c=1:C
171 inchain = sn.inchain{c};
172 for k=inchain
173 TN(ist,k) = lambda(c)*V(ist,k);
174 UN(ist,k) = S(ist,k)*TN(ist,k);
175 end
176 %Nc = sum(sn.njobs(inchain)); % closed population
177 Uden = min([1-GlobalConstants.FineTol,sum(UN(ist,:))]);
178 for k=inchain
179 %QN(ist,k) = (UN(ist,k)-UN(ist,k)^(Nc+1))/(1-Uden); % geometric bound type approximation
180 QN(ist,k) = UN(ist,k)/(1-Uden);
181 RN(ist,k) = QN(ist,k)/TN(ist,k);
182 end
183 end
184 case {SchedStrategy.FCFS, SchedStrategy.HOL, SchedStrategy.FCFSPRPRIO}
185 chainArrivalAtNode = cell(1,C);
186 rates = cell(M,C);
187 mapdcUsed = false; % Flag for exact MAP/D/c solver
188 finiteCapUsed = false; % Flag for finite-capacity truncate-and-renormalize
189 for c=1:C %for each chain
190 rates{ist,c} = V(ist,:) .* lambda(c); % visits of classes within the chain
191 inchain = find(sn.chains(c,:))';
192 markProb = rates{ist,c}(inchain) / sum(rates{ist,c}(inchain));
193 markProb(isnan(markProb)) = 0;
194 chainArrivalAtNode{c} = mmap_mark(chainSysArrivals{c}, markProb);
195 chainArrivalAtNode{c} = mmap_normalize(chainArrivalAtNode{c});
196 chainArrivalAtNode{c} = mmap_scale(chainArrivalAtNode{c}, 1./rates{ist,c}, 0); % non-iterative approximation
197 if c == 1
198 aggrArrivalAtNode = mmap_super_safe({chainArrivalAtNode{c}, mmap_exponential(0,1)}, config.space_max, 'default');
199 aggrArrivalAtNode = {aggrArrivalAtNode{1} aggrArrivalAtNode{2} aggrArrivalAtNode{2}};
200 lc = map_lambda(chainArrivalAtNode{c});
201 if lc>0
202 aggrArrivalAtNode = mmap_scale(aggrArrivalAtNode, 1/lc, 0); % non-iterative approximation
203 end
204 else
205 aggrArrivalAtNode = mmap_super_safe({aggrArrivalAtNode, chainArrivalAtNode{c}}, config.space_max, 'default');
206 end
207 end
208 Qret = cell(1,K);
209 if (strcmp(sn.sched(ist),SchedStrategy.HOL) && any(sn.classprio ~= sn.classprio(1))) % if priorities are not identical
210 [uK,iK] = unique(sn.classprio);
211 % BUTools convention: D1=lowest priority, DK=highest priority
212 % LINE convention: lower value = higher priority
213 % unique() returns ascending order, so we need to reverse for BUTools
214 iK = flipud(iK(:));
215 if length(uK) == length(sn.classprio) % if all priorities are different
216 [Qret{iK'}] = MMAPPH1NPPR({aggrArrivalAtNode{[1;2+iK]}}, {pie{ist}{iK}}, {D0{ist,iK}}, 'ncMoms', 1);
217 else
218 line_error(mfilename,'Solver MAM requires either identical priorities or all distinct priorities');
219 end
220 elseif (sn.sched(ist)==SchedStrategy.FCFSPRPRIO && any(sn.classprio ~= sn.classprio(1))) % FCFS preemptive resume priority
221 [uK,iK] = unique(sn.classprio);
222 % BUTools convention: D1=lowest priority, DK=highest priority
223 % LINE convention: lower value = higher priority
224 % unique() returns ascending order, so we need to reverse for BUTools
225 iK = flipud(iK(:));
226 if length(uK) == length(sn.classprio) % if all priorities are different
227 [Qret{iK'}] = MMAPPH1PRPR({aggrArrivalAtNode{[1;2+iK]}}, {pie{ist}{iK}}, {D0{ist,iK}}, 'ncMoms', 1);
228 else
229 line_error(mfilename,'Solver MAM requires either identical priorities or all distinct priorities');
230 end
231 else
232 aggrUtil = sum(mmap_lambda(aggrArrivalAtNode)./(GlobalConstants.FineTol+sn.rates(ist,1:K)*sn.nservers(ist)), 'omitnan'); %% to debug
233 aggrLambda = mmap_lambda(aggrArrivalAtNode);
234 if aggrUtil < 1-GlobalConstants.FineTol
235 if any(isinf(N))
236 % Check for MAP/D/c: single-class open model with deterministic service
237 isMapDc = (K == 1) && (sn.procid(ist, 1) == ProcessType.DET);
238 % Check for D/M/c: single-class open, exp service at this queue,
239 % deterministic source elsewhere.
240 isDMc = false;
241 dmcSourceIdx = -1;
242 if K == 1 && ~isMapDc && sn.procid(ist, 1) == ProcessType.EXP
243 for jst = 1:sn.nstations
244 if jst ~= ist && sn.procid(jst, 1) == ProcessType.DET
245 isDMc = true;
246 dmcSourceIdx = jst;
247 break;
248 end
249 end
250 end
251 % Check for PH/M/c: single-class open, exp service at this queue,
252 % source has PH inter-arrivals (Erlang/Coxian/MAP/PH/...).
253 % Covers both c=1 (PH/M/1) and c>1 (PH/M/c via matrix-geometric).
254 isPhM1 = false;
255 phM1SourceIdx = -1;
256 if K == 1 && ~isMapDc && ~isDMc && sn.procid(ist, 1) == ProcessType.EXP ...
257 && isfinite(sn.nservers(ist)) && sn.nservers(ist) >= 1
258 for jst = 1:sn.nstations
259 if jst == ist
260 continue;
261 end
262 srcProc = sn.procid(jst, 1);
263 if srcProc ~= ProcessType.EXP && srcProc ~= ProcessType.DET ...
264 && srcProc ~= ProcessType.IMMEDIATE && srcProc ~= ProcessType.DISABLED
265 isPhM1 = true;
266 phM1SourceIdx = jst;
267 break;
268 end
269 end
270 end
271 % Check for finite-buffer FCFS: truncate-and-renormalize
272 isFiniteCap = isfinite(sn.cap(ist)) && ~isMapDc && ~isDMc && ~isPhM1;
273 if isPhM1
274 muQ = 1.0 / S(ist, 1);
275 try
276 phPair = sn.proc{phM1SourceIdx}{1};
277 D0_ph = phPair{1};
278 D1_ph = phPair{2};
279 pie_src = map_pie({D0_ph, D1_ph});
280 phm1Result = qsys_phmc(pie_src, D0_ph, muQ, sn.nservers(ist));
281 Qret{1} = phm1Result.meanQueueLength;
282 mapdcUsed = true;
283 mapdcStations(ist) = true;
284 line_debug(options, 'Using exact PH/M/%d solver: Q=%.4f, W=%.4f', ...
285 sn.nservers(ist), phm1Result.meanQueueLength, phm1Result.meanWaitingTime);
286 catch
287 isPhM1 = false;
288 end
289 end
290 if isDMc
291 muQ = 1.0 / S(ist, 1);
292 lamD = sn.rates(dmcSourceIdx, 1);
293 try
294 dmcResult = qsys_dmc(lamD, muQ, sn.nservers(ist));
295 Qret{1} = dmcResult.meanQueueLength;
296 mapdcUsed = true; % skip surrogate delay correction
297 mapdcStations(ist) = true;
298 line_debug(options, 'Using exact D/M/%d solver: Q=%.4f, W=%.4f', ...
299 sn.nservers(ist), dmcResult.meanQueueLength, dmcResult.meanWaitingTime);
300 catch
301 isDMc = false;
302 end
303 end
304 if isPhM1 || isDMc
305 % handled above; skip the MAP/D/c, finite-cap, and MMAPPH1FCFS branches
306 elseif isMapDc
307 % Use exact MAP/D/c solver from Q-MAM
308 D0_arr = aggrArrivalAtNode{1};
309 D1_arr = aggrArrivalAtNode{2};
310 detServiceTime = S(ist, 1); % 1/rate = service time
311 numServers = sn.nservers(ist);
312
313 mapdcResult = qsys_mapdc(D0_arr, D1_arr, detServiceTime, numServers);
314 Qret{1} = mapdcResult.meanQueueLength;
315 % Store result for later use to skip surrogate delay adjustment
316 mapdcUsed = true;
317 mapdcStations(ist) = true; % Mark for skipping post-processing
318 line_debug('Using exact MAP/D/%d solver: Q=%.4f, W=%.4f', ...
319 numServers, mapdcResult.meanQueueLength, mapdcResult.meanWaitingTime);
320 elseif isFiniteCap
321 % Finite-buffer FCFS. Detect exact M/M/c/K
322 % (Poisson arrivals + same exp service across
323 % classes); else use MMAP[K]/PH[K]/1/FCFS
324 % truncate-and-renormalize approximation.
325 mapdcUsed = false;
326 capK = sn.cap(ist);
327 [isMmck, muMmck] = mam_detect_mmck(sn, ist, K, aggrArrivalAtNode);
328 if isMmck
329 aggrLambdaTotal = sum(aggrLambda, 'omitnan');
330 exactRes = qsys_mmck(aggrLambdaTotal, muMmck, sn.nservers(ist), capK);
331 finiteCapMeanQ = exactRes.meanQueueLength;
332 finiteCapLossProb = exactRes.lossProbability;
333 finiteCapP0 = exactRes.queueLengthDist(1);
334 line_debug('Using exact M/M/%d/%d: Q=%.4f, ploss=%.4f', ...
335 sn.nservers(ist), capK, finiteCapMeanQ, finiteCapLossProb);
336 else
337 [meanQ_fc, lossProb_fc, p_norm_fc] = mam_truncate_renorm( ...
338 {aggrArrivalAtNode{[1,3:end]}}, {pie{ist}{:}}, {D0{ist,:}}, capK);
339 finiteCapMeanQ = meanQ_fc;
340 finiteCapLossProb = lossProb_fc;
341 finiteCapP0 = p_norm_fc(1);
342 line_debug('Using MMAP/PH/%d/%d truncate-renorm: Q=%.4f, ploss=%.4f', ...
343 sn.nservers(ist), capK, meanQ_fc, lossProb_fc);
344 end
345 finiteCapUsed = true;
346 for k=1:K
347 Qret{k} = NaN; % filled per class in result loop
348 end
349 mapdcStations(ist) = true; % skip surrogate delay 2nd pass
350 else
351 mapdcUsed = false;
352 [Qret{1:K}] = MMAPPH1FCFS({aggrArrivalAtNode{[1,3:end]}}, {pie{ist}{:}}, {D0{ist,:}}, 'ncMoms', 1);
353 end
354 else % all closed classes
355 maxLevel = sum(N(isfinite(N)))+1;
356 D = {aggrArrivalAtNode{[1,3:end]}};
357 pdistr_k = cell(1,K);
358 if map_lambda(D)< GlobalConstants.FineTol
359 for k=1:K
360 pdistr_k = [1-GlobalConstants.FineTol, GlobalConstants.FineTol];
361 Qret{k} = GlobalConstants.FineTol / sn.rates(ist);
362 end
363 else
364 if sn.isfunction(ist)
365 alpharate = map_lambda(sn.nodeparam{ist}{end}.setupTime);
366 alphascv = map_scv(sn.nodeparam{ist}{end}.setupTime);
367 betarate = map_lambda(sn.nodeparam{ist}{end}.delayoffTime);
368 betascv = map_scv(sn.nodeparam{ist}{end}.delayoffTime);
369 % Multiclass decomposition: compute per-class
370 % service rates and traffic intensities, solve
371 % aggregate setup-delayoff queue, then
372 % decompose per class proportionally
373 mu_k = zeros(1, K);
374 lambda_k = zeros(1, K);
375 active_k = false(1, K);
376 for k=1:K
377 pie_k = pie{ist}{k};
378 if ~isnan(pie_k(1))
379 mu_k(k) = 1 / (pie_k * inv(-D0{ist,k}) * ones(size(pie_k))');
380 c = find(sn.chains(:,k), 1);
381 lambda_k(k) = rates{ist,c}(k);
382 active_k(k) = true;
383 end
384 end
385 if any(active_k)
386 rho_k = lambda_k ./ mu_k;
387 rho_k(~active_k) = 0;
388 rho_total = sum(rho_k);
389 if rho_total > 0
390 % Aggregate service rate: weighted harmonic mean
391 aggrRate = aggrLambda / rho_total;
392 Q_total = qbd_setupdelayoff(aggrLambda, aggrRate, alpharate, alphascv, betarate, betascv);
393 for k=1:K
394 if active_k(k)
395 Qret{k} = Q_total * rho_k(k) / rho_total;
396 else
397 Qret{k} = NaN;
398 end
399 end
400 else
401 for k=1:K
402 Qret{k} = 0;
403 end
404 end
405 else
406 for k=1:K
407 Qret{k} = NaN;
408 end
409 end
410 else
411 [pdistr] = MMAPPH1FCFS(D, {pie{ist}{:}}, {D0{ist,:}}, 'ncDistr', maxLevel);
412 % rough approximation
413 for k=1:K
414 pdistr_k = abs(pdistr(1:(N(k)+1)));
415 pdistr_k(end) = abs(1-sum(pdistr(1:end-1)));
416 pdistr_k = pdistr_k / sum(pdistr_k(1:(N(k)+1)));
417 Qret{k} = max(0,min(N(k),(0:N(k))*pdistr_k(1:(N(k)+1))'));
418 end
419 end
420 end
421 end
422 else
423 for k=1:K
424 Qret{k} = sn.njobs(k);
425 end
426 end
427 end
428 if finiteCapUsed
429 % Per-class decomposition under FCFS: wait time in queue is
430 % uniform across classes, so per-class response time is
431 % R_k = W_q + S_k. From E[N_total] (truncate-renorm) and
432 % per-class effective rates lambda_k_eff = lambda_k*(1-ploss):
433 % W_q = E[N_total]/lambda_total_eff - sum(lambda_k_eff*S_k)/lambda_total_eff
434 % N_k = lambda_k_eff * R_k
435 lambdaInflow = zeros(1, K);
436 for k=1:K
437 cidx = find(sn.chains(:,k),1);
438 lambdaInflow(k) = rates{ist,cidx}(k);
439 end
440 lambdaInflow(isnan(lambdaInflow)) = 0;
441 TN_eff = lambdaInflow * (1 - finiteCapLossProb);
442 sumTN = sum(TN_eff);
443 if sumTN > 0
444 Savg_eff = sum(TN_eff .* S(ist,1:K), 'omitnan') / sumTN;
445 Wq = max(0, finiteCapMeanQ / sumTN - Savg_eff);
446 else
447 Wq = 0;
448 end
449 for k=1:K
450 TN(ist,k) = TN_eff(k);
451 UN(ist,k) = TN(ist,k) * S(ist,k) / sn.nservers(ist);
452 if TN(ist,k) > 0
453 RN(ist,k) = Wq + S(ist,k);
454 QN(ist,k) = TN(ist,k) * RN(ist,k);
455 else
456 RN(ist,k) = 0;
457 QN(ist,k) = 0;
458 end
459 end
460 else
461 QN(ist,:) = cell2mat(Qret);
462 for k=1:K
463 c = find(sn.chains(:,k),1);
464 TN(ist,k) = rates{ist,c}(k);
465 UN(ist,k) = TN(ist,k) * S(ist,k) / sn.nservers(ist);
466 QN(ist,k) = Qret{k};
467 if mapdcUsed
468 % For MAP/D/c, D/M/c, or PH/M/1, use exact results
469 % (no surrogate delay adjustment).
470 if isPhM1
471 RN(ist,k) = phm1Result.meanSojournTime;
472 elseif isDMc
473 RN(ist,k) = dmcResult.meanSojournTime;
474 else
475 RN(ist,k) = mapdcResult.meanSojournTime;
476 end
477 else
478 % add number of jobs at the surrogate delay server
479 QN(ist,k) = QN(ist,k) + TN(ist,k)*S(ist,k) * (sn.nservers(ist)-1)/sn.nservers(ist);
480 RN(ist,k) = QN(ist,k) ./ TN(ist,k);
481 end
482 end
483 end
484 end
485 end
486 else % not a station
487 switch sn.nodetype(ind)
488 case NodeType.Fork
489 % line_error(mfilename,'Fork nodes not supported yet by MAM solver.');
490 end
491 end
492 end
493 %it
494 %max(max(abs(TN-TN_1)))
495end
496totiter = it + 2;
497CN = sum(RN,1);
498QN = abs(QN);
499for it=1:2 % second pass to rescale again QN based on RN correction
500 for c=1:C
501 inchain = sn.inchain{c};
502 Nc = sum(sn.njobs(inchain));
503 if isfinite(Nc)
504 QNc = sum(sum(QN(:,inchain)));
505 QN(:,inchain) = QN(:,inchain) * (Nc / QNc);
506 end
507 for ind=1:I
508 for k=inchain
509 if sn.isstation(ind)
510 ist = sn.nodeToStation(ind);
511 % Skip stations using exact MAP/D/c solver (already have exact values)
512 if mapdcStations(ist)
513 continue;
514 end
515 if V(ist,k)>0
516 if isinf(sn.nservers(ist))
517 RN(ist,k) = S(ist,k);
518 else
519 RN(ist,k) = max([S(ist,k), QN(ist,k) ./ TN(ist,k)]);
520 end
521 else
522 RN(ist,k) = 0;
523 end
524 QN(ist,k) = RN(ist,k) .* TN(ist,k);
525 end
526 end
527 end
528 Nc = sum(sn.njobs(inchain)); % closed population
529 if Nc == 0 % if closed chain
530 QN(:,c)=0;
531 UN(:,c)=0;
532 RN(:,c)=0;
533 TN(:,c)=0;
534 CN(c)=0;
535 XN(c)=0;
536 end
537 end
538end
539end