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)
309 % Copy all services including Disabled to ensure proper configuration
310 newNode.setService(newClass, copy(dist));
311 end
312 catch
313 % Service not defined for this class, skip
314 end
315 end
316 end
317 end
318 end
319
320 % Phase 5: Build routing matrix from linked routing matrices
321 P = unionNetwork.initRoutingMatrix();
322 nUnionClasses = unionNetwork.getNumberOfClasses();
323 nUnionNodes = unionNetwork.getNumberOfNodes();
324
325 % Initialize P cells if needed
326 for r = 1:nUnionClasses
327 for s = 1:nUnionClasses
328 if isempty(P{r,s})
329 P{r,s} = zeros(nUnionNodes);
330 end
331 end
332 end
333
334 for m = 1:length(models)
335 thisModel = models{m};
336 nodes = thisModel.getNodes();
337 classes = thisModel.getClasses();
338
339 % Get the linked routing matrix from this model
340 % This contains the full inter-class routing that was passed to link()
341 Pm = [];
342 try
343 Pm = thisModel.getLinkedRoutingMatrix();
344 catch
345 % If model was never linked, try to get struct and use rtorig
346 try
347 sn = thisModel.getStruct(false);
348 Pm = sn.rtorig;
349 catch
350 Pm = [];
351 end
352 end
353
354 if isempty(Pm)
355 % Fall back to outputStrategy-based extraction if no linked matrix
356 for n = 1:length(nodes)
357 oldNode = nodes{n};
358
359 % Skip nodes without routing output
360 if ~isprop(oldNode, 'output') || isempty(oldNode.output) || ...
361 ~isprop(oldNode.output, 'outputStrategy')
362 continue;
363 end
364
365 % Skip auto-added ClassSwitch
366 if isa(oldNode, 'ClassSwitch') && isprop(oldNode, 'autoAdded') && oldNode.autoAdded
367 continue;
368 end
369
370 % Get mapped source node
371 if ~isKey(nodeMap{m}, oldNode.getName())
372 continue;
373 end
374 newSrcNode = nodeMap{m}(oldNode.getName());
375
376 for c = 1:length(classes)
377 oldClass = classes{c};
378
379 if ~isKey(classMap{m}, oldClass.getName())
380 continue;
381 end
382
383 if length(oldNode.output.outputStrategy) < oldClass.index || ...
384 isempty(oldNode.output.outputStrategy{oldClass.index})
385 continue;
386 end
387
388 newClass = classMap{m}(oldClass.getName());
389 strategy = oldNode.output.outputStrategy{oldClass.index};
390
391 % Check for probabilistic routing with destinations
392 if length(strategy) >= 3 && ~isempty(strategy{3})
393 probs = strategy{3};
394
395 for p = 1:length(probs)
396 destNode = probs{p}{1};
397 prob = probs{p}{2};
398
399 if isKey(nodeMap{m}, destNode.getName())
400 newDestNode = nodeMap{m}(destNode.getName());
401
402 % Set probability in routing matrix
403 % P{r,s}(i,j) - from class r at node i to class s at node j
404 P{newClass.index, newClass.index}(...
405 newSrcNode.index, newDestNode.index) = prob;
406 end
407 end
408 end
409 end
410 end
411 else
412 % Use the linked routing matrix (includes inter-class routing)
413 nModelClasses = length(classes);
414 nModelNodes = length(nodes);
415
416 for r = 1:nModelClasses
417 oldClassR = classes{r};
418 if ~isKey(classMap{m}, oldClassR.getName())
419 continue;
420 end
421 newClassR = classMap{m}(oldClassR.getName());
422
423 for s = 1:nModelClasses
424 oldClassS = classes{s};
425 if ~isKey(classMap{m}, oldClassS.getName())
426 continue;
427 end
428 newClassS = classMap{m}(oldClassS.getName());
429
430 % Check if this routing block exists in original matrix
431 if size(Pm,1) < r || size(Pm,2) < s || isempty(Pm{r,s})
432 continue;
433 end
434
435 Prs = Pm{r,s};
436
437 % Copy routing probabilities, mapping old node indices to new
438 for i = 1:min(nModelNodes, size(Prs,1))
439 oldNodeI = nodes{i};
440
441 % Skip auto-added ClassSwitch nodes
442 if isa(oldNodeI, 'ClassSwitch') && isprop(oldNodeI, 'autoAdded') && oldNodeI.autoAdded
443 continue;
444 end
445
446 if ~isKey(nodeMap{m}, oldNodeI.getName())
447 continue;
448 end
449 newNodeI = nodeMap{m}(oldNodeI.getName());
450
451 for j = 1:min(nModelNodes, size(Prs,2))
452 if Prs(i,j) == 0
453 continue;
454 end
455
456 oldNodeJ = nodes{j};
457
458 % Skip auto-added ClassSwitch nodes
459 if isa(oldNodeJ, 'ClassSwitch') && isprop(oldNodeJ, 'autoAdded') && oldNodeJ.autoAdded
460 continue;
461 end
462
463 if ~isKey(nodeMap{m}, oldNodeJ.getName())
464 continue;
465 end
466 newNodeJ = nodeMap{m}(oldNodeJ.getName());
467
468 P{newClassR.index, newClassS.index}(newNodeI.index, newNodeJ.index) = Prs(i,j);
469 end
470 end
471 end
472 end
473 end
474 end
475
476 % Disable checks before linking - merged networks from LQN ensembles may have
477 % Delay nodes with all-Disabled services (e.g., async-only layers) which would
478 % otherwise fail sanitize validation. This mirrors buildLayersRecursive behavior.
479 unionNetwork.setChecks(false);
480
481 % Link the network with the routing matrix
482 unionNetwork.link(P);
483 end
484
485 function unionNetwork = toNetwork(ensemble)
486 % TONETWORK Alias for merge
487 %
488 % UNIONNETWORK = TONETWORK(ENSEMBLE) - see MERGE
489
490 unionNetwork = Ensemble.merge(ensemble);
491 end
492 end
493end
Definition mmt.m:92