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 isfield(sn,'isfjaugmented') && sn.isfjaugmented && sn.nodetype(ind) == NodeType.Join
38 % FJ-augmented struct: the join state
is the per-class count vector
39 % of buffered jobs/siblings, deterministic given the marginals
44if sn.isstation(ind) && any(sn.procid(ist,:)==ProcessType.MAP | sn.procid(sn.nodeToStation(ind),:)==ProcessType.MMPP2)
45 if sn.sched(ist) ~= SchedStrategy.FCFS && sn.nodetype(ind) ~= NodeType.Source
46 line_error(mfilename,'Non-FCFS MAP stations are not supported.')
50if sn.isstateful(ind) && ~sn.isstation(ind)
51 if sn.nodetype(ind) == NodeType.Transition
52 % Transition state format
is per-mode, not per-class
53 isf = sn.nodeToStateful(ind);
54 if ~isempty(sn.space) && isf <= length(sn.space) && ~isempty(sn.space{isf})
55 space = sn.space{isf};
57 % Generate initial state only (all servers idle)
58 nmodes_t = sn.nodeparam{ind}.nmodes;
59 nmodeservers_t = sn.nodeparam{ind}.nmodeservers;
60 nmodeservers_t(isinf(nmodeservers_t)) = GlobalConstants.MaxInt();
61 firingphases_t = sn.nodeparam{ind}.firingphases;
62 firingphases_t(isnan(firingphases_t)) = 1;
63 space = [nmodeservers_t, zeros(1, sum(firingphases_t)), zeros(size(nmodeservers_t))];
68 init_r = State.spaceClosedSingle(1,n(r));
69 state = State.cartesian(state,init_r);
71 space = State.cartesian(space,state);
77 if isempty(sn.proc{ist}{r})
79 elseif isfield(sn,'markidx') && ~isempty(sn.markidx) ...
80 && ist <= size(sn.markidx,1) && sn.markidx(ist,r) > 1
81 % Marked (
MMAP) non-carrier class: modulating chain lives in the carrier's phase block -- see _kb/04-networkstruct.md
84 phases(r) = length(sn.proc{ist}{r}{1});
87if (sn.sched(ist) ~= SchedStrategy.EXT) && any(n>sn.classcap(ist,:))
91% generate local-state space
92switch sn.nodetype(ind)
93 case {NodeType.Queue, NodeType.Delay, NodeType.Source, NodeType.Place}
94 isRetrialStation = isfield(sn,
'retrialProc') && ~isempty(sn.retrialProc) ...
95 && ist > 0 && any(~cellfun(@isempty, sn.retrialProc(ist,:)));
97 % Retrial station: enumerate every (in-service,orbit) split, incl. idle-server states -- see _kb/04-networkstruct.md
100 space = zeros(1, sum(phases));
104 for csrv = 0:min(n(r), S(ist))
106 buf = [zeros(1, maxorbit-orbit), r*ones(1, orbit)];
110 srv = State.cartesian(srv, State.spaceClosedSingle(phases(cls), csrv));
112 srv = State.cartesian(srv, State.spaceClosedSingle(phases(cls), 0));
115 space = [space; repmat(buf, size(srv,1), 1), srv];
120 case SchedStrategy.EXT
122 if ~isempty(sn.proc) && ~isempty(sn.proc{ist}{r}) && any(any(isnan(sn.proc{ist}{r}{1}))) % disabled
123 init_r = 0*ones(1,phases(r));
124 elseif isfield(sn,
'markidx') && ~isempty(sn.markidx) ...
125 && ist <= size(sn.markidx,1) && sn.markidx(ist,r) > 1
126 % marked non-carrier
class: single always-zero
column
129 init_r = State.spaceClosedSingle(phases(r),1);
131 state = State.cartesian(state,init_r);
133 space = State.cartesian(space,state); %server part
134 space = [Inf*ones(size(space,1),1),space]; % attach infinite buffer before servers
135 case {SchedStrategy.INF, SchedStrategy.PS, SchedStrategy.DPS, SchedStrategy.GPS, SchedStrategy.PSPRIO, SchedStrategy.DPSPRIO, SchedStrategy.GPSPRIO, SchedStrategy.LPS}
136 % in these policies we only track the jobs in the servers
138 init_r = State.spaceClosedSingle(phases(r),n(r));
139 state = State.cartesian(state,init_r);
141 space = State.cartesian(space,state);
142 case SchedStrategy.POLLING
143 % Un-ordered per-
class buffers and a single server, as in SIRO,
144 % but NOT work-conserving over the station: the server may be
145 % idle
while jobs wait, because it
is walking towards a buffer
146 % (a switchover) or
is parked. Both the empty-facility and the
147 % one-job-in-service configurations are therefore enumerated;
148 % the controller columns appended after the routing variables
149 % below discard the combinations the discipline cannot occupy.
151 line_error(mfilename,
'Polling stations must have a single server.');
153 space = [n, zeros(1, sum(phases))]; % service facility empty
156 bufp = n; bufp(p) = bufp(p) - 1;
159 srvp = State.cartesian(srvp, State.spaceClosedSingle(phases(cls),
double(cls==p)));
161 space = [space; repmat(bufp, size(srvp,1), 1), srvp];
164 case {SchedStrategy.SIRO, SchedStrategy.LEPT, SchedStrategy.SEPT, SchedStrategy.SRPT, SchedStrategy.SRPTPRIO, SchedStrategy.SETF, SchedStrategy.FSP}
165 % Unordered buffer + in-server jobs -- see _kb/04-networkstruct.md
166 % build list of job classes in the node, with repetition
169 init_r = State.spaceClosedSingle(phases(r),n(r));
170 state = State.cartesian(state,init_r);
172 space = State.cartesian(space,[zeros(size(state,1),R),state]);
174 si = multichoosecon(n,S(ist)); % jobs of
class r that are running
175 mi_buf = repmat(n,size(si,1),1) - si; % jobs of
class r in buffer
177 % determine number of classes r jobs running in phase j
180 init_r = State.spaceClosedSingle(phases(r),si(k,r));
181 kstate = State.cartesian(kstate,init_r);
183 state = [repmat(mi_buf(k,:),size(kstate,1),1), kstate];
184 space = [space; state];
187 case {SchedStrategy.FCFSPI, SchedStrategy.FCFSPR, SchedStrategy.FCFSPIPRIO, SchedStrategy.FCFSPRPRIO, SchedStrategy.LCFSPI, SchedStrategy.LCFSPR, SchedStrategy.LCFSPIPRIO, SchedStrategy.LCFSPRPRIO, SchedStrategy.EDF}
188 sizeEstimator = multinomialln(n) - gammaln(sum(n)) + gammaln(1+sn.cap(ist));
189 sizeEstimator = round(sizeEstimator/log(10));
191 if ~isfield(options,
'force') || options.force ==
false
192 %line_warning(mfilename,sprintf(
'Marginal state space size is in the order of thousands of states. Computation may be slow.',sizeEstimator));
197 % Preempt-resume buffer
is (
class,phase) pairs; empty buffer must be even-width -- see _kb/04-networkstruct.md
198 space = zeros(1,2+sum(phases));
199 space = sub_routevars(sn, ind, R, space);
200 space = sub_trailingvars(sn, ind, R, space, n);
203 % Ordered buffer + in-server jobs -- see _kb/04-networkstruct.md
205 % build list of job classes in the node, with repetition
209 vi=[vi, r*ones(1,n(r))];
213 % gen permutation of their positions in the waiting buffer
214 mi = uniqueperms(vi);
215 % now generate server states
217 mi_buf = zeros(1,max(0,sum(n)-S(ist)));
219 state = State.cartesian(state,[mi_buf,state]);
221 mi = mi(:,(end-min(sum(n),sn.cap(ist))+1):end); % n(r) may count more than once elements within the same chain
222 mi = unique(mi,
'rows');
223 % mi_buf:
class of job in buffer position i (0=empty)
224 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))];
226 mi_buf = zeros(size(mi,1),1);
230 % generate job phases
for all buffer states
231 %
for k=1:size(mi_buf,1)
232 % mi_buf_kstate(end+1:end+size(bkstate,1),1:size(bkstate,2)) = bkstate;
235 % mi_srv:
class of job running in server i
236 mi_srv = mi(:,max(size(mi,2)-S(ist)+1,1):end);
237 % si: number of
class r jobs that are running
239 for k=1:size(mi_srv,1)
240 %si(k,1:R) = hist(mi_srv(k,:),1:R); % deprecated
241 si(k, 1:R) = histcounts(mi_srv(k, :), 1:(R+1));
243 %si = unique(si,
'rows');
245 % determine number of
class r jobs running in phase
246 % j in server state mi_srv(kjs,:) and build
250 kstate = State.cartesian(kstate,State.spaceClosedSingle(phases(r),si(k,r)));
252 % generate job phases
for all buffer states
254 for j=mi_buf(k,:) % for each job in the buffer
256 bkstate = State.cartesian(bkstate,[1:phases(j)]
');
261 bufstate_tmp = State.cartesian(mi_buf(k,:), bkstate);
262 % here interleave positions of class and phases in
264 bufstate = zeros(size(bufstate_tmp));
265 bufstate(:,1:2:end)=bufstate_tmp(:,1:size(mi_buf,2));
266 bufstate(:,2:2:end)=bufstate_tmp(:,(size(mi_buf,2)+1):end);
267 state = [state; State.cartesian(bufstate, kstate)];
271 case {SchedStrategy.FCFS, SchedStrategy.HOL, SchedStrategy.FCFSPRIO, SchedStrategy.LCFS, SchedStrategy.LCFSPRIO, SchedStrategy.EDD}
272 sizeEstimator = multinomialln(n) - gammaln(sum(n)) + gammaln(1+sn.cap(ist));
273 sizeEstimator = round(sizeEstimator/log(10));
275 if ~isfield(options,'force
') || options.force == false
276 %line_warning(mfilename,sprintf('Marginal state space size
is in the order of thousands of states. Computation may be slow.
',sizeEstimator));
281 space = zeros(1,1+sum(phases));
282 if sn.nodetype(ind) ~= NodeType.Source
284 switch sn.procid(sn.nodeToStation(ind),r)
285 case {ProcessType.MAP, ProcessType.MMPP2}
286 space = State.cartesian(space, [1:sn.phases(ind,r)]');
290 % Routing vars precede the node block in the nvars layout.
291 space = sub_routevars(sn, ind, R, space);
292 space = sub_trailingvars(sn, ind, R, space, n);
295 % Ordered buffer + in-server jobs -- see _kb/04-networkstruct.md
297 % build list of job classes in the node, with repetition
301 vi=[vi, r*ones(1,n(r))];
305 % gen permutation of their positions in the waiting buffer
306 mi = uniqueperms(vi);
307 % now generate server states
309 mi_buf = zeros(1,max(0,sum(n)-S(ist)));
311 state = State.cartesian(state,[mi_buf,state]);
313 mi = mi(:,(end-min(sum(n),sn.cap(ist))+1):end); % n(r) may count more than once elements within the same chain
314 mi = unique(mi,
'rows');
315 % mi_buf:
class of job in buffer position i (0=empty)
316 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))];
318 mi_buf = zeros(size(mi,1),1);
320 % mi_srv:
class of job running in server i
321 mi_srv = mi(:,max(size(mi,2)-S(ist)+1,1):end);
322 % si: number of
class r jobs that are running
324 for k=1:size(mi_srv,1)
325 %si(k,1:R) = hist(mi_srv(k,:),1:R); % deprecated
326 si(k, 1:R) = histcounts(mi_srv(k, :), 1:(R+1));
328 %si = unique(si,
'rows');
330 % determine number of
class r jobs running in phase
331 % j in server state mi_srv(k,:) and build
337 init_r = State.spaceClosedSingle(phases(r),si(k,r));
338 if sn.procid(sn.nodeToStation(ind),r) == ProcessType.MAP || sn.procid(sn.nodeToStation(ind),r) == ProcessType.MMPP2
340 init_r = State.cartesian(init_r, [1:phases(r)]
');
343 % Single server case (original logic)
344 init_r = State.cartesian(init_r, 0);
345 for i=1:size(init_r,1)
346 if init_r(i,end) == 0
347 init_r(i,end) = find(init_r(i,:));
351 % Multiserver case: FCFS-aware phase selection
352 % Use original approach but bias toward occupied phases
355 for i=1:size(init_r,1)
356 phase_dist = init_r(i, 1:phases(r));
358 % Find phases that have jobs (occupied phases)
359 occupied_phases = find(phase_dist > 0);
361 if ~isempty(occupied_phases)
362 % For FCFS, include occupied phases plus adjacent phases
363 % to account for phase transitions during service
364 extended_phases = [];
365 for op = occupied_phases
366 extended_phases = [extended_phases, op];
367 % Add adjacent phases for smooth transitions
369 extended_phases = [extended_phases, op-1];
372 extended_phases = [extended_phases, op+1];
375 extended_phases = unique(extended_phases);
376 phase_list = [phase_list; extended_phases'];
378 % No jobs - include all phases (original behavior)
379 phase_list = [phase_list; [1:phases(r)]
'];
383 % Remove duplicates and create cartesian product
384 phase_list = unique(phase_list);
385 init_r = State.cartesian(init_r, phase_list);
389 kstate = State.cartesian(kstate,init_r);
390 if sn.procid(sn.nodeToStation(ind),r) == ProcessType.MAP || sn.procid(sn.nodeToStation(ind),r) == ProcessType.MMPP2
391 map_cols(end+1) = size(kstate,2);
394 kstate = kstate(:,[setdiff(1:size(kstate,2),map_cols),map_cols]);
395 state = [state; repmat(mi_buf(k,:),size(kstate,1),1), kstate];
399 case SchedStrategy.PAS
400 % PAS/OI: local state is the full ordered class-index list, left-aligned and zero-padded -- see _kb/04-networkstruct.md
403 line_error(mfilename,'PAS stations require finite capacity
for state-space generation.
');
408 space = zeros(0, W); % infeasible: exceeds total capacity
413 vi=[vi, r*ones(1,n(r))];
416 mi = uniqueperms(vi);
417 space = [mi, zeros(size(mi,1), W - size(mi,2))];
419 case {SchedStrategy.SJF, SchedStrategy.LJF}
420 % in these policies the state space includes continuous
421 % random variables for the service times
422 line_error(mfilename,'The scheduling policy does not admit a discrete state space.\n
');
424 end % if isRetrialStation
425 space = sub_routevars(sn, ind, R, space);
426 % The polling controller trails the routing variables, matching the
427 % nvars column order (modulation, routing, node block).
428 space = State.pollingSpace(sn, ind, space);
429 % True BAS blocked marker (nvars col 2*R+1): gate on sn.isbasblocking, not the station's own drop rule (BUG-83) -- see _kb/04-networkstruct.md
430 if ~isempty(sn.isbasblocking) && numel(sn.isbasblocking) >= ind ...
431 && sn.isbasblocking(ind) == 1 && sum(n) > 0
432 space = State.cartesian(space, [0;1]);
434 % Server breakdown status (nvars col 2*R+1): enumerated for EVERY marginal incl. empty -- see _kb/04-networkstruct.md
435 if isfield(sn,
'hasbreakdown') && ~isempty(sn.hasbreakdown) && numel(sn.hasbreakdown) >= ind ...
436 && sn.hasbreakdown(ind) == 1
437 space = State.cartesian(space, [0;1]);
441 case SchedStrategy.INF
442 % in
this policies we only track the jobs in the servers
444 init_r = State.spaceClosedSingle(phases(r),n(r));
445 state = State.cartesian(state,init_r);
447 space = State.cartesian(space,state);
450 switch sn.routing(ind,r)
451 case RoutingStrategy.RROBIN
452 space = State.cartesian(space, sn.nodeparam{ind}{r}.outlinks(:));
456space = unique(space,
'rows'); %
do not comment, required to sort empty state as first
457space = space(end:-1:1,:); % so that states with jobs in phase 1 comes earlier
460function space = sub_trailingvars(sn, ind, R, space, n)
461% Append the shared trailing local-variable
column (nvars col 2*R+1) on the
462% empty-station early-return paths, which bypass the general appends at the end
463% of fromMarginal. Exactly one feature owns that
column per station, which
464% refreshLocalVars enforces.
465if isfield(sn,'hasbreakdown') && ~isempty(sn.hasbreakdown) ...
466 && numel(sn.hasbreakdown) >= ind && sn.hasbreakdown(ind) == 1
467 % Server status enumerated for the empty station too (unlike BAS/polling), else queue length inflates by ~1 job -- see _kb/04-networkstruct.md
468 space = State.cartesian(space, [0;1]);
469elseif size(sn.nvars,2) >= 2*R+1 && sn.nvars(ind, 2*R+1) == 1
470 % True BAS: keep the empty-station state width consistent (blocked=0); a
471 % job can only be held at the server when the station
is non-empty.
472 space = State.cartesian(space, 0);
476function space = sub_routevars(sn, ind, R, space)
477% Append the round-robin routing-variable columns for node IND to SPACE.
478% Every branch of fromMarginal must produce rows carrying these columns,
479% including the empty-station early returns: an empty state emitted without
480% the pointer
column misaligns in fromMarginalBounds and silently drops the
481% empty configuration from the state space, which inflates the station QLen
482% by about one job (the chain can then never empty the station).
484 switch sn.routing(ind,r)
485 case RoutingStrategy.RROBIN
486 % RR slot holds destination node index -- enumerate over outlinks.
487 space = State.cartesian(space, sn.nodeparam{ind}{r}.outlinks(:));
488 case RoutingStrategy.WRROBIN
489 % WRR slot holds POSITION in weighted_outlinks.
490 np = sn.nodeparam{ind}{r};
491 if isfield(np,
'weighted_outlinks') && ~isempty(np.weighted_outlinks)
492 positions = (1:length(np.weighted_outlinks))';
494 positions = (1:length(np.outlinks))';
496 space = State.cartesian(space, positions);