LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
fromMarginal.m
1function space = fromMarginal(sn, ind, n, options)
2% FROMMARGINAL Generate state space with specific marginal queue lengths
3%
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
10%
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.
14%
15% Copyright (c) 2012-2026, Imperial College London
16% All rights reserved.
17
18if nargin<4 %~exist('options','var')
19 options.force = false;
20end
21if isa(sn,'Network')
22 sn=sn.getStruct();
23end
24
25
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
28R = sn.nclasses;
29S = sn.nservers;
30state = [];
31space = [];
32
33% ind: node index
34ist = sn.nodeToStation(ind);
35%isf = sn.nodeToStateful(ind);
36
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
40 space = n(:)';
41 return
42end
43
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.')
47 end
48end
49
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};
56 else
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))];
64 end
65 return
66 end
67 for r=1:R
68 init_r = State.spaceClosedSingle(1,n(r));
69 state = State.cartesian(state,init_r);
70 end
71 space = State.cartesian(space,state);
72 return
73end
74
75phases = zeros(1,R);
76for r=1:R
77 if isempty(sn.proc{ist}{r})
78 phases(r) = 0;
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
82 phases(r) = 1;
83 else
84 phases(r) = length(sn.proc{ist}{r}{1});
85 end
86end
87if (sn.sched(ist) ~= SchedStrategy.EXT) && any(n>sn.classcap(ist,:))
88 return
89end
90
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,:)));
96 if isRetrialStation
97 % Retrial station: enumerate every (in-service,orbit) split, incl. idle-server states -- see _kb/04-networkstruct.md
98 r = find(n>0, 1);
99 if isempty(r)
100 space = zeros(1, sum(phases));
101 else
102 maxorbit = n(r);
103 space = [];
104 for csrv = 0:min(n(r), S(ist))
105 orbit = n(r) - csrv;
106 buf = [zeros(1, maxorbit-orbit), r*ones(1, orbit)];
107 srv = [];
108 for cls = 1:R
109 if cls == r
110 srv = State.cartesian(srv, State.spaceClosedSingle(phases(cls), csrv));
111 else
112 srv = State.cartesian(srv, State.spaceClosedSingle(phases(cls), 0));
113 end
114 end
115 space = [space; repmat(buf, size(srv,1), 1), srv];
116 end
117 end
118 else
119 switch sn.sched(ist)
120 case SchedStrategy.EXT
121 for r=1:R
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
127 init_r = 0;
128 else
129 init_r = State.spaceClosedSingle(phases(r),1);
130 end
131 state = State.cartesian(state,init_r);
132 end
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
137 for r=1:R
138 init_r = State.spaceClosedSingle(phases(r),n(r));
139 state = State.cartesian(state,init_r);
140 end
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.
150 if S(ist) ~= 1
151 line_error(mfilename,'Polling stations must have a single server.');
152 end
153 space = [n, zeros(1, sum(phases))]; % service facility empty
154 for p=1:R
155 if n(p) > 0
156 bufp = n; bufp(p) = bufp(p) - 1;
157 srvp = [];
158 for cls=1:R
159 srvp = State.cartesian(srvp, State.spaceClosedSingle(phases(cls), double(cls==p)));
160 end
161 space = [space; repmat(bufp, size(srvp,1), 1), srvp];
162 end
163 end
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
167 if sum(n) <= S(ist)
168 for r=1:R
169 init_r = State.spaceClosedSingle(phases(r),n(r));
170 state = State.cartesian(state,init_r);
171 end
172 space = State.cartesian(space,[zeros(size(state,1),R),state]);
173 else
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
176 for k=1:size(si,1)
177 % determine number of classes r jobs running in phase j
178 kstate=[];
179 for r=1:R
180 init_r = State.spaceClosedSingle(phases(r),si(k,r));
181 kstate = State.cartesian(kstate,init_r);
182 end
183 state = [repmat(mi_buf(k,:),size(kstate,1),1), kstate];
184 space = [space; state];
185 end
186 end
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));
190 if sizeEstimator > 3
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));
193 end
194 end
195
196 if sum(n) == 0
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);
201 return
202 end
203 % Ordered buffer + in-server jobs -- see _kb/04-networkstruct.md
204
205 % build list of job classes in the node, with repetition
206 vi = [];
207 for r=1:R
208 if n(r)>0
209 vi=[vi, r*ones(1,n(r))];
210 end
211 end
212
213 % gen permutation of their positions in the waiting buffer
214 mi = uniqueperms(vi);
215 % now generate server states
216 if isempty(mi)
217 mi_buf = zeros(1,max(0,sum(n)-S(ist)));
218 state = zeros(1,R);
219 state = State.cartesian(state,[mi_buf,state]);
220 else
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))];
225 if isempty(mi_buf)
226 mi_buf = zeros(size(mi,1),1);
227 end
228 mi_buf_kstate = [];
229 %if mi_buf(1)>0
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;
233 %end
234 %end
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
238 si =[];
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));
242 end
243 %si = unique(si,'rows');
244 for k=1:size(si,1)
245 % determine number of class r jobs running in phase
246 % j in server state mi_srv(kjs,:) and build
247 % state
248 kstate=[];
249 for r=1:R
250 kstate = State.cartesian(kstate,State.spaceClosedSingle(phases(r),si(k,r)));
251 end
252 % generate job phases for all buffer states
253 bkstate = [];
254 for j=mi_buf(k,:) % for each job in the buffer
255 if j>0
256 bkstate = State.cartesian(bkstate,[1:phases(j)]');
257 else
258 bkstate = 0;
259 end
260 end
261 bufstate_tmp = State.cartesian(mi_buf(k,:), bkstate);
262 % here interleave positions of class and phases in
263 % buf
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)];
268 end
269 end
270 space = state;
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));
274 if sizeEstimator > 3
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));
277 end
278 end
279
280 if sum(n) == 0
281 space = zeros(1,1+sum(phases));
282 if sn.nodetype(ind) ~= NodeType.Source
283 for r=1:R
284 switch sn.procid(sn.nodeToStation(ind),r)
285 case {ProcessType.MAP, ProcessType.MMPP2}
286 space = State.cartesian(space, [1:sn.phases(ind,r)]');
287 end
288 end
289 end
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);
293 return
294 end
295 % Ordered buffer + in-server jobs -- see _kb/04-networkstruct.md
296
297 % build list of job classes in the node, with repetition
298 vi = [];
299 for r=1:R
300 if n(r)>0
301 vi=[vi, r*ones(1,n(r))];
302 end
303 end
304
305 % gen permutation of their positions in the waiting buffer
306 mi = uniqueperms(vi);
307 % now generate server states
308 if isempty(mi)
309 mi_buf = zeros(1,max(0,sum(n)-S(ist)));
310 state = zeros(1,R);
311 state = State.cartesian(state,[mi_buf,state]);
312 else
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))];
317 if isempty(mi_buf)
318 mi_buf = zeros(size(mi,1),1);
319 end
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
323 si =[];
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));
327 end
328 %si = unique(si,'rows');
329 for k=1:size(si,1)
330 % determine number of class r jobs running in phase
331 % j in server state mi_srv(k,:) and build
332 % state
333 kstate=[];
334 map_cols = [];
335
336 for r=1:R
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
339 if si(k,r) == 0
340 init_r = State.cartesian(init_r, [1:phases(r)]');
341 else
342 if S(ist) == 1
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,:));
348 end
349 end
350 else
351 % Multiserver case: FCFS-aware phase selection
352 % Use original approach but bias toward occupied phases
353 phase_list = [];
354
355 for i=1:size(init_r,1)
356 phase_dist = init_r(i, 1:phases(r));
357
358 % Find phases that have jobs (occupied phases)
359 occupied_phases = find(phase_dist > 0);
360
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
368 if op > 1
369 extended_phases = [extended_phases, op-1];
370 end
371 if op < phases(r)
372 extended_phases = [extended_phases, op+1];
373 end
374 end
375 extended_phases = unique(extended_phases);
376 phase_list = [phase_list; extended_phases'];
377 else
378 % No jobs - include all phases (original behavior)
379 phase_list = [phase_list; [1:phases(r)]'];
380 end
381 end
382
383 % Remove duplicates and create cartesian product
384 phase_list = unique(phase_list);
385 init_r = State.cartesian(init_r, phase_list);
386 end
387 end
388 end
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);
392 end
393 end
394 kstate = kstate(:,[setdiff(1:size(kstate,2),map_cols),map_cols]);
395 state = [state; repmat(mi_buf(k,:),size(kstate,1),1), kstate];
396 end
397 end
398 space = state;
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
401 W = sn.cap(ist);
402 if isinf(W)
403 line_error(mfilename,'PAS stations require finite capacity for state-space generation.');
404 end
405 if sum(n) == 0
406 space = zeros(1, W);
407 elseif sum(n) > W
408 space = zeros(0, W); % infeasible: exceeds total capacity
409 else
410 vi = [];
411 for r=1:R
412 if n(r)>0
413 vi=[vi, r*ones(1,n(r))];
414 end
415 end
416 mi = uniqueperms(vi);
417 space = [mi, zeros(size(mi,1), W - size(mi,2))];
418 end
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');
423 end
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]);
433 end
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]);
438 end
439 case NodeType.Cache
440 switch sn.sched(ist)
441 case SchedStrategy.INF
442 % in this policies we only track the jobs in the servers
443 for r=1:R
444 init_r = State.spaceClosedSingle(phases(r),n(r));
445 state = State.cartesian(state,init_r);
446 end
447 space = State.cartesian(space,state);
448 end
449 for r=1:R
450 switch sn.routing(ind,r)
451 case RoutingStrategy.RROBIN
452 space = State.cartesian(space, sn.nodeparam{ind}{r}.outlinks(:));
453 end
454 end
455end
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
458end
459
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);
473end
474end
475
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).
483for r=1:sn.nclasses
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))';
493 else
494 positions = (1:length(np.outlinks))';
495 end
496 space = State.cartesian(space, positions);
497 end
498end
499end