1function [QN,UN,RN,TN,CN,XN,totiter] = solver_mam_basic_fj(sn, options)
2% [QN,UN,RN,TN,CN,XN,TOTITER] = SOLVER_MAM_BASIC_FJ(SN, OPTIONS)
4% MAM decomposition solver with fork-join synchronization via mmap_max.
5% Follows the solver_mam.m (dec.mmap) iteration pattern with FJ-aware
6% traffic analysis
using solver_mam_traffic_fj.
8% Supports open networks with general FJ topologies and heterogeneous
9% service distributions at parallel queues.
11% Copyright (c) 2012-2026, Imperial College London
14config = options.config;
15if ~isfield(config,
'fj_sync_q_len')
16 config.fj_sync_q_len = 2;
18if ~isfield(config, 'etaqa_trunc')
19 config.etaqa_trunc = 8;
38% Build FJ synchronization
map
39fjSyncMap = sn_build_fj_sync_map(sn);
41% Determine per-chain arrival rates
44 inchain = sn.inchain{c};
45 lambdas_inchain = sn.rates(sn.refstat(inchain(1)),inchain);
46 lambdas_inchain = lambdas_inchain(isfinite(lambdas_inchain));
47 lambda(inchain) = sum(lambdas_inchain);
50% Prepare PH service distributions
55 case SchedStrategy.EXT
56 TN(ist,:) = sn.rates(ist,:);
57 TN(ist,isnan(TN(ist,:))) = 0;
58 case {SchedStrategy.FCFS, SchedStrategy.HOL, SchedStrategy.FCFSPRPRIO}
60 % divide service time by number of servers and put
61 % later a surrogate delay server in tandem to compensate
62 PH{ist}{k} = map_scale(PH{ist}{k}, map_mean(PH{ist}{k})/sn.nservers(ist));
63 pie{ist}{k} = map_pie(PH{ist}{k});
64 D0{ist,k} = PH{ist}{k}{1};
65 if any(isnan(D0{ist,k}))
66 D0{ist,k} = -GlobalConstants.Immediate;
68 PH{ist}{k} = map_exponential(GlobalConstants.Immediate);
71 case SchedStrategy.INF
73 pie{ist}{k} = map_pie(PH{ist}{k});
74 D0{ist,k} = PH{ist}{k}{1};
75 if any(isnan(D0{ist,k}))
76 D0{ist,k} = -GlobalConstants.Immediate;
78 PH{ist}{k} = map_exponential(GlobalConstants.Immediate);
83 PH{ist}{k} = map_scale(PH{ist}{k}, map_mean(PH{ist}{k})/sn.nservers(ist));
84 pie{ist}{k} = map_pie(PH{ist}{k});
85 D0{ist,k} = PH{ist}{k}{1};
86 if any(isnan(D0{ist,k}))
87 D0{ist,k} = -GlobalConstants.Immediate;
89 PH{ist}{k} = map_exponential(GlobalConstants.Immediate);
95it_max = options.iter_max;
97 % Initialize departure processes (node-indexed: DEP{ind,r})
101 isForkJoin = (sn.nodetype(ind) == NodeType.Fork || sn.nodetype(ind) == NodeType.Join);
102 if sn.isstation(ind) && ~isForkJoin
103 ist = sn.nodeToStation(ind);
105 if V(ist,r) > 0 && lambda(r) > 0
106 DEP{ind,r} = map_scale(PH{ist}{r}, 1 / (lambda(r) * V(ist,r)));
108 DEP{ind,r} = PH{ist}{r};
112 % Non-station
nodes or Fork/Join: pass-through
113 % map_exponential takes MEAN interarrival time = 1/rate
116 DEP{ind,r} = map_exponential(1/lambda(r));
118 DEP{ind,r} = map_exponential(1/GlobalConstants.Immediate);
125 % Compute arrival processes with FJ synchronization
126 ARV = solver_mam_traffic_fj(sn, DEP, config, fjSyncMap);
130 ind = sn.stationToNode(ist);
131 switch sn.nodetype(ind)
133 % Join: throughput from synced arrival rate, no queueing
134 if ~isempty(ARV{ind}) && iscell(ARV{ind})
135 arrRates = mmap_lambda(ARV{ind});
137 TN(ist,k) = arrRates(k);
144 if ~isempty(ARV{ind}) && iscell(ARV{ind})
145 % Compress arrival process
if too large
146 if length(ARV{ind}{1}) > config.space_max
148 line_printf(
'\nArrival process at node %d is now at %d states. Compressing.', ind, length(ARV{ind}{1}));
150 ARV{ind} = mmap_compress(ARV{ind});
154 case {SchedStrategy.FCFS, SchedStrategy.HOL, SchedStrategy.FCFSPRPRIO}
155 [Qret{1:K}, ~] = MMAPPH1FCFS({ARV{ind}{[1,3:end]}}, {pie{ist}{:}}, {D0{ist,:}},
'ncMoms', 1,
'ncDistr', 2);
157 QN(ist,k) = sum(Qret{k});
159 TN(ist,:) = mmap_lambda(ARV{ind});
160 case SchedStrategy.PS
161 TN(ist,:) = mmap_lambda(ARV{ind});
163 UN(ist,k) = TN(ist,k) * S(ist,k);
165 Uden = min([1-GlobalConstants.FineTol, sum(UN(ist,:))]);
167 QN(ist,k) = UN(ist,k)/(1-Uden);
172 UN(ist,k) = TN(ist,k) * map_mean(PH{ist}{k});
173 % add number of jobs at the surrogate delay server
174 QN(ist,k) = QN(ist,k) + TN(ist,k)*(map_mean(PH{ist}{k})*sn.nservers(ist)) * (sn.nservers(ist)-1)/sn.nservers(ist);
175 RN(ist,k) = QN(ist,k) ./ TN(ist,k);
180 case SchedStrategy.INF
181 if ~isempty(ARV{ind}) && iscell(ARV{ind})
182 TN(ist,:) = mmap_lambda(ARV{ind});
186 UN(ist,k) = S(ist,k)*TN(ist,k);
187 QN(ist,k) = TN(ist,k)*S(ist,k);
188 RN(ist,k) = S(ist,k);
191 case SchedStrategy.EXT
192 % Source: already set TN above
198 if it >= 3 && max(abs(QN(:)-QN_1(:))./(QN_1(:)+GlobalConstants.FineTol)) < options.iter_tol
202 % Update departure processes
204 ind = sn.stationToNode(ist);
205 switch sn.nodetype(ind)
207 if ~isempty(ARV{ind}) && iscell(ARV{ind})
209 case {SchedStrategy.FCFS, SchedStrategy.HOL, SchedStrategy.FCFSPRPRIO}
211 A = mmap_hide(ARV{ind}, setdiff(1:K,r));
215 etaqa_n = config.etaqa_trunc;
216 etaqa_sz = (etaqa_n+1)*na*ns;
217 rho = sum(UN(ist,:));
218 if etaqa_sz <= config.space_max && rho < 1-GlobalConstants.FineTol
220 DEP{ind,r} = qbd_depproc_etaqa(A, Srv, etaqa_n);
221 DEP{ind,r} = map_normalize(DEP{ind,r});
228 if V(ist,r) > 0 && lambda(r) > 0
229 DEP{ind,r} = map_scale(DEP{ind,r}, 1 / (lambda(r) * V(ist,r)));
232 case SchedStrategy.PS
234 A = mmap_hide(ARV{ind}, setdiff(1:K,r));
238 etaqa_n = config.etaqa_trunc;
239 etaqa_sz = (etaqa_n+1)*na*ns;
240 rho = sum(UN(ist,:));
241 if V(ist,r) > 0 && lambda(r) > 0
242 if etaqa_sz <= config.space_max && rho < 1-GlobalConstants.FineTol
244 DEP{ind,r} = qbd_depproc_etaqa_ps(A, Srv, etaqa_n);
245 DEP{ind,r} = map_normalize(DEP{ind,r});
252 DEP{ind,r} = map_scale(DEP{ind,r}, 1 / (lambda(r) * V(ist,r)));
258 % Join: departure =
synchronized arrival (pass-through)
259 % map_exponential takes MEAN interarrival time = 1/rate
262 DEP{ind,r} = map_exponential(1/TN(ist,r));
266 % Fork
nodes keep their initial departure processes (pass-through)
272 line_printf(
'\nMAM FJ parametric decomposition completed in %d iterations.', it);