1function ARV = solver_mam_traffic_mmap(sn, DEP, config, fjSyncMap)
2% ARV = SOLVER_MAM_TRAFFIC_MMAP(SN, DEP, CONFIG, FJSYNCMAP)
3% FJ-aware traffic solver extending solver_mam_traffic with mmap_max
4% synchronization at join points.
6% DEP{i,r}
is the departure process of
class r from i in (D0,D1) format
7% fjSyncMap
is built by sn_build_fj_sync_map
9% Copyright (c) 2012-2026, Imperial College London
15if ~isfield(config,
'fj_sync_q_len')
16 config.fj_sync_q_len = 2;
19% In this function we use indexing over all non-ClassSwitch
nodes
22nodeToNCS = zeros(1,I);
25 if sn.nodetype(ind) ~= NodeType.ClassSwitch
26 non_cs_classes(end+1:end+R)= ((ind-1)*R+1):(ind*R);
28 nodeToNCS(ind) = sum(isNCS);
29 NCStoNode(end+1) = ind;
35% Hide the
nodes that are class switches
36rtncs = dtmc_stochcomp(sn.rtnodes, non_cs_classes);
37Inc = I - sum(sn.nodetype == NodeType.ClassSwitch);
39% DEP
is indexed by node (ind, r) - convert to
MMAP format
43 MMAP{ind,r} = DEP{ind,r};
44 if isempty(
MMAP{ind,r}) || any(any(isnan(
MMAP{ind,r}{1})))
45 MMAP{ind,r} = {[0],[0],[0]}; % no arrivals from
this class
56% Build the nodeSync matrix in NCS indexing
57nodeSyncNCS = zeros(Inc, Inc);
64 if fjSyncMap.nodeSync(ind, jnd) > 0
65 nodeSyncNCS(inc, jnc) = fjSyncMap.nodeSync(ind, jnd);
72% First determine all outgoing flows from all
nodes
76 switch sn.nodetype(ind)
77 case {NodeType.Source, NodeType.Delay, NodeType.Queue, NodeType.Fork, NodeType.Join}
78 % obtain departure maps (
MMAP is now node-indexed)
80 % Order-preserving bounded
class-by-
class superposition;
81 % see _kb/06-solver-catalog.md
for rationale
82 DEP_NCS{inc} =
MMAP{ind,1};
84 DEP_NCS{inc} = mmap_super(DEP_NCS{inc},
MMAP{ind,rr});
85 if length(DEP_NCS{inc}{1}) > config.space_max
86 DEP_NCS{inc} = mmap_compress(DEP_NCS{inc}, config);
90 DEP_NCS{inc} =
MMAP{ind,1};
92 Psplit = zeros(R,Inc*R);
98 Psplit(r,(jnc-1)*R+s) = rtncs((inc-1)*R+r, (jnc-1)*R+s);
104 [Fsplit{1:Inc}] = npfqn_traffic_split_cs(DEP_NCS{inc}, Psplit, config);
106 LINKS{inc,jnc} = Fsplit{jnc};
107 LINKS{inc,jnc} = mmap_normalize(LINKS{inc,jnc});
113% Then determine all incoming flows, with FJ synchronization
115 if isNCS(ind) && sn.nodetype(ind) ~= NodeType.Source
116 inc = nodeToNCS(ind);
118 % Partition incoming links into sync groups and independent flows
119 syncGroupsAtNode = unique(nodeSyncNCS(inc, :));
120 syncGroupsAtNode = syncGroupsAtNode(syncGroupsAtNode > 0);
122 independentFlows = {};
123 syncFlows =
struct();
126 if isempty(LINKS{jnc,inc}) || sum(mmap_lambda(LINKS{jnc,inc})) <= GlobalConstants.FineTol
129 gid = nodeSyncNCS(inc, jnc);
132 independentFlows{end+1} = LINKS{jnc,inc};
134 % Synchronized flow — group by sync group ID
135 fname = [
'g' num2str(gid)];
136 if ~isfield(syncFlows, fname)
137 syncFlows.(fname) = {};
139 syncFlows.(fname){end+1} = LINKS{jnc,inc};
143 % Process
synchronized flows: apply mmap_max iteratively within each group
145 for g = 1:length(syncGroupsAtNode)
146 gid = syncGroupsAtNode(g);
147 fname = [
'g' num2str(gid)];
148 if ~isfield(syncFlows, fname)
151 groupFlows = syncFlows.(fname);
152 if isempty(groupFlows)
156 % Start with first flow, iteratively apply mmap_max with remaining
157 syncedFlow = groupFlows{1};
158 for f = 2:length(groupFlows)
159 syncedFlow = mmap_max(syncedFlow, groupFlows{f}, config.fj_sync_q_len);
160 % Normalize mmap_max output before use; see _kb/06-solver-catalog.md
for rationale
161 syncedFlow = mmap_normalize(syncedFlow);
163 % Compress after each mmap_max step
if state space
is too large
164 if length(syncedFlow{1}) > config.space_max
165 syncedFlow = mmap_compress(syncedFlow,
struct(
'method', config.compress));
168 syncResults{end+1} = syncedFlow;
171 % Merge synced flows with independent flows
172 allFlows = [syncResults, independentFlows];
174 if length(allFlows) > 1
175 ARV{ind} = npfqn_traffic_merge(allFlows, config);
176 elseif length(allFlows) == 1
177 ARV{ind} = allFlows{1};
179 % No flows — take a zero-rate link
181 if ~isempty(LINKS{jnc,inc})
182 ARV{ind} = LINKS{jnc,inc};
187 ARV{ind} = {[0],[0],[0]};