1classdef Ensemble < Model
2 % A model defined by a collection of sub-models.
4 % Copyright (c) 2012-2026, Imperial College London
12 function self = Ensemble(models)
13 % SELF = ENSEMBLE(MODELS)
15 self@Model(
'Ensemble');
16 self.ensemble = reshape(models,1,numel(models)); % flatten
19 function self = setEnsemble(self,ensemble)
20 % SELF = SETENSEMBLE(SELF,ENSEMBLE)
22 self.ensemble = ensemble;
25 function ensemble = getEnsemble(self)
26 % ENSEMBLE = GETENSEMBLE()
28 ensemble = self.ensemble;
31 function model = getModel(self, modelIdx)
32 model = self.ensemble{modelIdx};
36 methods(Access =
protected)
37 % Override copyElement method:
38 function clone = copyElement(self)
39 % CLONE = COPYELEMENT()
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});
51 function unionNetwork = merge(ensemble)
52 % MERGE Create a
union Network from all Networks in an ensemble
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.
60 % ensemble - Ensemble
object or cell array of Network objects
63 % unionNetwork - Network
object containing merged subnetworks
65 % Handle input - accept Ensemble
object or cell array
66 if isa(ensemble,
'Ensemble')
67 models = ensemble.getEnsemble();
68 elseif iscell(ensemble)
71 line_error(mfilename, 'Input must be an Ensemble or cell array of Networks');
74 % Edge case: empty ensemble
76 line_warning(mfilename, 'Empty ensemble provided, returning empty Network');
77 unionNetwork = Network('EmptyMergedNetwork');
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));
87 unionNetwork = Network(unionName);
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;
98 % Create single Source and Sink
if needed
102 unionSource = Source(unionNetwork,
'MergedSource');
103 unionSink = Sink(unionNetwork,
'MergedSink');
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
111 % Phase 1: Create
nodes for each model
112 for m = 1:length(models)
114 modelName = model.getName();
115 prefix = [modelName,
'_'];
117 nodes = model.getNodes();
118 nodeMap{m} = containers.Map(
'KeyType',
'char',
'ValueType',
'any');
120 for n = 1:length(
nodes)
122 newName = [prefix, oldNode.getName()];
124 if isa(oldNode,
'Source')
125 % Map to merged source
126 nodeMap{m}(oldNode.getName()) = unionSource;
128 elseif isa(oldNode,
'Sink')
130 nodeMap{m}(oldNode.getName()) = unionSink;
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);
139 if ~isempty(oldNode.lldScaling)
140 newNode.setLoadDependence(oldNode.lldScaling);
142 if ~isinf(oldNode.cap)
143 newNode.setCapacity(oldNode.cap);
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
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);
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);
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);
174 line_warning(mfilename, ...
175 sprintf('Unsupported node type: %s, skipping', class(oldNode)));
179 nodeMap{m}(oldNode.getName()) = newNode;
183 % Phase 2: Process pending Join
nodes (after Forks exist)
184 for j = 1:length(joinPendingList)
185 item = joinPendingList{j};
187 oldNode = item.oldNode;
188 newName = item.newName;
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);
196 newNode = Join(unionNetwork, newName);
199 newNode = Join(unionNetwork, newName);
201 nodeMap{m}(oldNode.getName()) = newNode;
204 % Phase 3: Create job
classes for each model
205 for m = 1:length(models)
207 modelName = model.getName();
208 prefix = [modelName,
'_'];
211 classMap{m} = containers.Map(
'KeyType',
'char',
'ValueType',
'any');
215 newName = [prefix, oldClass.getName()];
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);
225 line_error(mfilename, ...
226 sprintf(
'Reference station %s not found for class %s', oldRefStatName, oldClass.getName()));
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);
236 line_error(mfilename, ...
237 sprintf(
'Reference station %s not found for class %s', oldRefStatName, oldClass.getName()));
239 newClass = ClosedClass(unionNetwork, newName, ...
240 oldClass.population, newRefStat, oldClass.priority);
242 line_warning(mfilename, ...
243 sprintf(
'Unknown class type: %s, skipping',
class(oldClass)));
247 classMap{m}(oldClass.getName()) = newClass;
251 % Phase 4: Set service and arrival distributions
252 for m = 1:length(models)
254 nodes = model.getNodes();
257 for n = 1:length(
nodes)
260 % Handle Source arrivals
261 if isa(oldNode,
'Source')
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));
282 if isa(oldNode, 'Sink')
286 % Skip auto-added ClassSwitch
287 if isa(oldNode, 'ClassSwitch') && isprop(oldNode, 'autoAdded') && oldNode.autoAdded
292 if ~isKey(nodeMap{m}, oldNode.getName())
295 newNode = nodeMap{m}(oldNode.getName());
297 % Set service distributions
for Queue/Delay
298 if isa(oldNode,
'Queue') || isa(oldNode,
'Delay')
301 if ~isKey(classMap{m}, oldClass.getName())
304 newClass = classMap{m}(oldClass.getName());
307 dist = oldNode.getService(oldClass);
308 if ~isempty(dist) && ~isa(dist,
'Disabled')
309 newNode.setService(newClass, copy(dist));
312 % Service not defined for this class, skip
319 % Phase 5: Build routing matrix from linked routing matrices
320 P = unionNetwork.initRoutingMatrix();
321 nUnionClasses = unionNetwork.getNumberOfClasses();
322 nUnionNodes = unionNetwork.getNumberOfNodes();
324 % Initialize
P cells if needed
325 for r = 1:nUnionClasses
326 for s = 1:nUnionClasses
328 P{r,s} = zeros(nUnionNodes);
333 for m = 1:length(models)
334 thisModel = models{m};
335 nodes = thisModel.getNodes();
336 classes = thisModel.getClasses();
338 % Get the linked routing matrix from
this model
339 % This contains the full inter-
class routing that was passed to link()
342 Pm = thisModel.getLinkedRoutingMatrix();
344 % If model was never linked,
try to get
struct and use rtorig
346 sn = thisModel.getStruct(
false);
354 % Fall back to outputStrategy-based extraction
if no linked matrix
355 for n = 1:length(
nodes)
358 % Skip
nodes without routing output
359 if ~isprop(oldNode,
'output') || isempty(oldNode.output) || ...
360 ~isprop(oldNode.output,
'outputStrategy')
364 % Skip auto-added ClassSwitch
365 if isa(oldNode, 'ClassSwitch') && isprop(oldNode, 'autoAdded') && oldNode.autoAdded
369 % Get mapped source node
370 if ~isKey(nodeMap{m}, oldNode.getName())
373 newSrcNode = nodeMap{m}(oldNode.getName());
378 if ~isKey(classMap{m}, oldClass.getName())
382 if length(oldNode.output.outputStrategy) < oldClass.index || ...
383 isempty(oldNode.output.outputStrategy{oldClass.index})
387 newClass = classMap{m}(oldClass.getName());
388 strategy = oldNode.output.outputStrategy{oldClass.index};
390 % Check
for probabilistic routing with destinations
391 if length(strategy) >= 3 && ~isempty(strategy{3})
394 for p = 1:length(probs)
395 destNode = probs{p}{1};
398 if isKey(nodeMap{m}, destNode.getName())
399 newDestNode = nodeMap{m}(destNode.getName());
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;
411 % Use the linked routing matrix (includes inter-
class routing)
412 nModelClasses = length(
classes);
413 nModelNodes = length(
nodes);
415 for r = 1:nModelClasses
417 if ~isKey(classMap{m}, oldClassR.getName())
420 newClassR = classMap{m}(oldClassR.getName());
422 for s = 1:nModelClasses
424 if ~isKey(classMap{m}, oldClassS.getName())
427 newClassS = classMap{m}(oldClassS.getName());
429 % Check
if this routing block exists in original matrix
430 if size(Pm,1) < r || size(Pm,2) < s || isempty(Pm{r,s})
436 % Copy routing probabilities, mapping old node indices to
new
437 for i = 1:min(nModelNodes, size(Prs,1))
440 % Skip
auto-added ClassSwitch
nodes
441 if isa(oldNodeI,
'ClassSwitch') && isprop(oldNodeI,
'autoAdded') && oldNodeI.autoAdded
445 if ~isKey(nodeMap{m}, oldNodeI.getName())
448 newNodeI = nodeMap{m}(oldNodeI.getName());
450 for j = 1:min(nModelNodes, size(Prs,2))
457 % Skip
auto-added ClassSwitch
nodes
458 if isa(oldNodeJ,
'ClassSwitch') && isprop(oldNodeJ,
'autoAdded') && oldNodeJ.autoAdded
462 if ~isKey(nodeMap{m}, oldNodeJ.getName())
465 newNodeJ = nodeMap{m}(oldNodeJ.getName());
467 P{newClassR.index, newClassS.index}(newNodeI.index, newNodeJ.index) = Prs(i,j);
475 % Link the network with the routing matrix
476 unionNetwork.link(
P);
479 function unionNetwork = toNetwork(ensemble)
480 % TONETWORK Alias
for merge
482 % UNIONNETWORK = TONETWORK(ENSEMBLE) - see MERGE
484 unionNetwork = Ensemble.merge(ensemble);