LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
Ensemble.m
1classdef Ensemble < Model
2 % A model defined by a collection of sub-models.
3 %
4 % Copyright (c) 2012-2026, Imperial College London
5 % All rights reserved.
6
7 properties
8 ensemble;
9 end
10
11 methods
12 function self = Ensemble(models)
13 % SELF = ENSEMBLE(MODELS)
14
15 self@Model('Ensemble');
16 self.ensemble = reshape(models,1,numel(models)); % flatten
17 end
18
19 function self = setEnsemble(self,ensemble)
20 % SELF = SETENSEMBLE(SELF,ENSEMBLE)
21
22 self.ensemble = ensemble;
23 end
24
25 function ensemble = getEnsemble(self)
26 % ENSEMBLE = GETENSEMBLE()
27
28 ensemble = self.ensemble;
29 end
30
31 function model = getModel(self, modelIdx)
32 model = self.ensemble{modelIdx};
33 end
34 end
35
36 methods(Access = protected)
37 % Override copyElement method:
38 function clone = copyElement(self)
39 % CLONE = COPYELEMENT()
40
41 % Make a shallow copy of all properties
42 clone = copyElement@Copyable(self);
43 % Make a deep copy of each ensemble object
44 for e=1:length(self.ensemble)
45 clone.ensemble{e} = copy(self.ensemble{e});
46 end
47 end
48 end
49
50 methods(Static)
51 function unionNetwork = merge(ensemble)
52 % MERGE Create a union Network from all Networks in an ensemble
53 %
54 % UNIONNETWORK = MERGE(ENSEMBLE) returns a single Network containing
55 % all nodes and classes from each Network in the ensemble as
56 % disconnected subnetworks. Node and class names are prefixed with
57 % their originating model name to avoid collisions.
58 %
59 % Input:
60 % ensemble - Ensemble object or cell array of Network objects
61 %
62 % Output:
63 % unionNetwork - Network object containing merged subnetworks
64
65 % Handle input - accept Ensemble object or cell array
66 if isa(ensemble, 'Ensemble')
67 models = ensemble.getEnsemble();
68 elseif iscell(ensemble)
69 models = ensemble;
70 else
71 line_error(mfilename, 'Input must be an Ensemble or cell array of Networks');
72 end
73
74 % Edge case: empty ensemble
75 if isempty(models)
76 line_warning(mfilename, 'Empty ensemble provided, returning empty Network');
77 unionNetwork = Network('EmptyMergedNetwork');
78 return;
79 end
80
81 % Create union network with combined name
82 modelNames = cellfun(@(m) m.getName(), models, 'UniformOutput', false);
83 unionName = strjoin(modelNames, '_');
84 if length(unionName) > 50
85 unionName = sprintf('MergedNetwork_%d', length(models));
86 end
87 unionNetwork = Network(unionName);
88
89 % Check if any model has open classes (needs Source/Sink)
90 hasOpenClasses = false;
91 for m = 1:length(models)
92 if models{m}.hasOpenClasses()
93 hasOpenClasses = true;
94 break;
95 end
96 end
97
98 % Create single Source and Sink if needed
99 unionSource = [];
100 unionSink = [];
101 if hasOpenClasses
102 unionSource = Source(unionNetwork, 'MergedSource');
103 unionSink = Sink(unionNetwork, 'MergedSink');
104 end
105
106 % Storage for mapping old nodes/classes to new ones
107 nodeMap = cell(1, length(models)); % containers.Map for each model
108 classMap = cell(1, length(models)); % containers.Map for each model
109 joinPendingList = {}; % Join nodes need Forks to exist first
110
111 % Phase 1: Create nodes for each model
112 for m = 1:length(models)
113 model = models{m};
114 modelName = model.getName();
115 prefix = [modelName, '_'];
116
117 nodes = model.getNodes();
118 nodeMap{m} = containers.Map('KeyType', 'char', 'ValueType', 'any');
119
120 for n = 1:length(nodes)
121 oldNode = nodes{n};
122 newName = [prefix, oldNode.getName()];
123
124 if isa(oldNode, 'Source')
125 % Map to merged source
126 nodeMap{m}(oldNode.getName()) = unionSource;
127 continue;
128 elseif isa(oldNode, 'Sink')
129 % Map to merged sink
130 nodeMap{m}(oldNode.getName()) = unionSink;
131 continue;
132 elseif isa(oldNode, 'Delay')
133 newNode = Delay(unionNetwork, newName);
134 elseif isa(oldNode, 'Queue')
135 newNode = Queue(unionNetwork, newName, oldNode.schedStrategy);
136 if ~isinf(oldNode.numberOfServers) && oldNode.numberOfServers > 1
137 newNode.setNumberOfServers(oldNode.numberOfServers);
138 end
139 if ~isempty(oldNode.lldScaling)
140 newNode.setLoadDependence(oldNode.lldScaling);
141 end
142 if ~isinf(oldNode.cap)
143 newNode.setCapacity(oldNode.cap);
144 end
145 elseif isa(oldNode, 'Router')
146 newNode = Router(unionNetwork, newName);
147 elseif isa(oldNode, 'ClassSwitch')
148 % Skip auto-added ClassSwitch nodes (recreated by link())
149 if isprop(oldNode, 'autoAdded') && oldNode.autoAdded
150 continue;
151 end
152 newNode = ClassSwitch(unionNetwork, newName);
153 elseif isa(oldNode, 'Fork')
154 newNode = Fork(unionNetwork, newName);
155 if ~isempty(oldNode.output) && isprop(oldNode.output, 'tasksPerLink') && ~isempty(oldNode.output.tasksPerLink)
156 newNode.setTasksPerLink(oldNode.output.tasksPerLink);
157 end
158 elseif isa(oldNode, 'Join')
159 % Queue for later processing after Forks exist
160 joinPendingList{end+1} = struct('modelIdx', m, ...
161 'oldNode', oldNode, 'newName', newName);
162 continue;
163 elseif isa(oldNode, 'Logger')
164 newNode = Logger(unionNetwork, newName);
165 elseif isa(oldNode, 'Cache')
166 newNode = Cache(unionNetwork, newName, ...
167 oldNode.items.nitems, oldNode.itemLevelCap, ...
168 oldNode.replacementStrategy);
169 elseif isa(oldNode, 'Place')
170 newNode = Place(unionNetwork, newName);
171 elseif isa(oldNode, 'Transition')
172 newNode = Transition(unionNetwork, newName);
173 else
174 line_warning(mfilename, ...
175 sprintf('Unsupported node type: %s, skipping', class(oldNode)));
176 continue;
177 end
178
179 nodeMap{m}(oldNode.getName()) = newNode;
180 end
181 end
182
183 % Phase 2: Process pending Join nodes (after Forks exist)
184 for j = 1:length(joinPendingList)
185 item = joinPendingList{j};
186 m = item.modelIdx;
187 oldNode = item.oldNode;
188 newName = item.newName;
189
190 if ~isempty(oldNode.joinOf)
191 forkName = oldNode.joinOf.getName();
192 if isKey(nodeMap{m}, forkName)
193 forkNode = nodeMap{m}(forkName);
194 newNode = Join(unionNetwork, newName, forkNode);
195 else
196 newNode = Join(unionNetwork, newName);
197 end
198 else
199 newNode = Join(unionNetwork, newName);
200 end
201 nodeMap{m}(oldNode.getName()) = newNode;
202 end
203
204 % Phase 3: Create job classes for each model
205 for m = 1:length(models)
206 model = models{m};
207 modelName = model.getName();
208 prefix = [modelName, '_'];
209
210 classes = model.getClasses();
211 classMap{m} = containers.Map('KeyType', 'char', 'ValueType', 'any');
212
213 for c = 1:length(classes)
214 oldClass = classes{c};
215 newName = [prefix, oldClass.getName()];
216
217 if isa(oldClass, 'OpenClass')
218 newClass = OpenClass(unionNetwork, newName, oldClass.priority);
219 elseif isa(oldClass, 'SelfLoopingClass')
220 % Map reference station to new node
221 oldRefStatName = oldClass.refstat.getName();
222 if isKey(nodeMap{m}, oldRefStatName)
223 newRefStat = nodeMap{m}(oldRefStatName);
224 else
225 line_error(mfilename, ...
226 sprintf('Reference station %s not found for class %s', oldRefStatName, oldClass.getName()));
227 end
228 newClass = SelfLoopingClass(unionNetwork, newName, ...
229 oldClass.population, newRefStat, oldClass.priority);
230 elseif isa(oldClass, 'ClosedClass')
231 % Map reference station to new node
232 oldRefStatName = oldClass.refstat.getName();
233 if isKey(nodeMap{m}, oldRefStatName)
234 newRefStat = nodeMap{m}(oldRefStatName);
235 else
236 line_error(mfilename, ...
237 sprintf('Reference station %s not found for class %s', oldRefStatName, oldClass.getName()));
238 end
239 newClass = ClosedClass(unionNetwork, newName, ...
240 oldClass.population, newRefStat, oldClass.priority);
241 else
242 line_warning(mfilename, ...
243 sprintf('Unknown class type: %s, skipping', class(oldClass)));
244 continue;
245 end
246
247 classMap{m}(oldClass.getName()) = newClass;
248 end
249 end
250
251 % Phase 4: Set service and arrival distributions
252 for m = 1:length(models)
253 model = models{m};
254 nodes = model.getNodes();
255 classes = model.getClasses();
256
257 for n = 1:length(nodes)
258 oldNode = nodes{n};
259
260 % Handle Source arrivals
261 if isa(oldNode, 'Source')
262 for c = 1:length(classes)
263 oldClass = classes{c};
264 if isa(oldClass, 'OpenClass')
265 if isKey(classMap{m}, oldClass.getName())
266 newClass = classMap{m}(oldClass.getName());
267 if ~isempty(oldNode.arrivalProcess) && ...
268 length(oldNode.arrivalProcess) >= oldClass.index && ...
269 ~isempty(oldNode.arrivalProcess{oldClass.index})
270 dist = oldNode.arrivalProcess{oldClass.index};
271 if ~isa(dist, 'Disabled')
272 unionSource.setArrival(newClass, copy(dist));
273 end
274 end
275 end
276 end
277 end
278 continue;
279 end
280
281 % Skip Sink
282 if isa(oldNode, 'Sink')
283 continue;
284 end
285
286 % Skip auto-added ClassSwitch
287 if isa(oldNode, 'ClassSwitch') && isprop(oldNode, 'autoAdded') && oldNode.autoAdded
288 continue;
289 end
290
291 % Get mapped node
292 if ~isKey(nodeMap{m}, oldNode.getName())
293 continue;
294 end
295 newNode = nodeMap{m}(oldNode.getName());
296
297 % Set service distributions for Queue/Delay
298 if isa(oldNode, 'Queue') || isa(oldNode, 'Delay')
299 for c = 1:length(classes)
300 oldClass = classes{c};
301 if ~isKey(classMap{m}, oldClass.getName())
302 continue;
303 end
304 newClass = classMap{m}(oldClass.getName());
305
306 try
307 dist = oldNode.getService(oldClass);
308 if ~isempty(dist) && ~isa(dist, 'Disabled')
309 newNode.setService(newClass, copy(dist));
310 end
311 catch
312 % Service not defined for this class, skip
313 end
314 end
315 end
316 end
317 end
318
319 % Phase 5: Build routing matrix from linked routing matrices
320 P = unionNetwork.initRoutingMatrix();
321 nUnionClasses = unionNetwork.getNumberOfClasses();
322 nUnionNodes = unionNetwork.getNumberOfNodes();
323
324 % Initialize P cells if needed
325 for r = 1:nUnionClasses
326 for s = 1:nUnionClasses
327 if isempty(P{r,s})
328 P{r,s} = zeros(nUnionNodes);
329 end
330 end
331 end
332
333 for m = 1:length(models)
334 thisModel = models{m};
335 nodes = thisModel.getNodes();
336 classes = thisModel.getClasses();
337
338 % Get the linked routing matrix from this model
339 % This contains the full inter-class routing that was passed to link()
340 Pm = [];
341 try
342 Pm = thisModel.getLinkedRoutingMatrix();
343 catch
344 % If model was never linked, try to get struct and use rtorig
345 try
346 sn = thisModel.getStruct(false);
347 Pm = sn.rtorig;
348 catch
349 Pm = [];
350 end
351 end
352
353 if isempty(Pm)
354 % Fall back to outputStrategy-based extraction if no linked matrix
355 for n = 1:length(nodes)
356 oldNode = nodes{n};
357
358 % Skip nodes without routing output
359 if ~isprop(oldNode, 'output') || isempty(oldNode.output) || ...
360 ~isprop(oldNode.output, 'outputStrategy')
361 continue;
362 end
363
364 % Skip auto-added ClassSwitch
365 if isa(oldNode, 'ClassSwitch') && isprop(oldNode, 'autoAdded') && oldNode.autoAdded
366 continue;
367 end
368
369 % Get mapped source node
370 if ~isKey(nodeMap{m}, oldNode.getName())
371 continue;
372 end
373 newSrcNode = nodeMap{m}(oldNode.getName());
374
375 for c = 1:length(classes)
376 oldClass = classes{c};
377
378 if ~isKey(classMap{m}, oldClass.getName())
379 continue;
380 end
381
382 if length(oldNode.output.outputStrategy) < oldClass.index || ...
383 isempty(oldNode.output.outputStrategy{oldClass.index})
384 continue;
385 end
386
387 newClass = classMap{m}(oldClass.getName());
388 strategy = oldNode.output.outputStrategy{oldClass.index};
389
390 % Check for probabilistic routing with destinations
391 if length(strategy) >= 3 && ~isempty(strategy{3})
392 probs = strategy{3};
393
394 for p = 1:length(probs)
395 destNode = probs{p}{1};
396 prob = probs{p}{2};
397
398 if isKey(nodeMap{m}, destNode.getName())
399 newDestNode = nodeMap{m}(destNode.getName());
400
401 % Set probability in routing matrix
402 % P{r,s}(i,j) - from class r at node i to class s at node j
403 P{newClass.index, newClass.index}(...
404 newSrcNode.index, newDestNode.index) = prob;
405 end
406 end
407 end
408 end
409 end
410 else
411 % Use the linked routing matrix (includes inter-class routing)
412 nModelClasses = length(classes);
413 nModelNodes = length(nodes);
414
415 for r = 1:nModelClasses
416 oldClassR = classes{r};
417 if ~isKey(classMap{m}, oldClassR.getName())
418 continue;
419 end
420 newClassR = classMap{m}(oldClassR.getName());
421
422 for s = 1:nModelClasses
423 oldClassS = classes{s};
424 if ~isKey(classMap{m}, oldClassS.getName())
425 continue;
426 end
427 newClassS = classMap{m}(oldClassS.getName());
428
429 % Check if this routing block exists in original matrix
430 if size(Pm,1) < r || size(Pm,2) < s || isempty(Pm{r,s})
431 continue;
432 end
433
434 Prs = Pm{r,s};
435
436 % Copy routing probabilities, mapping old node indices to new
437 for i = 1:min(nModelNodes, size(Prs,1))
438 oldNodeI = nodes{i};
439
440 % Skip auto-added ClassSwitch nodes
441 if isa(oldNodeI, 'ClassSwitch') && isprop(oldNodeI, 'autoAdded') && oldNodeI.autoAdded
442 continue;
443 end
444
445 if ~isKey(nodeMap{m}, oldNodeI.getName())
446 continue;
447 end
448 newNodeI = nodeMap{m}(oldNodeI.getName());
449
450 for j = 1:min(nModelNodes, size(Prs,2))
451 if Prs(i,j) == 0
452 continue;
453 end
454
455 oldNodeJ = nodes{j};
456
457 % Skip auto-added ClassSwitch nodes
458 if isa(oldNodeJ, 'ClassSwitch') && isprop(oldNodeJ, 'autoAdded') && oldNodeJ.autoAdded
459 continue;
460 end
461
462 if ~isKey(nodeMap{m}, oldNodeJ.getName())
463 continue;
464 end
465 newNodeJ = nodeMap{m}(oldNodeJ.getName());
466
467 P{newClassR.index, newClassS.index}(newNodeI.index, newNodeJ.index) = Prs(i,j);
468 end
469 end
470 end
471 end
472 end
473 end
474
475 % Link the network with the routing matrix
476 unionNetwork.link(P);
477 end
478
479 function unionNetwork = toNetwork(ensemble)
480 % TONETWORK Alias for merge
481 %
482 % UNIONNETWORK = TONETWORK(ENSEMBLE) - see MERGE
483
484 unionNetwork = Ensemble.merge(ensemble);
485 end
486 end
487end
Definition mmt.m:92