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).
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.
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).
23% Copyright (c) 2012-2026, Imperial College London
26nstateful = sn.nstateful;
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.');
37if isfield(sn,'fjsync') && ~isempty(sn.fjsync)
38 line_error(mfilename,'WAITQ finite capacity regions are not supported together with fork-join in SolverCTMC.');
40if any(sn.isstatedep(:,3))
41 line_error(mfilename,'WAITQ finite capacity regions are not supported together with state-dependent routing in SolverCTMC.');
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
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
58 memvec = -ones(sn.nstations,1);
59 if isfield(sn,'regionmaxmem') && numel(sn.regionmaxmem) >= f && ~isempty(sn.regionmaxmem{f})
60 memvec = sn.regionmaxmem{f}(:);
62 members = find(any(Rmat ~= -1, 2) | memvec ~= -1)
';
63 memberMask(f, members) = true;
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);
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
75 if isfield(sn,'regionsz
') && ~isempty(sn.regionsz)
76 szrow(f,:) = sn.regionsz(f,:);
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};
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);
92 cutoffMat = zeros(sn.nstations, K);
97 c_ = find(sn.chains(:,r), 1); % chain of class r
98 chainpop = sum(sn.njobs(sn.chains(c_,:)));
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;
104 tokbound(r) = max(cutoffMat(:,r));
110 Lmax(f) = sum(tokbound(iswaitq(f,:)));
112bufoff = nstateful + [0; cumsum(Lmax(1:end-1))]; % column offset of buf_f
113width = nstateful + sum(Lmax);
115%% initial augmented state (buffers empty)
116h0 = zeros(1, nstateful);
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, []));
126 h0(isf) = State.getHash(sn, ind, sn.state{isf}(1,:));
129 line_error(mfilename, sprintf('Initial state of node %s not found in its local state space.
', sn.nodenames{ind}));
133row0 = zeros(1, width);
134row0(1:nstateful) = h0;
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.
');
141%% breadth-first construction of the reachable augmented space
143SSH = zeros(capRows, width);
146keymap = containers.Map(rowkey(row0), 1);
148% transition triplets per action
150ta = zeros(capTrip,1); ti = zeros(capTrip,1); tj = zeros(capTrip,1); tv = zeros(capTrip,1);
153while ~isempty(frontier)
154 s = frontier(1); frontier(1) = [];
156 h = row(1:nstateful);
159 bf = row(bufoff(f)+1:bufoff(f)+Lmax(f));
160 bufs{f} = bf(bf > 0);
162 % current per-region aggregate populations
165 xf(f,:) = regionAggr(h, f);
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)
176 for ia = 1:length(new_state_a)
177 if isnan(rate_a(ia)) || rate_a(ia) <= 0 || new_state_a(ia) == -1
180 node_p = sync{a}.passive{1}.node;
183 newh(isf_a) = new_state_a(ia);
184 emit(a, s, newh, bufs, rate_a(ia));
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
192 if node_a <= sn.nnodes && sn.isstation(node_a)
193 stat_a = sn.nodeToStation(node_a);
196 if sn.isstation(node_p)
197 stat_p = sn.nodeToStation(node_p);
201 if event_p == EventType.ARV && stat_p > 0
203 if memberMask(f, stat_p) && (stat_a <= 0 || ~memberMask(f, stat_a))
205 xn(class_p) = xn(class_p) + 1;
207 if ~iswaitq(f, class_p)
208 droppedf = f; % DROP rule: the job is destroyed
218 % DROP rule (JMT semantics): the refused job is destroyed;
219 % only the active (departing) part of the transition applies
221 newh(isf_a) = new_state_a(ia);
222 emit(a, s, newh, bufs, rate_a(ia) * sync{a}.passive{1}.prob);
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
230 if blockedf == 0 && event_p == EventType.ARV && class_p ~= class_a ...
231 && stat_a > 0 && stat_p > 0
233 if memberMask(f, stat_a) && memberMask(f, stat_p)
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)]);
247 if numel(bufs{blockedf}) >= Lmax(blockedf)
248 continue % FIFO truncation boundary (open-class cutoff)
251 newh(isf_a) = new_state_a(ia);
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);
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);
259 [new_state_p, ~, outprob_p] = State.afterEventHashed(sn, node_p, h(isf_p), event_p, class_p);
261 if isempty(new_state_p) || isequal(new_state_p, -1)
264 for ip = 1:size(new_state_p,1)
265 if new_state_p(ip) == -1
268 prob_sync_p = sync{a}.passive{1}.prob * outprob_p(ip);
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}));
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);
291 sel = (ta(1:ntrip) == a);
292 Dfilt{a} = sparse(ti(sel), tj(sel), tv(sel), nrows, nrows);
294stateSpaceHashed = SSH;
295% full state matrix: concatenated per-node states plus the token buffers
298 cols = cols + size(sn.space{isf},2);
300stateSpace = zeros(nrows, cols + sum(Lmax));
301stateSpaceAggr = zeros(nrows, sn.nstations * K);
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);
311 ist = sn.nodeToStation(ind);
312 [~, nir] = State.toMarginal(sn, ind, srow);
313 stateSpaceAggr(s, ((ist-1)*K+1):ist*K) = nir;
317 stateSpace(s, cols+1:end) = SSH(s, nstateful+1:end);
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}(:));
329 function x = regionAggr(hvec, f)
330 % X=REGIONAGGR(HVEC,F) per-
class population of region f under hashed
331 % station states hvec
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_),:));
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.
354 % work items: {h, bufs, prob, pend}
355 work = {{newh, newbufs, 1.0, pend}};
357 it = work{1}; work(1) = [];
358 hh = it{1}; bb = it{2}; pw = it{3}; pd = it{4};
364 x_ = regionAggr(hh, f_);
366 dest = floor((tok-1)/K) + 1;
367 r_ = mod(tok-1, K) + 1;
369 xn_(r_) = xn_(r_) + 1;
371 continue % head-of-line: this region's FIFO stays blocked
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)
378 for
id = 1:length(hd)
379 if hd(
id) == -1 || opd(
id) <= 0
386 work{end+1} = {hh2, bb2, pw * opd(
id), pd}; %#ok<AGROW>
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_);
396 xn_(cls_) = xn_(cls_) + 1;
398 if numel(pd) >= 4 && ~pd(4)
399 % DROP rule:
the switching job
is destroyed
400 work{end+1} = {hh, bb, pw, []}; %#ok<AGROW>
402 % no capacity: park at
the tail of
the region FIFO
404 bb2{f_}(end+1) = (dest_-1)*K + cls_;
405 work{end+1} = {hh, bb2, pw, []}; %#ok<AGROW>
408 isf_d = sn.nodeToStateful(dest_);
409 [hd, ~, opd] = State.afterEventHashed(sn, dest_, hh(isf_d), EventType.ARV, cls_);
411 if ~isempty(hd) && ~isequal(hd, -1)
412 for
id = 1:length(hd)
413 if hd(
id) == -1 || opd(
id) <= 0
418 work{end+1} = {hh2, bb, pw * opd(
id), []}; %#ok<AGROW>
423 % destination local state missing (e.g. station cap):
424 % park in
the FIFO instead
426 bb2{f_}(end+1) = (dest_-1)*K + cls_;
427 work{end+1} = {hh, bb2, pw, []}; %#ok<AGROW>
433 % settled:
register the augmented state and
the transition
434 rr = zeros(1, width);
435 rr(1:nstateful) = hh;
437 rr(bufoff(f_)+1:bufoff(f_)+numel(bb{f_})) = bb{f_};
444 if nrows > size(SSH,1)
445 SSH = [SSH; zeros(size(SSH,1), width)]; %
#ok<AGROW>
450 frontier(end+1) = nrows; %#ok<AGROW>
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>
459 ta(ntrip) = a; ti(ntrip) = src; tj(ntrip) = dst; tv(ntrip) = w * pw;
466function k = rowkey(v)
467% K=ROWKEY(V) character key
for an augmented state row
468k = sprintf(
'%d,', v);