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)
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
8% Copyright (c) 2012-2026, Imperial College London
11config = options.config;
15%% generate local state spaces
21V = cellsum(sn.visits);
23Lchain = sn_get_demands_chain(sn);
32% Track stations using exact MAP/D/c solver (skip post-processing for these)
33mapdcStations = false(M, 1);
39chainSysArrivals = cell(1,C);
44% open queueing system (one node is the external world)
45% first build the joint arrival process
48 case {SchedStrategy.FCFS, SchedStrategy.HOL, SchedStrategy.FCFSPRPRIO, SchedStrategy.PS}
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
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;
65 PH{ist}{k} = map_exponential(GlobalConstants.Immediate);
73if any(isinf(sn.njobs))
76if any(isfinite(sn.njobs))
79isMixed = isOpen & isClosed;
81lambdas_inchain = cell(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
92 if isnan(PH{ist}{k}{1})
93 PH{ist}{k} = map_exponential(Inf); % no arrivals from
this class
96 inchain = sn.inchain{c};
99 for ki=2:length(inchain)
101 if isnan(PH{ist}{k}{1})
102 PH{ist}{k} = map_exponential(Inf); % no arrivals from
this class
106 TN(ist,inchain
') = lambdas_inchain{c};
110sd = isfinite(sn.nservers);
112while max(max(abs(TN-TN_1))) > tol && it <= options.iter_max %#ok<max>
115 Umax = max(sum(UN(sd,:),2));
117 lambda = lambda * 1/Umax;
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));
126 lambda(c) = TNlb; % lower bound
128 lambda(c) = lambda(c) * it/options.iter_max + (Nc / QNc) * lambda(c) * (options.iter_max-it)/options.iter_max; % iteration-averaged regula falsi;
135 inchain = sn.inchain{c};
136 chainSysArrivals{c} = mmap_exponential(lambda(c));
138 TN(m,inchain) = V(m,inchain) .* lambda(c);
144 ist = sn.nodeToStation(ind);
145 switch sn.nodetype(ind)
148 inchain = sn.inchain{c};
150 fanin = nnz(sn.rtnodes(:, (ind-1)*K+k));
151 TN(ist,k) = lambda(c)*V(ist,k)/fanin;
159 case SchedStrategy.INF
161 inchain = sn.inchain{c};
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);
169 case SchedStrategy.PS
171 inchain = sn.inchain{c};
173 TN(ist,k) = lambda(c)*V(ist,k);
174 UN(ist,k) = S(ist,k)*TN(ist,k);
176 %Nc = sum(sn.njobs(inchain)); % closed population
177 Uden = min([1-GlobalConstants.FineTol,sum(UN(ist,:))]);
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);
184 case {SchedStrategy.FCFS, SchedStrategy.HOL, SchedStrategy.FCFSPRPRIO}
185 chainArrivalAtNode = cell(1,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;
195 chainArrivalAtNode{c} = mmap_normalize(chainArrivalAtNode{c});
196 chainArrivalAtNode{c} = mmap_scale(chainArrivalAtNode{c}, 1./rates{ist,c}, 0); % non-iterative approximation
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});
202 aggrArrivalAtNode = mmap_scale(aggrArrivalAtNode, 1/lc, 0); % non-iterative approximation
205 aggrArrivalAtNode = mmap_super_safe({aggrArrivalAtNode, chainArrivalAtNode{c}}, config.space_max,
'default');
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
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);
218 line_error(mfilename,'Solver MAM
requires either identical priorities or all distinct priorities
');
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
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);
229 line_error(mfilename,
'Solver MAM requires either identical priorities or all distinct priorities');
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
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.
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
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).
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
262 srcProc = sn.procid(jst, 1);
263 if srcProc ~= ProcessType.EXP && srcProc ~= ProcessType.DET ...
264 && srcProc ~= ProcessType.IMMEDIATE && srcProc ~= ProcessType.DISABLED
271 % Check
for finite-buffer FCFS: truncate-and-renormalize
272 isFiniteCap = isfinite(sn.cap(ist)) && ~isMapDc && ~isDMc && ~isPhM1;
274 muQ = 1.0 / S(ist, 1);
276 phPair = sn.proc{phM1SourceIdx}{1};
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;
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);
291 muQ = 1.0 / S(ist, 1);
292 lamD = sn.rates(dmcSourceIdx, 1);
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);
305 % handled above; skip the MAP/D/c, finite-cap, and MMAPPH1FCFS branches
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);
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
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);
321 % Finite-buffer FCFS. Detect exact M/M/c/K
322 % (Poisson arrivals + same exp service across
324 % truncate-and-renormalize approximation.
327 [isMmck, muMmck] = mam_detect_mmck(sn, ist, K, aggrArrivalAtNode);
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);
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);
345 finiteCapUsed =
true;
347 Qret{k} = NaN; % filled per
class in result loop
349 mapdcStations(ist) =
true; % skip surrogate delay 2nd pass
352 [Qret{1:K}] = MMAPPH1FCFS({aggrArrivalAtNode{[1,3:end]}}, {pie{ist}{:}}, {D0{ist,:}},
'ncMoms', 1);
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
360 pdistr_k = [1-GlobalConstants.FineTol, GlobalConstants.FineTol];
361 Qret{k} = GlobalConstants.FineTol / sn.rates(ist);
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
374 lambda_k = zeros(1, K);
375 active_k =
false(1, K);
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);
386 rho_k = lambda_k ./ mu_k;
387 rho_k(~active_k) = 0;
388 rho_total = sum(rho_k);
390 % Aggregate service rate: weighted harmonic mean
391 aggrRate = aggrLambda / rho_total;
392 Q_total = qbd_setupdelayoff(aggrLambda, aggrRate, alpharate, alphascv, betarate, betascv);
395 Qret{k} = Q_total * rho_k(k) / rho_total;
411 [pdistr] = MMAPPH1FCFS(D, {pie{ist}{:}}, {D0{ist,:}}, 'ncDistr
', maxLevel);
412 % rough approximation
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))'));
424 Qret{k} = sn.njobs(k);
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);
437 cidx = find(sn.chains(:,k),1);
438 lambdaInflow(k) = rates{ist,cidx}(k);
440 lambdaInflow(isnan(lambdaInflow)) = 0;
441 TN_eff = lambdaInflow * (1 - finiteCapLossProb);
444 Savg_eff = sum(TN_eff .* S(ist,1:K),
'omitnan') / sumTN;
445 Wq = max(0, finiteCapMeanQ / sumTN - Savg_eff);
450 TN(ist,k) = TN_eff(k);
451 UN(ist,k) = TN(ist,k) * S(ist,k) / sn.nservers(ist);
453 RN(ist,k) = Wq + S(ist,k);
454 QN(ist,k) = TN(ist,k) * RN(ist,k);
461 QN(ist,:) = cell2mat(Qret);
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);
468 % For MAP/D/c, D/M/c, or PH/M/1, use exact results
469 % (no surrogate delay adjustment).
471 RN(ist,k) = phm1Result.meanSojournTime;
473 RN(ist,k) = dmcResult.meanSojournTime;
475 RN(ist,k) = mapdcResult.meanSojournTime;
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);
487 switch sn.nodetype(ind)
489 % line_error(mfilename,
'Fork nodes not supported yet by MAM solver.');
494 %max(max(abs(TN-TN_1)))
499for it=1:2 % second pass to rescale again QN based on RN correction
501 inchain = sn.inchain{c};
502 Nc = sum(sn.njobs(inchain));
504 QNc = sum(sum(QN(:,inchain)));
505 QN(:,inchain) = QN(:,inchain) * (Nc / QNc);
510 ist = sn.nodeToStation(ind);
511 % Skip stations
using exact MAP/D/c solver (already have exact values)
512 if mapdcStations(ist)
516 if isinf(sn.nservers(ist))
517 RN(ist,k) = S(ist,k);
519 RN(ist,k) = max([S(ist,k), QN(ist,k) ./ TN(ist,k)]);
524 QN(ist,k) = RN(ist,k) .* TN(ist,k);
528 Nc = sum(sn.njobs(inchain)); % closed population
529 if Nc == 0 %
if closed chain