1function space = fromMarginal(sn, ind, n, options)
2% FROMMARGINAL Generate state space with specific marginal queue lengths
4% @brief Creates state space where a specific node has given marginal queue lengths
5% @param sn Network structure or Network
object
6% @param ind Node index
for which to set marginal queue lengths
7% @param n Vector of jobs per
class at the specified node
8% @param options Optional structure with configuration parameters
9% @
return space Generated state space satisfying marginal constraints
11% This function generates all possible network states where the specified
12% node (ind) has exactly n(r) jobs of
class r, for all
classes. It
is
13% essential
for state space analysis and marginal probability computations.
15% Copyright (c) 2012-2026, Imperial College London
18if nargin<4 %~exist(
'options',
'var')
19 options.force = false;
26% generate states such that the marginal queue-lengths are as in vector n
27% n(r): number of jobs at the station in class r
34ist = sn.nodeToStation(ind);
35%isf = sn.nodeToStateful(ind);
37if sn.isstation(ind) && any(sn.procid(ist,:)==ProcessType.MAP | sn.procid(sn.nodeToStation(ind),:)==ProcessType.MMPP2)
38 if sn.sched(ist) ~= SchedStrategy.FCFS & sn.nodetype ~= NodeType.Source
39 line_error(mfilename,'Non-FCFS MAP stations are not supported.')
43if sn.isstateful(ind) && ~sn.isstation(ind)
44 if sn.nodetype(ind) == NodeType.Transition
45 % Transition state format
is per-mode, not per-class
46 isf = sn.nodeToStateful(ind);
47 if ~isempty(sn.space) && isf <= length(sn.space) && ~isempty(sn.space{isf})
48 space = sn.space{isf};
50 % Generate initial state only (all servers idle)
51 nmodes_t = sn.nodeparam{ind}.nmodes;
52 nmodeservers_t = sn.nodeparam{ind}.nmodeservers;
53 nmodeservers_t(isinf(nmodeservers_t)) = GlobalConstants.MaxInt();
54 firingphases_t = sn.nodeparam{ind}.firingphases;
55 firingphases_t(isnan(firingphases_t)) = 1;
56 space = [nmodeservers_t, zeros(1, sum(firingphases_t)), zeros(size(nmodeservers_t))];
61 init_r = State.spaceClosedSingle(1,n(r));
62 state = State.cartesian(state,init_r);
64 space = State.cartesian(space,state);
70 if isempty(sn.proc{ist}{r})
73 phases(r) = length(sn.proc{ist}{r}{1});
76if (sn.sched(ist) ~= SchedStrategy.EXT) && any(n>sn.classcap(ist,:))
80% generate local-state space
81switch sn.nodetype(ind)
82 case {NodeType.Queue, NodeType.Delay, NodeType.Source, NodeType.Place}
84 case SchedStrategy.EXT
86 if ~isempty(sn.proc) && ~isempty(sn.proc{ist}{r}) && any(any(isnan(sn.proc{ist}{r}{1}))) % disabled
87 init_r = 0*ones(1,phases(r));
89 init_r = State.spaceClosedSingle(phases(r),1);
91 state = State.cartesian(state,init_r);
93 space = State.cartesian(space,state); %server part
94 space = [Inf*ones(size(space,1),1),space]; % attach infinite buffer before servers
95 case {SchedStrategy.INF, SchedStrategy.PS, SchedStrategy.DPS, SchedStrategy.GPS, SchedStrategy.PSPRIO, SchedStrategy.DPSPRIO, SchedStrategy.GPSPRIO, SchedStrategy.LPS}
96 % in these policies we only track the jobs in the servers
98 init_r = State.spaceClosedSingle(phases(r),n(r));
99 state = State.cartesian(state,init_r);
101 space = State.cartesian(space,state);
102 case {SchedStrategy.SIRO, SchedStrategy.LEPT, SchedStrategy.SEPT, SchedStrategy.SRPT, SchedStrategy.SRPTPRIO}
103 % in these policies we track an un-ordered buffer and
104 % the jobs in the servers
105 % build list of job
classes in the node, with repetition
108 init_r = State.spaceClosedSingle(phases(r),n(r));
109 state = State.cartesian(state,init_r);
111 space = State.cartesian(space,[zeros(size(state,1),R),state]);
113 si = multichoosecon(n,S(ist)); % jobs of
class r that are running
114 mi_buf = repmat(n,size(si,1),1) - si; % jobs of
class r in buffer
116 % determine number of
classes r jobs running in phase j
119 init_r = State.spaceClosedSingle(phases(r),si(k,r));
120 kstate = State.cartesian(kstate,init_r);
122 state = [repmat(mi_buf(k,:),size(kstate,1),1), kstate];
123 space = [space; state];
126 case {SchedStrategy.FCFSPI, SchedStrategy.FCFSPR, SchedStrategy.FCFSPIPRIO, SchedStrategy.FCFSPRPRIO, SchedStrategy.LCFSPI, SchedStrategy.LCFSPR, SchedStrategy.LCFSPIPRIO, SchedStrategy.LCFSPRPRIO}
127 sizeEstimator = multinomialln(n) - gammaln(sum(n)) + gammaln(1+sn.cap(ist));
128 sizeEstimator = round(sizeEstimator/log(10));
130 if ~isfield(options,
'force') || options.force ==
false
131 %line_warning(mfilename,sprintf(
'Marginal state space size is in the order of thousands of states. Computation may be slow.',sizeEstimator));
136 space = zeros(1,1+sum(phases)); % unclear
if this should 1+sum(K), was sum(K) but State.fromMarginalAndStarted uses 1+sum(K) so was changed here as well
139 % in these policies we track an ordered buffer and
140 % the jobs in the servers
142 % build list of job
classes in the node, with repetition
146 vi=[vi, r*ones(1,n(r))];
150 % gen permutation of their positions in the waiting buffer
151 mi = uniqueperms(vi);
152 % now generate server states
154 mi_buf = zeros(1,max(0,sum(n)-S(ist)));
156 state = State.cartesian(state,[mi_buf,state]);
158 mi = mi(:,(end-min(sum(n),sn.cap(ist))+1):end); % n(r) may count more than once elements within the same chain
159 mi = unique(mi,
'rows');
160 % mi_buf:
class of job in buffer position i (0=empty)
161 mi_buf = [zeros(size(mi,1),min(sum(n),sn.cap(ist))-S(ist)-size(mi(:,1:end-S(ist)),2)), mi(:,1:end-S(ist))];
163 mi_buf = zeros(size(mi,1),1);
167 % generate job phases
for all buffer states
168 %
for k=1:size(mi_buf,1)
169 % mi_buf_kstate(end+1:end+size(bkstate,1),1:size(bkstate,2)) = bkstate;
172 % mi_srv:
class of job running in server i
173 mi_srv = mi(:,max(size(mi,2)-S(ist)+1,1):end);
174 % si: number of
class r jobs that are running
176 for k=1:size(mi_srv,1)
177 %si(k,1:R) = hist(mi_srv(k,:),1:R); % deprecated
178 si(k, 1:R) = histcounts(mi_srv(k, :), 1:(R+1));
180 %si = unique(si,
'rows');
182 % determine number of
class r jobs running in phase
183 % j in server state mi_srv(kjs,:) and build
187 kstate = State.cartesian(kstate,State.spaceClosedSingle(phases(r),si(k,r)));
189 % generate job phases
for all buffer states
191 for j=mi_buf(k,:) % for each job in the buffer
193 bkstate = State.cartesian(bkstate,[1:phases(j)]
');
198 bufstate_tmp = State.cartesian(mi_buf(k,:), bkstate);
199 % here interleave positions of class and phases in
201 bufstate = zeros(size(bufstate_tmp));
202 bufstate(:,1:2:end)=bufstate_tmp(:,1:size(mi_buf,2));
203 bufstate(:,2:2:end)=bufstate_tmp(:,(size(mi_buf,2)+1):end);
204 state = [state; State.cartesian(bufstate, kstate)];
208 case {SchedStrategy.FCFS, SchedStrategy.HOL, SchedStrategy.FCFSPRIO, SchedStrategy.LCFS, SchedStrategy.LCFSPRIO}
209 sizeEstimator = multinomialln(n) - gammaln(sum(n)) + gammaln(1+sn.cap(ist));
210 sizeEstimator = round(sizeEstimator/log(10));
212 if ~isfield(options,'force
') || options.force == false
213 %line_warning(mfilename,sprintf('Marginal state space size
is in the order of thousands of states. Computation may be slow.
',sizeEstimator));
218 space = zeros(1,1+sum(phases));
219 if sn.nodetype(ind) ~= NodeType.Source
221 switch sn.procid(sn.nodeToStation(ind),r)
222 case {ProcessType.MAP, ProcessType.MMPP2}
223 space = State.cartesian(space, [1:sn.phases(ind,r)]');
229 % in these policies we track an ordered buffer and
230 % the jobs in the servers
232 % build list of job
classes in the node, with repetition
236 vi=[vi, r*ones(1,n(r))];
240 % gen permutation of their positions in the waiting buffer
241 mi = uniqueperms(vi);
242 % now generate server states
244 mi_buf = zeros(1,max(0,sum(n)-S(ist)));
246 state = State.cartesian(state,[mi_buf,state]);
248 mi = mi(:,(end-min(sum(n),sn.cap(ist))+1):end); % n(r) may count more than once elements within the same chain
249 mi = unique(mi,
'rows');
250 % mi_buf:
class of job in buffer position i (0=empty)
251 mi_buf = [zeros(size(mi,1),min(sum(n),sn.cap(ist))-S(ist)-size(mi(:,1:end-S(ist)),2)), mi(:,1:end-S(ist))];
253 mi_buf = zeros(size(mi,1),1);
255 % mi_srv:
class of job running in server i
256 mi_srv = mi(:,max(size(mi,2)-S(ist)+1,1):end);
257 % si: number of
class r jobs that are running
259 for k=1:size(mi_srv,1)
260 %si(k,1:R) = hist(mi_srv(k,:),1:R); % deprecated
261 si(k, 1:R) = histcounts(mi_srv(k, :), 1:(R+1));
263 %si = unique(si,
'rows');
265 % determine number of
class r jobs running in phase
266 % j in server state mi_srv(k,:) and build
272 init_r = State.spaceClosedSingle(phases(r),si(k,r));
273 if sn.procid(sn.nodeToStation(ind),r) == ProcessType.MAP || sn.procid(sn.nodeToStation(ind),r) == ProcessType.MMPP2
275 init_r = State.cartesian(init_r, [1:phases(r)]
');
278 % Single server case (original logic)
279 init_r = State.cartesian(init_r, 0);
280 for i=1:size(init_r,1)
281 if init_r(i,end) == 0
282 init_r(i,end) = find(init_r(i,:));
286 % Multiserver case: FCFS-aware phase selection
287 % Use original approach but bias toward occupied phases
290 for i=1:size(init_r,1)
291 phase_dist = init_r(i, 1:phases(r));
293 % Find phases that have jobs (occupied phases)
294 occupied_phases = find(phase_dist > 0);
296 if ~isempty(occupied_phases)
297 % For FCFS, include occupied phases plus adjacent phases
298 % to account for phase transitions during service
299 extended_phases = [];
300 for op = occupied_phases
301 extended_phases = [extended_phases, op];
302 % Add adjacent phases for smooth transitions
304 extended_phases = [extended_phases, op-1];
307 extended_phases = [extended_phases, op+1];
310 extended_phases = unique(extended_phases);
311 phase_list = [phase_list; extended_phases'];
313 % No jobs - include all phases (original behavior)
314 phase_list = [phase_list; [1:phases(r)]
'];
318 % Remove duplicates and create cartesian product
319 phase_list = unique(phase_list);
320 init_r = State.cartesian(init_r, phase_list);
324 kstate = State.cartesian(kstate,init_r);
325 if sn.procid(sn.nodeToStation(ind),r) == ProcessType.MAP || sn.procid(sn.nodeToStation(ind),r) == ProcessType.MMPP2
326 map_cols(end+1) = size(kstate,2);
329 kstate = kstate(:,[setdiff(1:size(kstate,2),map_cols),map_cols]);
330 state = [state; repmat(mi_buf(k,:),size(kstate,1),1), kstate];
334 case {SchedStrategy.SJF, SchedStrategy.LJF}
335 % in these policies the state space includes continuous
336 % random variables for the service times
337 line_error(mfilename,'The scheduling policy does not admit a discrete state space.\n
');
340 switch sn.routing(ind,r)
341 case {RoutingStrategy.RROBIN, RoutingStrategy.WRROBIN}
342 space = State.cartesian(space, sn.nodeparam{ind}{r}.outlinks(:));
347 case SchedStrategy.INF
348 % in this policies we only track the jobs in the servers
350 init_r = State.spaceClosedSingle(phases(r),n(r));
351 state = State.cartesian(state,init_r);
353 space = State.cartesian(space,state);
356 switch sn.routing(ind,r)
357 case RoutingStrategy.RROBIN
358 space = State.cartesian(space, sn.nodeparam{ind}{r}.outlinks(:));
362space = unique(space,'rows
'); % do not comment, required to sort empty state as first
363space = space(end:-1:1,:); % so that states with jobs in phase 1 comes earlier