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: 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).
84 phases(r) = 1;
85 else
86 phases(r) = length(sn.proc{ist}{r}{1});
87 end
88end
89if (sn.sched(ist) ~= SchedStrategy.EXT) && any(n>sn.classcap(ist,:))
90 return
91end
92
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,:)));
98 if isRetrialStation
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).
107 r = find(n>0, 1);
108 if isempty(r)
109 space = zeros(1, sum(phases));
110 else
111 maxorbit = n(r);
112 space = [];
113 for csrv = 0:min(n(r), S(ist))
114 orbit = n(r) - csrv;
115 buf = [zeros(1, maxorbit-orbit), r*ones(1, orbit)];
116 srv = [];
117 for cls = 1:R
118 if cls == r
119 srv = State.cartesian(srv, State.spaceClosedSingle(phases(cls), csrv));
120 else
121 srv = State.cartesian(srv, State.spaceClosedSingle(phases(cls), 0));
122 end
123 end
124 space = [space; repmat(buf, size(srv,1), 1), srv];
125 end
126 end
127 else
128 switch sn.sched(ist)
129 case SchedStrategy.EXT
130 for r=1:R
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
136 init_r = 0;
137 else
138 init_r = State.spaceClosedSingle(phases(r),1);
139 end
140 state = State.cartesian(state,init_r);
141 end
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
146 for r=1:R
147 init_r = State.spaceClosedSingle(phases(r),n(r));
148 state = State.cartesian(state,init_r);
149 end
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.
159 if S(ist) ~= 1
160 line_error(mfilename,'Polling stations must have a single server.');
161 end
162 space = [n, zeros(1, sum(phases))]; % service facility empty
163 for p=1:R
164 if n(p) > 0
165 bufp = n; bufp(p) = bufp(p) - 1;
166 srvp = [];
167 for cls=1:R
168 srvp = State.cartesian(srvp, State.spaceClosedSingle(phases(cls), double(cls==p)));
169 end
170 space = [space; repmat(bufp, size(srvp,1), 1), srvp];
171 end
172 end
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
175 % the jobs in the servers
176 % build list of job classes in the node, with repetition
177 if sum(n) <= S(ist)
178 for r=1:R
179 init_r = State.spaceClosedSingle(phases(r),n(r));
180 state = State.cartesian(state,init_r);
181 end
182 space = State.cartesian(space,[zeros(size(state,1),R),state]);
183 else
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
186 for k=1:size(si,1)
187 % determine number of classes r jobs running in phase j
188 kstate=[];
189 for r=1:R
190 init_r = State.spaceClosedSingle(phases(r),si(k,r));
191 kstate = State.cartesian(kstate,init_r);
192 end
193 state = [repmat(mi_buf(k,:),size(kstate,1),1), kstate];
194 space = [space; state];
195 end
196 end
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));
200 if sizeEstimator > 3
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));
203 end
204 end
205
206 if sum(n) == 0
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);
214 return
215 end
216 % in these policies we track an ordered buffer and
217 % the jobs in the servers
218
219 % build list of job classes in the node, with repetition
220 vi = [];
221 for r=1:R
222 if n(r)>0
223 vi=[vi, r*ones(1,n(r))];
224 end
225 end
226
227 % gen permutation of their positions in the waiting buffer
228 mi = uniqueperms(vi);
229 % now generate server states
230 if isempty(mi)
231 mi_buf = zeros(1,max(0,sum(n)-S(ist)));
232 state = zeros(1,R);
233 state = State.cartesian(state,[mi_buf,state]);
234 else
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))];
239 if isempty(mi_buf)
240 mi_buf = zeros(size(mi,1),1);
241 end
242 mi_buf_kstate = [];
243 %if mi_buf(1)>0
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;
247 %end
248 %end
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
252 si =[];
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));
256 end
257 %si = unique(si,'rows');
258 for k=1:size(si,1)
259 % determine number of class r jobs running in phase
260 % j in server state mi_srv(kjs,:) and build
261 % state
262 kstate=[];
263 for r=1:R
264 kstate = State.cartesian(kstate,State.spaceClosedSingle(phases(r),si(k,r)));
265 end
266 % generate job phases for all buffer states
267 bkstate = [];
268 for j=mi_buf(k,:) % for each job in the buffer
269 if j>0
270 bkstate = State.cartesian(bkstate,[1:phases(j)]');
271 else
272 bkstate = 0;
273 end
274 end
275 bufstate_tmp = State.cartesian(mi_buf(k,:), bkstate);
276 % here interleave positions of class and phases in
277 % buf
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)];
282 end
283 end
284 space = state;
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));
288 if sizeEstimator > 3
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));
291 end
292 end
293
294 if sum(n) == 0
295 space = zeros(1,1+sum(phases));
296 if sn.nodetype(ind) ~= NodeType.Source
297 for r=1:R
298 switch sn.procid(sn.nodeToStation(ind),r)
299 case {ProcessType.MAP, ProcessType.MMPP2}
300 space = State.cartesian(space, [1:sn.phases(ind,r)]');
301 end
302 end
303 end
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);
309 end
310 return
311 end
312 % in these policies we track an ordered buffer and
313 % the jobs in the servers
314
315 % build list of job classes in the node, with repetition
316 vi = [];
317 for r=1:R
318 if n(r)>0
319 vi=[vi, r*ones(1,n(r))];
320 end
321 end
322
323 % gen permutation of their positions in the waiting buffer
324 mi = uniqueperms(vi);
325 % now generate server states
326 if isempty(mi)
327 mi_buf = zeros(1,max(0,sum(n)-S(ist)));
328 state = zeros(1,R);
329 state = State.cartesian(state,[mi_buf,state]);
330 else
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))];
335 if isempty(mi_buf)
336 mi_buf = zeros(size(mi,1),1);
337 end
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
341 si =[];
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));
345 end
346 %si = unique(si,'rows');
347 for k=1:size(si,1)
348 % determine number of class r jobs running in phase
349 % j in server state mi_srv(k,:) and build
350 % state
351 kstate=[];
352 map_cols = [];
353
354 for r=1:R
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
357 if si(k,r) == 0
358 init_r = State.cartesian(init_r, [1:phases(r)]');
359 else
360 if S(ist) == 1
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,:));
366 end
367 end
368 else
369 % Multiserver case: FCFS-aware phase selection
370 % Use original approach but bias toward occupied phases
371 phase_list = [];
372
373 for i=1:size(init_r,1)
374 phase_dist = init_r(i, 1:phases(r));
375
376 % Find phases that have jobs (occupied phases)
377 occupied_phases = find(phase_dist > 0);
378
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
386 if op > 1
387 extended_phases = [extended_phases, op-1];
388 end
389 if op < phases(r)
390 extended_phases = [extended_phases, op+1];
391 end
392 end
393 extended_phases = unique(extended_phases);
394 phase_list = [phase_list; extended_phases'];
395 else
396 % No jobs - include all phases (original behavior)
397 phase_list = [phase_list; [1:phases(r)]'];
398 end
399 end
400
401 % Remove duplicates and create cartesian product
402 phase_list = unique(phase_list);
403 init_r = State.cartesian(init_r, phase_list);
404 end
405 end
406 end
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);
410 end
411 end
412 kstate = kstate(:,[setdiff(1:size(kstate,2),map_cols),map_cols]);
413 state = [state; repmat(mi_buf(k,:),size(kstate,1),1), kstate];
414 end
415 end
416 space = state;
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.
422 W = sn.cap(ist);
423 if isinf(W)
424 line_error(mfilename,'PAS stations require finite capacity for state-space generation.');
425 end
426 if sum(n) == 0
427 space = zeros(1, W);
428 elseif sum(n) > W
429 space = zeros(0, W); % infeasible: exceeds total capacity
430 else
431 vi = [];
432 for r=1:R
433 if n(r)>0
434 vi=[vi, r*ones(1,n(r))];
435 end
436 end
437 mi = uniqueperms(vi);
438 space = [mi, zeros(size(mi,1), W - size(mi,2))];
439 end
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');
444 end
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]);
460 end
461 case NodeType.Cache
462 switch sn.sched(ist)
463 case SchedStrategy.INF
464 % in this policies we only track the jobs in the servers
465 for r=1:R
466 init_r = State.spaceClosedSingle(phases(r),n(r));
467 state = State.cartesian(state,init_r);
468 end
469 space = State.cartesian(space,state);
470 end
471 for r=1:R
472 switch sn.routing(ind,r)
473 case RoutingStrategy.RROBIN
474 space = State.cartesian(space, sn.nodeparam{ind}{r}.outlinks(:));
475 end
476 end
477end
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
480end
481
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).
489for r=1:sn.nclasses
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))';
499 else
500 positions = (1:length(np.outlinks))';
501 end
502 space = State.cartesian(space, positions);
503 end
504end
505end
Definition Station.m:245