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:
the modulating chain lives in
the
82 % carrier's phase block; this class holds a single always-zero
column
83 % (mirrors sn.phasessz in refreshProcessRepresentations).
86 phases(r) = length(sn.proc{ist}{r}{1});
89if (sn.sched(ist) ~= SchedStrategy.EXT) && any(n>sn.classcap(ist,:))
93% generate local-state space
94switch sn.nodetype(ind)
95 case {NodeType.Queue, NodeType.Delay, NodeType.Source, NodeType.Place}
96 isRetrialStation = isfield(sn,
'retrialProc') && ~isempty(sn.retrialProc) ...
97 && ist > 0 && any(~cellfun(@isempty, sn.retrialProc(ist,:)));
99 % Retrial station: enumerate every (in-service, orbit) split, not
100 % just
the work-conserving one. The server holds csrv in
101 % 0..min(n,S) jobs and
the remaining (orbit) jobs sit in
the buffer
102 % (
class-
id slots, right-aligned). The idle-server states (csrv
103 % below min(n,S)) are essential: an orbiting job re-enters service
104 % only through a RETRY event, so a server may be idle while
the
105 % orbit
is non-empty. Scoped to a single populated class per
106 % retrial station (enforced by
the CTMC/SSA analyzer guard).
109 space = zeros(1, sum(phases));
113 for csrv = 0:min(n(r), S(ist))
115 buf = [zeros(1, maxorbit-orbit), r*ones(1, orbit)];
119 srv = State.cartesian(srv, State.spaceClosedSingle(phases(cls), csrv));
121 srv = State.cartesian(srv, State.spaceClosedSingle(phases(cls), 0));
124 space = [space; repmat(buf, size(srv,1), 1), srv];
129 case SchedStrategy.EXT
131 if ~isempty(sn.proc) && ~isempty(sn.proc{ist}{r}) && any(any(isnan(sn.proc{ist}{r}{1}))) % disabled
132 init_r = 0*ones(1,phases(r));
133 elseif isfield(sn,
'markidx') && ~isempty(sn.markidx) ...
134 && ist <= size(sn.markidx,1) && sn.markidx(ist,r) > 1
135 % marked non-carrier
class: single always-zero
column
138 init_r = State.spaceClosedSingle(phases(r),1);
140 state = State.cartesian(state,init_r);
142 space = State.cartesian(space,state); %server part
143 space = [Inf*ones(size(space,1),1),space]; % attach infinite buffer before servers
144 case {SchedStrategy.INF, SchedStrategy.PS, SchedStrategy.DPS, SchedStrategy.GPS, SchedStrategy.PSPRIO, SchedStrategy.DPSPRIO, SchedStrategy.GPSPRIO, SchedStrategy.LPS}
145 % in these policies we only track
the jobs in
the servers
147 init_r = State.spaceClosedSingle(phases(r),n(r));
148 state = State.cartesian(state,init_r);
150 space = State.cartesian(space,state);
151 case SchedStrategy.POLLING
152 % Un-ordered per-
class buffers and a single server, as in SIRO,
153 % but NOT work-conserving over
the station:
the server may be
154 % idle
while jobs wait, because it
is walking towards a buffer
155 % (a switchover) or
is parked. Both
the empty-facility and
the
156 % one-job-in-service configurations are therefore enumerated;
157 %
the controller columns appended after
the routing variables
158 % below discard
the combinations
the discipline cannot occupy.
160 line_error(mfilename,
'Polling stations must have a single server.');
162 space = [n, zeros(1, sum(phases))]; % service facility empty
165 bufp = n; bufp(p) = bufp(p) - 1;
168 srvp = State.cartesian(srvp, State.spaceClosedSingle(phases(cls),
double(cls==p)));
170 space = [space; repmat(bufp, size(srvp,1), 1), srvp];
173 case {SchedStrategy.SIRO, SchedStrategy.LEPT, SchedStrategy.SEPT, SchedStrategy.SRPT, SchedStrategy.SRPTPRIO, SchedStrategy.SETF, SchedStrategy.FSP}
174 % in these policies we track an un-ordered buffer and
176 % build list of job classes in
the node, with repetition
179 init_r = State.spaceClosedSingle(phases(r),n(r));
180 state = State.cartesian(state,init_r);
182 space = State.cartesian(space,[zeros(size(state,1),R),state]);
184 si = multichoosecon(n,S(ist)); % jobs of
class r that are running
185 mi_buf = repmat(n,size(si,1),1) - si; % jobs of
class r in buffer
187 % determine number of classes r jobs running in phase j
190 init_r = State.spaceClosedSingle(phases(r),si(k,r));
191 kstate = State.cartesian(kstate,init_r);
193 state = [repmat(mi_buf(k,:),size(kstate,1),1), kstate];
194 space = [space; state];
197 case {SchedStrategy.FCFSPI, SchedStrategy.FCFSPR, SchedStrategy.FCFSPIPRIO, SchedStrategy.FCFSPRPRIO, SchedStrategy.LCFSPI, SchedStrategy.LCFSPR, SchedStrategy.LCFSPIPRIO, SchedStrategy.LCFSPRPRIO, SchedStrategy.EDF}
198 sizeEstimator = multinomialln(n) - gammaln(sum(n)) + gammaln(1+sn.cap(ist));
199 sizeEstimator = round(sizeEstimator/log(10));
201 if ~isfield(options,
'force') || options.force ==
false
202 %line_warning(mfilename,sprintf(
'Marginal state space size is in the order of thousands of states. Computation may be slow.',sizeEstimator));
207 % Preempt-resume/independent track
the buffer as (
class,phase)
208 % PAIRS (2 columns per waiting job), so
the empty buffer must be
209 % even-width; a lone extra
column is half a pair and makes
the
210 % simulator drop
the first preempted job (server left idle with a
211 % job
"waiting"). Use one empty pair (width 2), matching JAR/Python.
212 space = zeros(1,2+sum(phases));
213 space = sub_routevars(sn, ind, R, space);
216 % in these policies we track an ordered buffer and
219 % build list of job classes in
the node, with repetition
223 vi=[vi, r*ones(1,n(r))];
227 % gen permutation of their positions in
the waiting buffer
228 mi = uniqueperms(vi);
229 % now generate server states
231 mi_buf = zeros(1,max(0,sum(n)-S(ist)));
233 state = State.cartesian(state,[mi_buf,state]);
235 mi = mi(:,(end-min(sum(n),sn.cap(ist))+1):end); % n(r) may
count more than once elements within
the same chain
236 mi = unique(mi,
'rows');
237 % mi_buf:
class of job in buffer position i (0=empty)
238 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))];
240 mi_buf = zeros(size(mi,1),1);
244 % generate job phases
for all buffer states
245 %
for k=1:size(mi_buf,1)
246 % mi_buf_kstate(end+1:end+size(bkstate,1),1:size(bkstate,2)) = bkstate;
249 % mi_srv:
class of job running in server i
250 mi_srv = mi(:,max(size(mi,2)-S(ist)+1,1):end);
251 % si: number of
class r jobs that are running
253 for k=1:size(mi_srv,1)
254 %si(k,1:R) = hist(mi_srv(k,:),1:R); % deprecated
255 si(k, 1:R) = histcounts(mi_srv(k, :), 1:(R+1));
257 %si = unique(si,
'rows');
259 % determine number of
class r jobs running in phase
260 % j in server state mi_srv(kjs,:) and build
264 kstate = State.cartesian(kstate,State.spaceClosedSingle(phases(r),si(k,r)));
266 % generate job phases
for all buffer states
268 for j=mi_buf(k,:) % for each job in
the buffer
270 bkstate = State.cartesian(bkstate,[1:phases(j)]
');
275 bufstate_tmp = State.cartesian(mi_buf(k,:), bkstate);
276 % here interleave positions of class and phases in
278 bufstate = zeros(size(bufstate_tmp));
279 bufstate(:,1:2:end)=bufstate_tmp(:,1:size(mi_buf,2));
280 bufstate(:,2:2:end)=bufstate_tmp(:,(size(mi_buf,2)+1):end);
281 state = [state; State.cartesian(bufstate, kstate)];
285 case {SchedStrategy.FCFS, SchedStrategy.HOL, SchedStrategy.FCFSPRIO, SchedStrategy.LCFS, SchedStrategy.LCFSPRIO, SchedStrategy.EDD}
286 sizeEstimator = multinomialln(n) - gammaln(sum(n)) + gammaln(1+sn.cap(ist));
287 sizeEstimator = round(sizeEstimator/log(10));
289 if ~isfield(options,'force
') || options.force == false
290 %line_warning(mfilename,sprintf('Marginal state space size
is in
the order of thousands of states. Computation may be slow.
',sizeEstimator));
295 space = zeros(1,1+sum(phases));
296 if sn.nodetype(ind) ~= NodeType.Source
298 switch sn.procid(sn.nodeToStation(ind),r)
299 case {ProcessType.MAP, ProcessType.MMPP2}
300 space = State.cartesian(space, [1:sn.phases(ind,r)]');
304 % Routing vars precede
the node block in
the nvars layout.
305 space = sub_routevars(sn, ind, R, space);
306 % True BAS: keep
the empty-station state width consistent (blocked=0).
307 if size(sn.nvars,2) >= 2*R+1 && sn.nvars(ind, 2*R+1) == 1
308 space = State.cartesian(space, 0);
312 % in these policies we track an ordered buffer and
315 % build list of job classes in
the node, with repetition
319 vi=[vi, r*ones(1,n(r))];
323 % gen permutation of their positions in
the waiting buffer
324 mi = uniqueperms(vi);
325 % now generate server states
327 mi_buf = zeros(1,max(0,sum(n)-S(ist)));
329 state = State.cartesian(state,[mi_buf,state]);
331 mi = mi(:,(end-min(sum(n),sn.cap(ist))+1):end); % n(r) may
count more than once elements within
the same chain
332 mi = unique(mi,
'rows');
333 % mi_buf:
class of job in buffer position i (0=empty)
334 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))];
336 mi_buf = zeros(size(mi,1),1);
338 % mi_srv:
class of job running in server i
339 mi_srv = mi(:,max(size(mi,2)-S(ist)+1,1):end);
340 % si: number of
class r jobs that are running
342 for k=1:size(mi_srv,1)
343 %si(k,1:R) = hist(mi_srv(k,:),1:R); % deprecated
344 si(k, 1:R) = histcounts(mi_srv(k, :), 1:(R+1));
346 %si = unique(si,
'rows');
348 % determine number of
class r jobs running in phase
349 % j in server state mi_srv(k,:) and build
355 init_r = State.spaceClosedSingle(phases(r),si(k,r));
356 if sn.procid(sn.nodeToStation(ind),r) == ProcessType.MAP || sn.procid(sn.nodeToStation(ind),r) == ProcessType.MMPP2
358 init_r = State.cartesian(init_r, [1:phases(r)]
');
361 % Single server case (original logic)
362 init_r = State.cartesian(init_r, 0);
363 for i=1:size(init_r,1)
364 if init_r(i,end) == 0
365 init_r(i,end) = find(init_r(i,:));
369 % Multiserver case: FCFS-aware phase selection
370 % Use original approach but bias toward occupied phases
373 for i=1:size(init_r,1)
374 phase_dist = init_r(i, 1:phases(r));
376 % Find phases that have jobs (occupied phases)
377 occupied_phases = find(phase_dist > 0);
379 if ~isempty(occupied_phases)
380 % For FCFS, include occupied phases plus adjacent phases
381 % to account for phase transitions during service
382 extended_phases = [];
383 for op = occupied_phases
384 extended_phases = [extended_phases, op];
385 % Add adjacent phases for smooth transitions
387 extended_phases = [extended_phases, op-1];
390 extended_phases = [extended_phases, op+1];
393 extended_phases = unique(extended_phases);
394 phase_list = [phase_list; extended_phases'];
396 % No jobs - include all phases (original behavior)
397 phase_list = [phase_list; [1:phases(r)]
'];
401 % Remove duplicates and create cartesian product
402 phase_list = unique(phase_list);
403 init_r = State.cartesian(init_r, phase_list);
407 kstate = State.cartesian(kstate,init_r);
408 if sn.procid(sn.nodeToStation(ind),r) == ProcessType.MAP || sn.procid(sn.nodeToStation(ind),r) == ProcessType.MMPP2
409 map_cols(end+1) = size(kstate,2);
412 kstate = kstate(:,[setdiff(1:size(kstate,2),map_cols),map_cols]);
413 state = [state; repmat(mi_buf(k,:),size(kstate,1),1), kstate];
417 case SchedStrategy.PAS
418 % Pass-and-swap / order-independent queue: the local state is the
419 % full ordered list of class indices (oldest at column 1),
420 % left-aligned and right zero-padded to the station capacity.
421 % Every distinct ordering of the population is a distinct state.
424 line_error(mfilename,'PAS stations require finite capacity
for state-space generation.
');
429 space = zeros(0, W); % infeasible: exceeds total capacity
434 vi=[vi, r*ones(1,n(r))];
437 mi = uniqueperms(vi);
438 space = [mi, zeros(size(mi,1), W - size(mi,2))];
440 case {SchedStrategy.SJF, SchedStrategy.LJF}
441 % in these policies the state space includes continuous
442 % random variables for the service times
443 line_error(mfilename,'The scheduling policy does not admit a discrete state space.\n
');
445 end % if isRetrialStation
446 space = sub_routevars(sn, ind, R, space);
447 % The polling controller trails the routing variables, matching the
448 % nvars column order (modulation, routing, node block).
449 space = State.pollingSpace(sn, ind, space);
450 % True BAS blocked marker (nvars col 2*R+1 == 1): a completed job held at the
451 % server awaiting room downstream. Enumerate {0,1} when the station holds >=1 job.
452 % The marker column is shared with the polling controller, so gate on the
453 % dedicated sn.isbasblocking field (set by refreshLocalVars for exactly the
454 % blocking/upstream stations, under BOTH the upstream and destination
455 % declaration forms) rather than re-testing the station's own drop rule --
the
456 % latter fails
for a destination-declared BAS. See BUG-83.
457 if ~isempty(sn.isbasblocking) && numel(sn.isbasblocking) >= ind ...
458 && sn.isbasblocking(ind) == 1 && sum(n) > 0
459 space = State.cartesian(space, [0;1]);
463 case SchedStrategy.INF
464 % in
this policies we only track
the jobs in
the servers
466 init_r = State.spaceClosedSingle(phases(r),n(r));
467 state = State.cartesian(state,init_r);
469 space = State.cartesian(space,state);
472 switch sn.routing(ind,r)
473 case RoutingStrategy.RROBIN
474 space = State.cartesian(space, sn.nodeparam{ind}{r}.outlinks(:));
478space = unique(space,
'rows'); %
do not comment, required to sort empty state as first
479space = space(end:-1:1,:); % so that states with jobs in phase 1 comes earlier
482function space = sub_routevars(sn, ind, R, space)
483% Append
the round-robin routing-variable columns
for node IND to SPACE.
484% Every branch of fromMarginal must produce rows carrying these columns,
485% including
the empty-station early returns: an empty state emitted without
486%
the pointer
column misaligns in fromMarginalBounds and silently drops
the
487% empty configuration from
the state space, which inflates
the station QLen
488% by about one job (
the chain can then never empty
the station).
490 switch sn.routing(ind,r)
491 case RoutingStrategy.RROBIN
492 % RR slot holds destination node index -- enumerate over outlinks.
493 space = State.cartesian(space, sn.nodeparam{ind}{r}.outlinks(:));
494 case RoutingStrategy.WRROBIN
495 % WRR slot holds POSITION in weighted_outlinks.
496 np = sn.nodeparam{ind}{r};
497 if isfield(np,
'weighted_outlinks') && ~isempty(np.weighted_outlinks)
498 positions = (1:length(np.weighted_outlinks))';
500 positions = (1:length(np.outlinks))';
502 space = State.cartesian(space, positions);