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