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 % P may look linear only because state-dependent routing leaves some zero entries unspecified
39 %cacheNodes = find(cellfun(@(c) isa(c,'Cache'), self.getStatefulNodes));
40 for ind=1:I
41 switch class(self.nodes{ind})
42 case 'Cache'
43 % A cache needs class-switch to distinguish hits/misses unless the model is degenerate
44 isLinearP = false;
45 if self.nodes{ind}.server.hitClass == self.nodes{ind}.server.missClass
46 line_warning(mfilename,'Ambiguous use of hitClass and missClass at cache, it is recommended to use different classes.\n');
47 end
48 end
49 end
50end
51
52% This block is to make sure that P = model.initRoutingMatrix; P{2} writes
53% into P{2,2} rather than being interpreted as P{2,1}.
54if isLinearP
55 Ptmp = P;
56 P = cell(K,K);
57 for r=1:K
58 if iscell(Ptmp)
59 P{r,r} = Ptmp{r};
60 else
61 P{r,r} = Ptmp;
62 end
63 for s=1:K
64 if s~=r
65 P{r,s} = 0*Ptmp{r};
66 end
67 end
68 end
69end
70
71% assign routing for self-looping jobs
72for r=1:K
73 if isa(self.classes{r},'SelfLoopingClass')
74 for s=1:K
75 P{r,s} = 0 * P{r,s};
76 end
77 P{r,r}(self.classes{r}.refstat, self.classes{r}.refstat) = 1.0;
78 end
79end
80
81% link virtual sinks automatically to sink
82ispool = cellisa(self.nodes,'Sink');
83if sum(ispool) > 1
84 line_error(mfilename,'The model can have at most one sink node.');
85end
86
87if sum(cellisa(self.nodes,'Source')) > 1
88 line_error(mfilename,'The model can have at most one source node.');
89end
90ispool_nnz = find(ispool)';
91
92
93if ~iscell(P)
94 if K>1
95 newP = cell(1,K);
96 for r=1:K
97 newP{r} = P;
98 end
99 P = newP;
100 else %R==1
101 % single class
102 for ind=ispool_nnz
103 P((ind-1)*K+1:ind*K,:)=0;
104 end
105 Pmat = P;
106 P = cell(K,K);
107 for r=1:K
108 for s=1:K
109 P{r,s} = zeros(I);
110 for ind=1:I
111 for jnd=1:I
112 P{r,s}(ind,jnd) = Pmat((ind-1)*K+r,(jnd-1)*K+s);
113 end
114 end
115 end
116 end
117 end
118end
119
120if numel(P) == K
121 % 1 matrix per class
122 for r=1:K
123 for ind=ispool_nnz
124 P{r}((ind-1)*K+1:ind*K,:)=0;
125 end
126 end
127 Pmat = P;
128 P = cell(K,K);
129 for r=1:K
130 P{r,r} = Pmat{r};
131 for s=setdiff(1:K,r)
132 P{r,s} = zeros(I);
133 end
134 end
135end
136
137% Default per-item retrieval routing inherited from the read class -- see _kb/09-ldes-and-cache.md
138for ind=1:I
139 if isa(self.nodes{ind}, 'Cache') && isprop(self.nodes{ind}, 'retrievalSystemQueueIndices') ...
140 && ~isempty(keys(self.nodes{ind}.retrievalSystemQueueIndices))
141 cacheNode = self.nodes{ind};
142 c = cacheNode.index;
143 nItems = cacheNode.items.nitems;
144 rcMap = cacheNode.retrievalSystemQueueIndices;
145 ks = keys(rcMap);
146 for kk=1:numel(ks)
147 r = double(ks{kk}) + 1; % read class index (1-based; key is index-1)
148 Q = rcMap(ks{kk}); % retrieval-system queue node indices
149 nQ = numel(Q);
150 if isempty(P{r,r})
151 continue
152 end
153 Pr = P{r,r};
154 for it=1:nItems
155 rClass = cacheNode.server.retrievalClasses(it, r);
156 if rClass <= 0
157 continue
158 end
159 if isempty(P{rClass,rClass})
160 P{rClass,rClass} = zeros(I);
161 end
162 for q=1:nQ
163 if Pr(c, Q(q)) > 0 % cache -> queue (entry)
164 P{rClass,rClass}(c, Q(q)) = Pr(c, Q(q));
165 end
166 if Pr(Q(q), c) > 0 % queue -> cache (exit)
167 P{rClass,rClass}(Q(q), c) = Pr(Q(q), c);
168 end
169 for dq=1:nQ % queue -> queue
170 if Pr(Q(q), Q(dq)) > 0
171 P{rClass,rClass}(Q(q), Q(dq)) = Pr(Q(q), Q(dq));
172 end
173 end
174 end
175 end
176 % consume the read class's template edges over the queue set
177 for q=1:nQ
178 P{r,r}(c, Q(q)) = 0;
179 P{r,r}(Q(q), c) = 0;
180 for dq=1:nQ
181 P{r,r}(Q(q), Q(dq)) = 0;
182 end
183 end
184 end
185 end
186end
187
188% Inject deferred retrieval-system routing entries from Cache.setRetrievalSystem -- see _kb/09-ldes-and-cache.md
189for ind=1:I
190 if isa(self.nodes{ind}, 'Cache') && isprop(self.nodes{ind}, 'retrievalRoutingEntries') ...
191 && ~isempty(self.nodes{ind}.retrievalRoutingEntries)
192 rre = self.nodes{ind}.retrievalRoutingEntries;
193 for e=1:numel(rre)
194 ent = rre{e}; % [fromCls, toCls, srcNode, dstNode, prob]
195 if isempty(P{ent(1),ent(2)})
196 P{ent(1),ent(2)} = zeros(I);
197 end
198 P{ent(1),ent(2)}(ent(3),ent(4)) = ent(5);
199 end
200 end
201end
202
203isemptyP = false(K,K);
204for r=1:K
205 for s=1:K
206 if isempty(P{r,s})
207 isemptyP(r,s)= true;
208 P{r,s} = zeros(I);
209 else
210 for ind=ispool_nnz
211 P{r,s}(ind,:)=0;
212 end
213 end
214 end
215end
216
217csnodematrix = cell(I,I);
218for ind=1:I
219 for jnd=1:I
220 csnodematrix{ind,jnd} = zeros(K,K);
221 end
222end
223
224for r=1:K
225 for s=1:K
226 if ~isemptyP(r,s)
227 [If,Jf] = find(P{r,s});
228 for k=1:size(If,1)
229 csnodematrix{If(k),Jf(k)}(r,s) = P{r,s}(If(k),Jf(k));
230 end
231 end
232 end
233end
234
235
236% for r=1:R
237% Psum=cellsum({P{r,:}})*ones(M,1);
238% if min(Psum)<1-GlobalConstants.CoarseTol
239% line_error(mfilename,'Invalid routing probabilities (Node %d departures, switching from class %d).',minpos(Psum),r);
240% end
241% if max(Psum)>1+GlobalConstants.CoarseTol
242% line_error(mfilename,sprintf('Invalid routing probabilities (Node %d departures, switching from class %d).',maxpos(Psum),r));
243% end
244% end
245
246self.sn.rtorig = P;
247
248% As we will now create a CS for each link i->j,
249% we now condition on the job going from node i to j
250for ind=1:I
251 for jnd=1:I
252 for r=1:K
253 S = sum(csnodematrix{ind,jnd}(r,:));
254 if S>0
255 csnodematrix{ind,jnd}(r,:)=csnodematrix{ind,jnd}(r,:)/S;
256 else
257 csnodematrix{ind,jnd}(r,r)=1.0;
258 end
259 end
260 end
261end
262
263csid = zeros(I);
264% eye(K): a job to an autoAdded class switch stays in the same class beforehand; csMatrix diagonal is irrelevant for chains
265csMatrix = eye(K);
266nodeNames = self.getNodeNames;
267for ind=1:I
268 for jnd=1:I
269 csMatrix = csMatrix + csnodematrix{ind,jnd};
270 if ~isdiag(csnodematrix{ind,jnd})
271 self.nodes{end+1} = ClassSwitch(self, sprintf('CS_%s_to_%s',nodeNames{ind},nodeNames{jnd}),csnodematrix{ind,jnd});
272 self.nodes{end}.autoAdded = true;
273 csid(ind,jnd) = length(self.nodes);
274 end
275 end
276end
277
278for ind=1:I
279 % this is to ensure that also stateful cs like caches
280 % are accounted
281 if isa(self.nodes{ind},'Cache')
282 for r=find(self.nodes{ind}.server.hitClass)
283 csMatrix(r,self.nodes{ind}.server.hitClass(r)) = 1.0;
284 end
285 for r=find(self.nodes{ind}.server.missClass)
286 csMatrix(r,self.nodes{ind}.server.missClass(r)) = 1.0;
287 end
288 elseif isa(self.nodes{ind},'ClassSwitch')
289 if isempty(self.nodes{ind}.server.csMatrix )
290 line_error(mfilename,'Uninitialized ClassSwitch node, use the setClassSwitchingMatrix method.');
291 end
292 csMatrix = csMatrix | self.nodes{ind}.server.csMatrix > 0.0;
293 end
294end
295
296self.csMatrix = csMatrix~=0;
297
298Ip = length(self.nodes); % number of nodes after addition of cs nodes
299
300% resize matrices
301for r=1:K
302 for s=1:K
303 P{r,s}((I+1):Ip,(I+1):Ip)=0;
304 end
305end
306
307for ind=1:I
308 for jnd=1:I
309 if csid(ind,jnd)>0
310 % re-route
311 for r=1:K
312 for s=1:K
313 if P{r,s}(ind,jnd)>0
314 P{r,r}(ind,csid(ind,jnd)) = P{r,r}(ind,csid(ind,jnd)) + P{r,s}(ind,jnd);
315 P{r,s}(ind,jnd) = 0;
316 P{s,s}(csid(ind,jnd),jnd) = 1;
317 end
318 end
319 end
320 end
321 end
322end
323
324connected = zeros(Ip);
325nodes = self.nodes;
326% Clear non-station nodes' outputStrategy before setting new routing (prevents accumulation across link() calls) -- see _kb/04-networkstruct.md
327for ind=1:Ip
328 if ~isa(nodes{ind}, 'Station') && ismethod(nodes{ind}.output, 'initDispatcherJobClasses')
329 if ismethod(nodes{ind}.output, 'initDispatcherJobClassesPreservingRouted')
330 nodes{ind}.output.initDispatcherJobClassesPreservingRouted(self.classes);
331 else
332 nodes{ind}.output.initDispatcherJobClasses(self.classes);
333 end
334 % Sync sn.routing with the post-clear outputStrategy so getRoutingMatrix won't try PROB routing for cleared classes
335 if ~isempty(self.sn) && isfield(self.sn, 'routing') && ind <= size(self.sn.routing, 1)
336 for k=1:K
337 if k <= numel(nodes{ind}.output.outputStrategy) && ~isempty(nodes{ind}.output.outputStrategy{k})
338 entry = nodes{ind}.output.outputStrategy{k};
339 self.sn.routing(ind,k) = RoutingStrategy.fromText(entry{2});
340 else
341 self.sn.routing(ind,k) = RoutingStrategy.DISABLED;
342 end
343 end
344 end
345 end
346end
347for r=1:K
348 [If,Jf,S] = find(P{r,r});
349 for k=1:length(If)
350 if connected(If(k),Jf(k)) == 0
351 self.addLink(nodes{If(k)}, nodes{Jf(k)});
352 connected(If(k),Jf(k)) = 1;
353 end
354 nodes{If(k)}.setProbRouting(self.classes{r}, nodes{Jf(k)}, S(k));
355 end
356end
357self.nodes = nodes;
358
359% Refresh sn.routing from outputStrategy: initDispatcherJobClasses above set it DISABLED, setProbRouting has since repopulated it
360if ~isempty(self.sn) && isfield(self.sn, 'routing')
361 for ind=1:Ip
362 if ~isa(nodes{ind}, 'Station') && ind <= size(self.sn.routing, 1)
363 for k=1:K
364 if isempty(nodes{ind}.output.outputStrategy{k})
365 self.sn.routing(ind,k) = RoutingStrategy.DISABLED;
366 else
367 self.sn.routing(ind,k) = RoutingStrategy.fromText(nodes{ind}.output.outputStrategy{k}{2});
368 end
369 end
370 end
371 end
372end
373
374% Validate the routing probabilities -- see _kb/04-networkstruct.md
375% Pcheck normalizes the two accepted shapes (plain matrix, or K x K class-pair cell) so both are checked the same way.
376if iscell(P)
377 Pcheck = P;
378else
379 Pcheck = {P};
380end
381
382% (1) Nonnegativity, checked separately from the row-sum test below -- see _kb/04-networkstruct.md
383if self.enableChecks
384 for r=1:size(Pcheck,1)
385 for s=1:size(Pcheck,2)
386 if ~isempty(Pcheck{r,s})
387 [Ineg,Jneg] = find(Pcheck{r,s} < -GlobalConstants.FineTol);
388 if ~isempty(Ineg)
389 if size(Pcheck,1) > 1 || size(Pcheck,2) > 1
390 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));
391 else
392 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));
393 end
394 end
395 end
396 end
397 end
398end
399
400% (2) Row-sum <= 1, summed over destination j AND arrival class s (class-switching correctness) -- see _kb/04-networkstruct.md
401if self.enableChecks
402 for ind=1:I
403 % Place/Transition (SPN incidence arcs) and Router (connectivity declared before setRouting) are exempt -- see _kb/04-networkstruct.md
404 if isa(self.nodes{ind},'Place') || isa(self.nodes{ind},'Transition') || isa(self.nodes{ind},'Router')
405 continue
406 end
407 % FORK legitimately emits on several output links at once; guarded since not every node carries schedStrategy (e.g. Join)
408 if isprop(self.nodes{ind},'schedStrategy') && ~isempty(self.nodes{ind}.schedStrategy) && SchedStrategy.toId(self.nodes{ind}.schedStrategy) == SchedStrategy.FORK
409 continue
410 end
411 for r=1:size(Pcheck,1)
412 pOut = 0;
413 for s=1:size(Pcheck,2)
414 if ~isempty(Pcheck{r,s})
415 pOut = pOut + sum(Pcheck{r,s}(ind,:));
416 end
417 end
418 if pOut > 1.0 + GlobalConstants.FineTol
419 if size(Pcheck,1) > 1
420 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)));
421 else
422 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)));
423 end
424 end
425 end
426 % 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
427 % if self.nodes{i}.schedStrategy ~= SchedStrategy.EXT % if not a sink
428 % 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);
429 % end
430 end
431end
432
433for ind=1:I
434 if isa(self.nodes{ind},'Place')
435 self.nodes{ind}.init;
436 end
437end
438
439if isReset && ~isempty(self.sn) && isfield(self.sn,'rates')
440 self.refreshChains; % without this exception with linkAndLog
441end
442
443%% Check for reducible routing (absorbing states)
444if self.enableChecks
445 % Pass routing matrix directly to avoid getStruct() call during link()
446 [isErg, ergInfo] = self.isRoutingErgodic(self.sn.rtorig);
447 if ~isErg && ~isempty(ergInfo.absorbingStations)
448 % Build warning message
449 absNames = strjoin(ergInfo.absorbingStations, ', ');
450 line_warning(mfilename, 'Reducible network topology detected, results may be unreliable.\n');
451 end
452end
453
454%% Check that order-independent (OI) stations have a permutation-invariant rate
455if self.enableChecks
456 Nvec = zeros(1, self.getNumberOfClasses);
457 for r = 1:numel(Nvec)
458 if isa(self.classes{r}, 'ClosedClass')
459 Nvec(r) = self.classes{r}.population;
460 else
461 Nvec(r) = Inf; % open classes have infinite population
462 end
463 end
464 for ind = 1:I
465 nd = self.nodes{ind};
466 if isa(nd, 'Queue') && SchedStrategy.toId(nd.schedStrategy) == SchedStrategy.OI ...
467 && ~isempty(nd.svcRateFun)
468 [ok, badc, partial] = nd.checkPermInvariance(Nvec, nd.cap);
469 if ~ok
470 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));
471 elseif partial
472 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());
473 end
474 end
475 end
476end
477
478%% DEBUGGING
479if false
480 % create java version of the network
481 if isempty(self.obj)
482 jnetwork = JLINE.from_line_network(self);
483 else
484 jnetwork = self.obj;
485 end
486 % compare sn data structures
487 jsn = JLINE.from_jline_struct(jnetwork);
488 fprintf(1,'* Comparison with Java NetworkStruct: ');
489 bool = testJavaStruct(self.getName(),self.getStruct(),jsn);
490end
491
492end
Definition fjtag.m:161