LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
solver_ctmc_fcr_waitq.m
1function [stateSpace,stateSpaceAggr,stateSpaceHashed,Dfilt,sn] = solver_ctmc_fcr_waitq(sn, options)
2% [SS,SSA,SSH,DFILT,SN]=SOLVER_CTMC_FCR_WAITQ(SN,OPTIONS)
3% Reachability-based state space and per-action rate filters for models with
4% a finite capacity region (FCR) whose drop rule is WAITQ (waiting queue).
5%
6% JMT WAITQ semantics (reference, mirrored by LDES): a job refused entry to a
7% full region leaves the upstream station and waits in a per-region FIFO of
8% (class, destination) tokens outside the region; after every transition that
9% frees region capacity, tokens are released strictly in FIFO order (head-of-
10% line: a stuck head blocks the queue) as long as the admission constraints
11% (global cap, per-class caps, memory budget, linear constraints A*x<=b)
12% permit; a fresh arrival that satisfies the constraints is admitted even if
13% the FIFO is non-empty (it overtakes a head stuck on a different constraint).
14% Blocked jobs are counted neither in the region occupancy nor in any station
15% state, so station QLen excludes them, matching the JMT report convention.
16%
17% The CTMC state is augmented as [h(1:nstateful), buf_1, ..., buf_F] where h
18% are the per-node hashed states and buf_f is the token FIFO of region f,
19% padded with zeros to its maximum length. Classes whose region rule is DROP
20% keep the transition-censoring behavior of the default generator (exact for
21% memoryless sources, cross-validated against JMT).
22%
23% Copyright (c) 2012-2026, Imperial College London
24% All rights reserved.
25
26nstateful = sn.nstateful;
27K = sn.nclasses;
28sync = sn.sync;
29A = length(sync);
30csmask = sn.csmask;
31local = sn.nnodes + 1;
32
33%% feature gates: combinations that would need semantics not defined here
34if isfield(sn,'gsync') && ~isempty(sn.gsync)
35 line_error(mfilename,'WAITQ finite capacity regions are not supported together with stochastic Petri net transitions in SolverCTMC.');
36end
37if isfield(sn,'fjsync') && ~isempty(sn.fjsync)
38 line_error(mfilename,'WAITQ finite capacity regions are not supported together with fork-join in SolverCTMC.');
39end
40if any(sn.isstatedep(:,3))
41 line_error(mfilename,'WAITQ finite capacity regions are not supported together with state-dependent routing in SolverCTMC.');
42end
43
44%% region data
45F = sn.nregions;
46memberMask = false(F, sn.nstations); % (f,ist) true if station ist in region f
47ccap = inf(F, K); % per-class caps
48gcap = inf(F, 1); % global job cap
49memcap = inf(F, 1); % global memory budget
50szrow = ones(F, K); % per-class memory footprint
51linA = cell(F,1); linb = cell(F,1);
52iswaitq = false(F, K); % rule per (region, class): true=WAITQ-like, false=DROP
53for f = 1:F
54 Rmat = sn.region{f}; % M x (K+1)
55 % membership: a station is a member if any job-count cap OR the region
56 % memory budget is set on its row (a memory-only region has all job-count
57 % entries at -1)
58 memvec = -ones(sn.nstations,1);
59 if isfield(sn,'regionmaxmem') && numel(sn.regionmaxmem) >= f && ~isempty(sn.regionmaxmem{f})
60 memvec = sn.regionmaxmem{f}(:);
61 end
62 members = find(any(Rmat ~= -1, 2) | memvec ~= -1)';
63 memberMask(f, members) = true;
64 for r = 1:K
65 cv = Rmat(members, r); cv = cv(cv ~= -1);
66 if ~isempty(cv); ccap(f,r) = min(cv); end
67 iswaitq(f,r) = (sn.regionrule(f,r) ~= DropStrategy.DROP);
68 end
69 gv = Rmat(members, K+1); gv = gv(gv ~= -1);
70 if ~isempty(gv); gcap(f) = min(gv); end
71 if isfield(sn,'regionmaxmem') && numel(sn.regionmaxmem) >= f && ~isempty(sn.regionmaxmem{f})
72 mv = sn.regionmaxmem{f}(members); mv = mv(mv ~= -1);
73 if ~isempty(mv); memcap(f) = min(mv); end
74 end
75 if isfield(sn,'regionsz') && ~isempty(sn.regionsz)
76 szrow(f,:) = sn.regionsz(f,:);
77 end
78 if isfield(sn,'regionlincon') && size(sn.regionlincon,1) >= f && ~isempty(sn.regionlincon{f,1})
79 linA{f} = sn.regionlincon{f,1};
80 linb{f} = sn.regionlincon{f,2};
81 end
82end
83
84% token FIFO length bound per region: at most all closed jobs of WAITQ classes
85% plus, per open WAITQ class, the state-space cutoff of that class
86if isfield(options,'cutoff') && ~isempty(options.cutoff)
87 cutoffMat = options.cutoff;
88 if isscalar(cutoffMat)
89 cutoffMat = cutoffMat * ones(sn.nstations, K);
90 end
91else
92 cutoffMat = zeros(sn.nstations, K);
93end
94tokbound = zeros(1,K);
95for r = 1:K
96 if any(iswaitq(:,r))
97 c_ = find(sn.chains(:,r), 1); % chain of class r
98 chainpop = sum(sn.njobs(sn.chains(c_,:)));
99 if isfinite(chainpop)
100 % closed chain: jobs may switch into class r, so bound by the
101 % whole chain population rather than njobs(r)
102 tokbound(r) = chainpop;
103 else
104 tokbound(r) = max(cutoffMat(:,r));
105 end
106 end
107end
108Lmax = zeros(F,1);
109for f = 1:F
110 Lmax(f) = sum(tokbound(iswaitq(f,:)));
111end
112bufoff = nstateful + [0; cumsum(Lmax(1:end-1))]; % column offset of buf_f
113width = nstateful + sum(Lmax);
114
115%% initial augmented state (buffers empty)
116h0 = zeros(1, nstateful);
117for ind = 1:sn.nnodes
118 if sn.isstateful(ind)
119 isf = sn.nodeToStateful(ind);
120 if sn.nodetype(ind) == NodeType.Source
121 % canonical Source state (spaceGenerator convention); the stored
122 % sn.state row may omit the Inf pool marker or the arrival phase
123 hh_ = State.getHash(sn, ind, State.fromMarginal(sn, ind, []));
124 h0(isf) = hh_(1);
125 else
126 h0(isf) = State.getHash(sn, ind, sn.state{isf}(1,:));
127 end
128 if h0(isf) <= 0
129 line_error(mfilename, sprintf('Initial state of node %s not found in its local state space.', sn.nodenames{ind}));
130 end
131 end
132end
133row0 = zeros(1, width);
134row0(1:nstateful) = h0;
135for f = 1:F
136 if any(regionAggr(h0, f) > ccap(f,:)) || violates(f, regionAggr(h0, f))
137 line_error(mfilename,'The initial state violates the finite capacity region constraints.');
138 end
139end
140
141%% breadth-first construction of the reachable augmented space
142capRows = 1024;
143SSH = zeros(capRows, width);
144SSH(1,:) = row0;
145nrows = 1;
146keymap = containers.Map(rowkey(row0), 1);
147frontier = 1;
148% transition triplets per action
149capTrip = 4096;
150ta = zeros(capTrip,1); ti = zeros(capTrip,1); tj = zeros(capTrip,1); tv = zeros(capTrip,1);
151ntrip = 0;
152
153while ~isempty(frontier)
154 s = frontier(1); frontier(1) = [];
155 row = SSH(s,:);
156 h = row(1:nstateful);
157 bufs = cell(F,1);
158 for f = 1:F
159 bf = row(bufoff(f)+1:bufoff(f)+Lmax(f));
160 bufs{f} = bf(bf > 0);
161 end
162 % current per-region aggregate populations
163 xf = zeros(F, K);
164 for f = 1:F
165 xf(f,:) = regionAggr(h, f);
166 end
167 for a = 1:A
168 node_a = sync{a}.active{1}.node;
169 isf_a = sn.nodeToStateful(node_a);
170 class_a = sync{a}.active{1}.class;
171 event_a = sync{a}.active{1}.event;
172 [new_state_a, rate_a] = State.afterEventHashed(sn, node_a, h(isf_a), event_a, class_a);
173 if isequal(new_state_a, -1)
174 continue
175 end
176 for ia = 1:length(new_state_a)
177 if isnan(rate_a(ia)) || rate_a(ia) <= 0 || new_state_a(ia) == -1
178 continue
179 end
180 node_p = sync{a}.passive{1}.node;
181 if node_p == local
182 newh = h;
183 newh(isf_a) = new_state_a(ia);
184 emit(a, s, newh, bufs, rate_a(ia));
185 else
186 class_p = sync{a}.passive{1}.class;
187 event_p = sync{a}.passive{1}.event;
188 isf_p = sn.nodeToStateful(node_p);
189 % region-entry detection: passive station inside region f,
190 % active node outside it, and the passive event is an arrival
191 stat_a = 0;
192 if node_a <= sn.nnodes && sn.isstation(node_a)
193 stat_a = sn.nodeToStation(node_a);
194 end
195 stat_p = 0;
196 if sn.isstation(node_p)
197 stat_p = sn.nodeToStation(node_p);
198 end
199 blockedf = 0;
200 droppedf = 0;
201 if event_p == EventType.ARV && stat_p > 0
202 for f = 1:F
203 if memberMask(f, stat_p) && (stat_a <= 0 || ~memberMask(f, stat_a))
204 xn = xf(f,:);
205 xn(class_p) = xn(class_p) + 1;
206 if violates(f, xn)
207 if ~iswaitq(f, class_p)
208 droppedf = f; % DROP rule: the job is destroyed
209 else
210 blockedf = f;
211 end
212 break
213 end
214 end
215 end
216 end
217 if droppedf > 0
218 % DROP rule (JMT semantics): the refused job is destroyed;
219 % only the active (departing) part of the transition applies
220 newh = h;
221 newh(isf_a) = new_state_a(ia);
222 emit(a, s, newh, bufs, rate_a(ia) * sync{a}.passive{1}.prob);
223 continue
224 end
225 % class-switching hop between two members of the same region:
226 % JMT routes it through a ClassSwitch node outside the region,
227 % so it is an exit (with FIFO release) followed by a gated
228 % re-entry of the new class; only WAITQ classes are re-gated
229 switchf = 0;
230 if blockedf == 0 && event_p == EventType.ARV && class_p ~= class_a ...
231 && stat_a > 0 && stat_p > 0
232 for f = 1:F
233 if memberMask(f, stat_a) && memberMask(f, stat_p)
234 switchf = f;
235 break
236 end
237 end
238 end
239 if switchf > 0
240 newh = h;
241 newh(isf_a) = new_state_a(ia);
242 emit(a, s, newh, bufs, rate_a(ia) * sync{a}.passive{1}.prob, ...
243 [switchf, class_p, node_p, iswaitq(switchf, class_p)]);
244 continue
245 end
246 if blockedf > 0
247 if numel(bufs{blockedf}) >= Lmax(blockedf)
248 continue % FIFO truncation boundary (open-class cutoff)
249 end
250 newh = h;
251 newh(isf_a) = new_state_a(ia);
252 newbufs = bufs;
253 newbufs{blockedf}(end+1) = (node_p-1)*K + class_p;
254 emit(a, s, newh, newbufs, rate_a(ia) * sync{a}.passive{1}.prob);
255 else
256 if node_p == node_a % self-loop
257 [new_state_p, ~, outprob_p] = State.afterEventHashed(sn, node_p, new_state_a(ia), event_p, class_p);
258 else
259 [new_state_p, ~, outprob_p] = State.afterEventHashed(sn, node_p, h(isf_p), event_p, class_p);
260 end
261 if isempty(new_state_p) || isequal(new_state_p, -1)
262 continue
263 end
264 for ip = 1:size(new_state_p,1)
265 if new_state_p(ip) == -1
266 continue
267 end
268 prob_sync_p = sync{a}.passive{1}.prob * outprob_p(ip);
269 if prob_sync_p <= 0
270 continue
271 end
272 if node_p < local && ~csmask(class_a, class_p) && rate_a(ia) * prob_sync_p > 0 && (sn.nodetype(node_p) ~= NodeType.Source)
273 line_error(mfilename, sprintf('Error: routing at node %d (%s) violates the class switching mask (class %s -> class %s).', node_a, sn.nodenames{node_a}, sn.classnames{class_a}, sn.classnames{class_p}));
274 end
275 newh = h;
276 newh(isf_a) = new_state_a(ia);
277 newh(isf_p) = new_state_p(ip);
278 emit(a, s, newh, bufs, rate_a(ia) * prob_sync_p);
279 end
280 end
281 end
282 end
283 end
284end
285
286SSH = SSH(1:nrows,:);
287
288%% assemble outputs
289Dfilt = cell(1,A);
290for a = 1:A
291 sel = (ta(1:ntrip) == a);
292 Dfilt{a} = sparse(ti(sel), tj(sel), tv(sel), nrows, nrows);
293end
294stateSpaceHashed = SSH;
295% full state matrix: concatenated per-node states plus the token buffers
296cols = 0;
297for isf = 1:nstateful
298 cols = cols + size(sn.space{isf},2);
299end
300stateSpace = zeros(nrows, cols + sum(Lmax));
301stateSpaceAggr = zeros(nrows, sn.nstations * K);
302for s = 1:nrows
303 pos = 0;
304 for ind = 1:sn.nnodes
305 if sn.isstateful(ind)
306 isf = sn.nodeToStateful(ind);
307 srow = sn.space{isf}(SSH(s,isf),:);
308 stateSpace(s, pos+1:pos+length(srow)) = srow;
309 pos = pos + size(sn.space{isf},2);
310 if sn.isstation(ind)
311 ist = sn.nodeToStation(ind);
312 [~, nir] = State.toMarginal(sn, ind, srow);
313 stateSpaceAggr(s, ((ist-1)*K+1):ist*K) = nir;
314 end
315 end
316 end
317 stateSpace(s, cols+1:end) = SSH(s, nstateful+1:end);
318end
319
320 function tf = violates(f, x)
321 % TF=VIOLATES(F,X) true if per-class population vector x breaks any
322 % admission constraint of region f
323 tf = any(x > ccap(f,:)) || sum(x) > gcap(f) || (x * szrow(f,:)') > memcap(f);
324 if ~tf && ~isempty(linA{f})
325 tf = any(linA{f} * x(:) > linb{f}(:));
326 end
327 end
328
329 function x = regionAggr(hvec, f)
330 % X=REGIONAGGR(HVEC,F) per-class population of region f under hashed
331 % station states hvec
332 x = zeros(1, K);
333 for ist_ = find(memberMask(f,:))
334 ind_ = sn.stationToNode(ist_);
335 isf_ = sn.nodeToStateful(ind_);
336 [~, nir_] = State.toMarginalAggr(sn, ind_, sn.space{isf_}(hvec(isf_),:));
337 x = x + nir_(:)';
338 end
339 end
340
341 function emit(a, src, newh, newbufs, w, pend)
342 % EMIT(A,SRC,NEWH,NEWBUFS,W,PEND) applies the FIFO release cascade to
343 % the tentative augmented state and records the transitions of action
344 % a. PEND = [f cls destNode] is a pending gated re-entry (a class-
345 % switching hop between members of region f): once the cascade
346 % settles, the job of class cls is admitted at destNode if region f
347 % has capacity, else parked at the tail of the FIFO.
348 if w <= 0
349 return
350 end
351 if nargin < 6
352 pend = [];
353 end
354 % work items: {h, bufs, prob, pend}
355 work = {{newh, newbufs, 1.0, pend}};
356 while ~isempty(work)
357 it = work{1}; work(1) = [];
358 hh = it{1}; bb = it{2}; pw = it{3}; pd = it{4};
359 progressed = false;
360 for f_ = 1:F
361 if isempty(bb{f_})
362 continue
363 end
364 x_ = regionAggr(hh, f_);
365 tok = bb{f_}(1);
366 dest = floor((tok-1)/K) + 1;
367 r_ = mod(tok-1, K) + 1;
368 xn_ = x_;
369 xn_(r_) = xn_(r_) + 1;
370 if violates(f_, xn_)
371 continue % head-of-line: this region's FIFO stays blocked
372 end
373 isf_d = sn.nodeToStateful(dest);
374 [hd, ~, opd] = State.afterEventHashed(sn, dest, hh(isf_d), EventType.ARV, r_);
375 if isempty(hd) || isequal(hd, -1)
376 continue
377 end
378 for id = 1:length(hd)
379 if hd(id) == -1 || opd(id) <= 0
380 continue
381 end
382 hh2 = hh;
383 hh2(isf_d) = hd(id);
384 bb2 = bb;
385 bb2{f_}(1) = [];
386 work{end+1} = {hh2, bb2, pw * opd(id), pd}; %#ok<AGROW>
387 end
388 progressed = true;
389 break
390 end
391 if ~progressed && ~isempty(pd)
392 % cascade settled: resolve the pending gated re-entry
393 f_ = pd(1); cls_ = pd(2); dest_ = pd(3);
394 x_ = regionAggr(hh, f_);
395 xn_ = x_;
396 xn_(cls_) = xn_(cls_) + 1;
397 if violates(f_, xn_)
398 if numel(pd) >= 4 && ~pd(4)
399 % DROP rule: the switching job is destroyed
400 work{end+1} = {hh, bb, pw, []}; %#ok<AGROW>
401 else
402 % no capacity: park at the tail of the region FIFO
403 bb2 = bb;
404 bb2{f_}(end+1) = (dest_-1)*K + cls_;
405 work{end+1} = {hh, bb2, pw, []}; %#ok<AGROW>
406 end
407 else
408 isf_d = sn.nodeToStateful(dest_);
409 [hd, ~, opd] = State.afterEventHashed(sn, dest_, hh(isf_d), EventType.ARV, cls_);
410 admitted = false;
411 if ~isempty(hd) && ~isequal(hd, -1)
412 for id = 1:length(hd)
413 if hd(id) == -1 || opd(id) <= 0
414 continue
415 end
416 hh2 = hh;
417 hh2(isf_d) = hd(id);
418 work{end+1} = {hh2, bb, pw * opd(id), []}; %#ok<AGROW>
419 admitted = true;
420 end
421 end
422 if ~admitted
423 % destination local state missing (e.g. station cap):
424 % park in the FIFO instead
425 bb2 = bb;
426 bb2{f_}(end+1) = (dest_-1)*K + cls_;
427 work{end+1} = {hh, bb2, pw, []}; %#ok<AGROW>
428 end
429 end
430 continue
431 end
432 if ~progressed
433 % settled: register the augmented state and the transition
434 rr = zeros(1, width);
435 rr(1:nstateful) = hh;
436 for f_ = 1:F
437 rr(bufoff(f_)+1:bufoff(f_)+numel(bb{f_})) = bb{f_};
438 end
439 kk = rowkey(rr);
440 if keymap.isKey(kk)
441 dst = keymap(kk);
442 else
443 nrows = nrows + 1;
444 if nrows > size(SSH,1)
445 SSH = [SSH; zeros(size(SSH,1), width)]; %#ok<AGROW>
446 end
447 SSH(nrows,:) = rr;
448 keymap(kk) = nrows;
449 dst = nrows;
450 frontier(end+1) = nrows; %#ok<AGROW>
451 end
452 ntrip = ntrip + 1;
453 if ntrip > numel(ta)
454 ta = [ta; zeros(numel(ta),1)]; %#ok<AGROW>
455 ti = [ti; zeros(numel(ti),1)]; %#ok<AGROW>
456 tj = [tj; zeros(numel(tj),1)]; %#ok<AGROW>
457 tv = [tv; zeros(numel(tv),1)]; %#ok<AGROW>
458 end
459 ta(ntrip) = a; ti(ntrip) = src; tj(ntrip) = dst; tv(ntrip) = w * pw;
460 end
461 end
462 end
463
464end
465
466function k = rowkey(v)
467% K=ROWKEY(V) character key for an augmented state row
468k = sprintf('%d,', v);
469end
Definition Station.m:245