LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
link.m
1function self = link(self, P)
2% SELF = LINK(P)
3
4% Copyright (c) 2012-2026, Imperial College London
5% All rights reserved.
6
7if ~isempty(self.connections)
8 line_error(mfilename,'The Network.link method cannot be used after calling the addLink() method. Use Node.setProbRouting instead to configure routing probabilities.');
9end
10
11if isa(P,'RoutingMatrix')
12 P = P.getCell();
13end
14sanitize(self);
15
16isReset = false;
17if ~isempty(self.sn)
18 isReset = true;
19 self.resetNetwork; % remove artificial class switch nodes
20end
21K = self.getNumberOfClasses;
22I = self.getNumberOfNodes;
23
24if ~iscell(P) && K>1
25 line_error(mfilename,'Multiclass model: the linked routing matrix P must be a cell array, e.g., P = model.initRoutingMatrix; P{1} = Pclass1; P{2} = Pclass2.');
26end
27
28isLinearP = true;
29if size(P,1) == size(P,2)
30 for s=2:K
31 for r=1:K
32 if nnz(P{r,s})>0
33 isLinearP = false;
34 break;
35 end
36 end
37 end
38 % in this case it is possible that P is linear but just because the
39 % routing is state-dependent and therefore some zero entries are
40 % actually unspecified
41 %cacheNodes = find(cellfun(@(c) isa(c,'Cache'), self.getStatefulNodes));
42 for ind=1:I
43 switch class(self.nodes{ind})
44 case 'Cache'
45 % note that since a cache needs to distinguish hits and
46 % misses, it needs to do class-switch unless the model is
47 % degenerate
48 isLinearP = false;
49 if self.nodes{ind}.server.hitClass == self.nodes{ind}.server.missClass
50 line_warning(mfilename,'Ambiguous use of hitClass and missClass at cache, it is recommended to use different classes.\n');
51 end
52 end
53 end
54end
55
56% This block is to make sure that P = model.initRoutingMatrix; P{2} writes
57% into P{2,2} rather than being interpreted as P{2,1}.
58if isLinearP
59 Ptmp = P;
60 P = cell(K,K);
61 for r=1:K
62 if iscell(Ptmp)
63 P{r,r} = Ptmp{r};
64 else
65 P{r,r} = Ptmp;
66 end
67 for s=1:K
68 if s~=r
69 P{r,s} = 0*Ptmp{r};
70 end
71 end
72 end
73end
74
75% assign routing for self-looping jobs
76for r=1:K
77 if isa(self.classes{r},'SelfLoopingClass')
78 for s=1:K
79 P{r,s} = 0 * P{r,s};
80 end
81 P{r,r}(self.classes{r}.refstat, self.classes{r}.refstat) = 1.0;
82 end
83end
84
85% link virtual sinks automatically to sink
86ispool = cellisa(self.nodes,'Sink');
87if sum(ispool) > 1
88 line_error(mfilename,'The model can have at most one sink node.');
89end
90
91if sum(cellisa(self.nodes,'Source')) > 1
92 line_error(mfilename,'The model can have at most one source node.');
93end
94ispool_nnz = find(ispool)';
95
96
97if ~iscell(P)
98 if K>1
99 newP = cell(1,K);
100 for r=1:K
101 newP{r} = P;
102 end
103 P = newP;
104 else %R==1
105 % single class
106 for ind=ispool_nnz
107 P((ind-1)*K+1:ind*K,:)=0;
108 end
109 Pmat = P;
110 P = cell(K,K);
111 for r=1:K
112 for s=1:K
113 P{r,s} = zeros(I);
114 for ind=1:I
115 for jnd=1:I
116 P{r,s}(ind,jnd) = Pmat((ind-1)*K+r,(jnd-1)*K+s);
117 end
118 end
119 end
120 end
121 end
122end
123
124if numel(P) == K
125 % 1 matrix per class
126 for r=1:K
127 for ind=ispool_nnz
128 P{r}((ind-1)*K+1:ind*K,:)=0;
129 end
130 end
131 Pmat = P;
132 P = cell(K,K);
133 for r=1:K
134 P{r,r} = Pmat{r};
135 for s=setdiff(1:K,r)
136 P{r,s} = zeros(I);
137 end
138 end
139end
140
141% Default per-item retrieval routing inherited from the read class. A user may
142% draw the retrieval topology once as ordinary P{readClass,readClass} edges over
143% the retrieval-system queues (and cache<->queue); each item's auto-generated
144% retrieval class then inherits that routing on a miss by default. Explicit
145% per-item setItem* entries (injected in the loop below) override these defaults.
146% The read class itself switches to a retrieval class at the cache and never
147% traverses the queues, so its template edges over the queue set are consumed
148% from P afterwards to leave the read class's own flow unchanged.
149for ind=1:I
150 if isa(self.nodes{ind}, 'Cache') && isprop(self.nodes{ind}, 'retrievalSystemQueueIndices') ...
151 && ~isempty(keys(self.nodes{ind}.retrievalSystemQueueIndices))
152 cacheNode = self.nodes{ind};
153 c = cacheNode.index;
154 nItems = cacheNode.items.nitems;
155 rcMap = cacheNode.retrievalSystemQueueIndices;
156 ks = keys(rcMap);
157 for kk=1:numel(ks)
158 r = double(ks{kk}) + 1; % read class index (1-based; key is index-1)
159 Q = rcMap(ks{kk}); % retrieval-system queue node indices
160 nQ = numel(Q);
161 if isempty(P{r,r})
162 continue
163 end
164 Pr = P{r,r};
165 for it=1:nItems
166 rClass = cacheNode.server.retrievalClasses(it, r);
167 if rClass <= 0
168 continue
169 end
170 if isempty(P{rClass,rClass})
171 P{rClass,rClass} = zeros(I);
172 end
173 for q=1:nQ
174 if Pr(c, Q(q)) > 0 % cache -> queue (entry)
175 P{rClass,rClass}(c, Q(q)) = Pr(c, Q(q));
176 end
177 if Pr(Q(q), c) > 0 % queue -> cache (exit)
178 P{rClass,rClass}(Q(q), c) = Pr(Q(q), c);
179 end
180 for dq=1:nQ % queue -> queue
181 if Pr(Q(q), Q(dq)) > 0
182 P{rClass,rClass}(Q(q), Q(dq)) = Pr(Q(q), Q(dq));
183 end
184 end
185 end
186 end
187 % consume the read class's template edges over the queue set
188 for q=1:nQ
189 P{r,r}(c, Q(q)) = 0;
190 P{r,r}(Q(q), c) = 0;
191 for dq=1:nQ
192 P{r,r}(Q(q), Q(dq)) = 0;
193 end
194 end
195 end
196 end
197end
198
199% Inject deferred retrieval-system routing entries registered by
200% Cache.setRetrievalSystem. The auto-generated retrieval-pending / -complete
201% classes are not part of the user-supplied P, so their routing edges
202% (cache->queue, queue->queue, queue->cache with the pending->complete class
203% switch) are recorded on the Cache node and merged into P here, before the
204% routing matrix is processed. link() then auto-creates the artificial
205% class-switch node for the pending->complete edge like any other P-encoded
206% class switch.
207for ind=1:I
208 if isa(self.nodes{ind}, 'Cache') && isprop(self.nodes{ind}, 'retrievalRoutingEntries') ...
209 && ~isempty(self.nodes{ind}.retrievalRoutingEntries)
210 rre = self.nodes{ind}.retrievalRoutingEntries;
211 for e=1:numel(rre)
212 ent = rre{e}; % [fromCls, toCls, srcNode, dstNode, prob]
213 if isempty(P{ent(1),ent(2)})
214 P{ent(1),ent(2)} = zeros(I);
215 end
216 P{ent(1),ent(2)}(ent(3),ent(4)) = ent(5);
217 end
218 end
219end
220
221isemptyP = false(K,K);
222for r=1:K
223 for s=1:K
224 if isempty(P{r,s})
225 isemptyP(r,s)= true;
226 P{r,s} = zeros(I);
227 else
228 for ind=ispool_nnz
229 P{r,s}(ind,:)=0;
230 end
231 end
232 end
233end
234
235csnodematrix = cell(I,I);
236for ind=1:I
237 for jnd=1:I
238 csnodematrix{ind,jnd} = zeros(K,K);
239 end
240end
241
242for r=1:K
243 for s=1:K
244 if ~isemptyP(r,s)
245 [If,Jf] = find(P{r,s});
246 for k=1:size(If,1)
247 csnodematrix{If(k),Jf(k)}(r,s) = P{r,s}(If(k),Jf(k));
248 end
249 end
250 end
251end
252
253
254% for r=1:R
255% Psum=cellsum({P{r,:}})*ones(M,1);
256% if min(Psum)<1-GlobalConstants.CoarseTol
257% line_error(mfilename,'Invalid routing probabilities (Node %d departures, switching from class %d).',minpos(Psum),r);
258% end
259% if max(Psum)>1+GlobalConstants.CoarseTol
260% line_error(mfilename,sprintf('Invalid routing probabilities (Node %d departures, switching from class %d).',maxpos(Psum),r));
261% end
262% end
263
264self.sn.rtorig = P;
265
266% As we will now create a CS for each link i->j,
267% we now condition on the job going from node i to j
268for ind=1:I
269 for jnd=1:I
270 for r=1:K
271 S = sum(csnodematrix{ind,jnd}(r,:));
272 if S>0
273 csnodematrix{ind,jnd}(r,:)=csnodematrix{ind,jnd}(r,:)/S;
274 else
275 csnodematrix{ind,jnd}(r,r)=1.0;
276 end
277 end
278 end
279end
280
281csid = zeros(I);
282% eye(K) is because any job that travels to an autoAdded class
283% switch stays in the same class prior to reaching the ClassSwitch
284% and anyway the diagonal of csMatrix is irrelevant for the chains
285csMatrix = eye(K);
286nodeNames = self.getNodeNames;
287for ind=1:I
288 for jnd=1:I
289 csMatrix = csMatrix + csnodematrix{ind,jnd};
290 if ~isdiag(csnodematrix{ind,jnd})
291 self.nodes{end+1} = ClassSwitch(self, sprintf('CS_%s_to_%s',nodeNames{ind},nodeNames{jnd}),csnodematrix{ind,jnd});
292 self.nodes{end}.autoAdded = true;
293 csid(ind,jnd) = length(self.nodes);
294 end
295 end
296end
297
298for ind=1:I
299 % this is to ensure that also stateful cs like caches
300 % are accounted
301 if isa(self.nodes{ind},'Cache')
302 for r=find(self.nodes{ind}.server.hitClass)
303 csMatrix(r,self.nodes{ind}.server.hitClass(r)) = 1.0;
304 end
305 for r=find(self.nodes{ind}.server.missClass)
306 csMatrix(r,self.nodes{ind}.server.missClass(r)) = 1.0;
307 end
308 elseif isa(self.nodes{ind},'ClassSwitch')
309 if isempty(self.nodes{ind}.server.csMatrix )
310 line_error(mfilename,'Uninitialized ClassSwitch node, use the setClassSwitchingMatrix method.');
311 end
312 csMatrix = csMatrix | self.nodes{ind}.server.csMatrix > 0.0;
313 end
314end
315
316self.csMatrix = csMatrix~=0;
317
318Ip = length(self.nodes); % number of nodes after addition of cs nodes
319
320% resize matrices
321for r=1:K
322 for s=1:K
323 P{r,s}((I+1):Ip,(I+1):Ip)=0;
324 end
325end
326
327for ind=1:I
328 for jnd=1:I
329 if csid(ind,jnd)>0
330 % re-route
331 for r=1:K
332 for s=1:K
333 if P{r,s}(ind,jnd)>0
334 P{r,r}(ind,csid(ind,jnd)) = P{r,r}(ind,csid(ind,jnd)) + P{r,s}(ind,jnd);
335 P{r,s}(ind,jnd) = 0;
336 P{s,s}(csid(ind,jnd),jnd) = 1;
337 end
338 end
339 end
340 end
341 end
342end
343
344connected = zeros(Ip);
345nodes = self.nodes;
346% Clear non-station nodes' outputStrategy before setting new routing.
347% This prevents accumulation from previous link() calls,
348% since setProbRouting appends entries and resetNetwork only
349% clears station nodes (e.g., Queue, Delay), not non-station
350% stateful nodes (e.g., Router).
351%
352% Use the "PreservingRouted" variant when available so that classes already
353% configured upstream (e.g. by Cache.setRetrievalSystem on its auto-generated
354% ClassSwitch and Queues for retrieval-pending classes that don't appear in
355% P) survive the clearing step. Falls back to the legacy clearing for
356% Dispatcher subclasses (Forker/Firing/Linkage) that don't define the variant.
357for ind=1:Ip
358 if ~isa(nodes{ind}, 'Station') && ismethod(nodes{ind}.output, 'initDispatcherJobClasses')
359 if ismethod(nodes{ind}.output, 'initDispatcherJobClassesPreservingRouted')
360 nodes{ind}.output.initDispatcherJobClassesPreservingRouted(self.classes);
361 else
362 nodes{ind}.output.initDispatcherJobClasses(self.classes);
363 end
364 % Sync sn.routing with the post-clear outputStrategy so getRoutingMatrix
365 % won't try PROB routing for classes whose entry was cleared, while
366 % preserved entries continue to advertise their routing strategy.
367 if ~isempty(self.sn) && isfield(self.sn, 'routing') && ind <= size(self.sn.routing, 1)
368 for k=1:K
369 if k <= numel(nodes{ind}.output.outputStrategy) && ~isempty(nodes{ind}.output.outputStrategy{k})
370 entry = nodes{ind}.output.outputStrategy{k};
371 self.sn.routing(ind,k) = RoutingStrategy.fromText(entry{2});
372 else
373 self.sn.routing(ind,k) = RoutingStrategy.DISABLED;
374 end
375 end
376 end
377 end
378end
379for r=1:K
380 [If,Jf,S] = find(P{r,r});
381 for k=1:length(If)
382 if connected(If(k),Jf(k)) == 0
383 self.addLink(nodes{If(k)}, nodes{Jf(k)});
384 connected(If(k),Jf(k)) = 1;
385 end
386 nodes{If(k)}.setProbRouting(self.classes{r}, nodes{Jf(k)}, S(k));
387 end
388end
389self.nodes = nodes;
390
391% Refresh sn.routing from outputStrategy for non-station nodes.
392% The initDispatcherJobClasses call above set sn.routing to DISABLED,
393% but setProbRouting has since repopulated outputStrategy for routed classes.
394if ~isempty(self.sn) && isfield(self.sn, 'routing')
395 for ind=1:Ip
396 if ~isa(nodes{ind}, 'Station') && ind <= size(self.sn.routing, 1)
397 for k=1:K
398 if isempty(nodes{ind}.output.outputStrategy{k})
399 self.sn.routing(ind,k) = RoutingStrategy.DISABLED;
400 else
401 self.sn.routing(ind,k) = RoutingStrategy.fromText(nodes{ind}.output.outputStrategy{k}{2});
402 end
403 end
404 end
405 end
406end
407
408% Validate the routing probabilities.
409%
410% Pcheck normalizes the two accepted shapes (a plain matrix for a
411% single-class model, a K x K class-pair cell otherwise) so both are checked
412% the same way.
413if iscell(P)
414 Pcheck = P;
415else
416 Pcheck = {P};
417end
418
419% (1) Nonnegativity. This is an unconditional invariant, unlike the row-sum
420% LOWER bound left commented out below: a class that never reaches a node has
421% entries exactly zero there, never negative, so this check has no false
422% positives. It must be done separately from the row-sum test because a
423% negative entry can only LOWER a row sum and therefore passes that test
424% silently; the traffic equations then solve over a matrix that is not a
425% stochastic kernel and every mean-value solver reports the resulting visit
426% ratios without complaint.
427if self.enableChecks
428 for r=1:size(Pcheck,1)
429 for s=1:size(Pcheck,2)
430 if ~isempty(Pcheck{r,s})
431 [Ineg,Jneg] = find(Pcheck{r,s} < -GlobalConstants.FineTol);
432 if ~isempty(Ineg)
433 if size(Pcheck,1) > 1 || size(Pcheck,2) > 1
434 line_error(mfilename,sprintf('Negative routing probability %g from node %s to node %s (class %s to class %s). Routing probabilities must be nonnegative.', full(Pcheck{r,s}(Ineg(1),Jneg(1))), self.nodes{Ineg(1)}.name, self.nodes{Jneg(1)}.name, self.classes{r}.name, self.classes{s}.name));
435 else
436 line_error(mfilename,sprintf('Negative routing probability %g from node %s to node %s. Routing probabilities must be nonnegative.', full(Pcheck{r,s}(Ineg(1),Jneg(1))), self.nodes{Ineg(1)}.name, self.nodes{Jneg(1)}.name));
437 end
438 end
439 end
440 end
441 end
442end
443
444% (2) The total routing probability leaving a node in a given class may not
445% exceed 1. Summing over the destination j AND over the arrival class s is
446% what makes this correct under class switching, where a job leaving node i
447% in class r may arrive as any class s.
448%
449% This replaces a cellsum(P) formulation that did not compute a row sum at
450% all: cellsum indexes its argument linearly over length(C), so on a K x K
451% cell it walked only the FIRST COLUMN, P{1,1}..P{K,1}, and compared
452% individual entries (never their sum over j) against 1.0. Its find() then
453% produced linear indices into an I x I matrix, which indexed self.nodes out
454% of range as soon as a violation occurred anywhere but the first column.
455% FORK nodes are exempt, as before: a fork legitimately emits on several
456% output links at once, so its outgoing total exceeds 1 by design.
457if self.enableChecks
458 for ind=1:I
459 % Not every node carries schedStrategy (Join does not, for one), so
460 % the FORK exemption must be guarded. The previous formulation only
461 % ever reached this property for nodes it had already flagged, which
462 % is why the gap went unnoticed.
463 % Nodes whose outgoing entries are structural indicators rather than
464 % a probability distribution are exempt. An SPN Place/Transition
465 % carries incidence arcs: spn_inhibiting sets P2->T2 and P2->T3 both
466 % to 1.0, and T1->P2, T1->P3 likewise. A Router is exempt because the
467 % established idiom declares connectivity with 1.0 entries and only
468 % then calls setRouting(class, RROBIN), AFTER link() has run, so the
469 % routing strategy is not yet knowable here.
470 if isa(self.nodes{ind},'Place') || isa(self.nodes{ind},'Transition') || isa(self.nodes{ind},'Router')
471 continue
472 end
473 % A fork legitimately emits on several output links at once, so its
474 % outgoing total exceeds 1 by design. Not every node carries
475 % schedStrategy (Join does not), so this must be guarded.
476 if isprop(self.nodes{ind},'schedStrategy') && ~isempty(self.nodes{ind}.schedStrategy) && SchedStrategy.toId(self.nodes{ind}.schedStrategy) == SchedStrategy.FORK
477 continue
478 end
479 for r=1:size(Pcheck,1)
480 pOut = 0;
481 for s=1:size(Pcheck,2)
482 if ~isempty(Pcheck{r,s})
483 pOut = pOut + sum(Pcheck{r,s}(ind,:));
484 end
485 end
486 if pOut > 1.0 + GlobalConstants.FineTol
487 if size(Pcheck,1) > 1
488 line_error(mfilename,sprintf('The total routing probability for jobs leaving node %s in class %s is %g, which is greater than 1.0.',self.nodes{ind}.name,self.classes{r}.name,full(pOut)));
489 else
490 line_error(mfilename,sprintf('The total routing probability for jobs leaving node %s is %g, which is greater than 1.0.',self.nodes{ind}.name,full(pOut)));
491 end
492 end
493 end
494 % elseif pOut < 1.0 - GlobalConstants.FineTol % we cannot check this case as class r may not reach station i, in which case its outgoing routing prob is zero
495 % if self.nodes{i}.schedStrategy ~= SchedStrategy.EXT % if not a sink
496 % line_error(mfilename,'The total routing probability for jobs leaving node %s in class %s is less than 1.0.',self.nodes{i}.name,self.classes{r}.name);
497 % end
498 end
499end
500
501for ind=1:I
502 if isa(self.nodes{ind},'Place')
503 self.nodes{ind}.init;
504 end
505end
506
507if isReset && ~isempty(self.sn) && isfield(self.sn,'rates')
508 self.refreshChains; % without this exception with linkAndLog
509end
510
511%% Check for reducible routing (absorbing states)
512if self.enableChecks
513 % Pass routing matrix directly to avoid getStruct() call during link()
514 [isErg, ergInfo] = self.isRoutingErgodic(self.sn.rtorig);
515 if ~isErg && ~isempty(ergInfo.absorbingStations)
516 % Build warning message
517 absNames = strjoin(ergInfo.absorbingStations, ', ');
518 line_warning(mfilename, 'Reducible network topology detected, results may be unreliable.\n');
519 end
520end
521
522%% Check that order-independent (OI) stations have a permutation-invariant rate
523if self.enableChecks
524 Nvec = zeros(1, self.getNumberOfClasses);
525 for r = 1:numel(Nvec)
526 if isa(self.classes{r}, 'ClosedClass')
527 Nvec(r) = self.classes{r}.population;
528 else
529 Nvec(r) = Inf; % open classes have infinite population
530 end
531 end
532 for ind = 1:I
533 nd = self.nodes{ind};
534 if isa(nd, 'Queue') && SchedStrategy.toId(nd.schedStrategy) == SchedStrategy.OI ...
535 && ~isempty(nd.svcRateFun)
536 [ok, badc, partial] = nd.checkPermInvariance(Nvec, nd.cap);
537 if ~ok
538 line_error(mfilename, 'Order-independent (OI) station ''%s'' has a service rate function that is not permutation-invariant: mu(c) differs for a reordering of the microstate %s. Use SchedStrategy.PAS for order-dependent service, or disable this check with model.setChecks(false).', nd.getName(), mat2str(badc));
539 elseif partial
540 line_warning(mfilename, 'Order-independent (OI) station ''%s'': the permutation-invariance check was only partial because the reachable population is large; a subset of microstates was verified. To skip this check, call model.setChecks(false) before link().\n', nd.getName());
541 end
542 end
543 end
544end
545
546%% DEBUGGING
547if false
548 % create java version of the network
549 if isempty(self.obj)
550 jnetwork = JLINE.from_line_network(self);
551 else
552 jnetwork = self.obj;
553 end
554 % compare sn data structures
555 jsn = JLINE.from_jline_struct(jnetwork);
556 fprintf(1,'* Comparison with Java NetworkStruct: ');
557 bool = testJavaStruct(self.getName(),self.getStruct(),jsn);
558end
559
560end
Definition fjtag.m:157
Definition Station.m:245