LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
Cluster.m
1classdef Cluster < handle
2 % CLUSTER Builder for cluster models with comparison helpers.
3 %
4 % Mirrors the Java jline.gen.Cluster and the Python
5 % line_solver.gen.Cluster builders.
6 %
7 % Example:
8 % cluster = Cluster('numStations', 4, 'arrivalRate', 1.0, 'serviceRate', 0.4);
9 % cluster.setDispatching(RoutingStrategy.RAND).setScheduling(SchedStrategy.PS);
10 % model = cluster.build();
11 % table = SolverMVA(model).getAvgTable();
12
13 properties
14 numStations (1, 1) double = 2
15 arrivalRates double = 1.0
16 serviceRates double = 1.0 % (M, R) matrix of *rates* (1/mean service time)
17 stationCounts double % per-server multiplicity
18 scheduling = SchedStrategy.PS
19 dispatching = RoutingStrategy.RAND
20 closed (1, 1) logical = false
21 population double = [] % per-class population (closed only)
22 thinkTimes double = [] % per-class think time (closed only)
23 dispatchProbs = [] % PROB: rows = classes (1 broadcasts), cols = servers
24 dispatchWeights = [] % WRROBIN: per-server integer weights
25 kChoicesK = [] % KCHOICES: K parameter
26 kChoicesMemory = false % KCHOICES: memory flag
27 arrivalScvs = [] % per-class arrival SCVs (1 = exponential, default)
28 serviceScvs = [] % (M, R) service SCVs (1 = exponential, default)
29 end
30
31 methods
32 function obj = Cluster()
33 % CLUSTER Default cluster: 2 servers, single open class with arrival
34 % rate 1.0, service rate 1.0 at each server, PS scheduling, RAND
35 % dispatching. Configure further via the chainable set* methods.
36 obj.numStations = 2;
37 obj.arrivalRates = 1.0;
38 obj.serviceRates = ones(2, 1);
39 obj.stationCounts = ones(2, 1);
40 obj.scheduling = SchedStrategy.PS;
41 obj.dispatching = RoutingStrategy.RAND;
42 obj.closed = false;
43 end
44
45 function obj = setNumStations(obj, M)
46 % SETNUMSTATIONS Number of parallel server queues. Replicates the
47 % current single-class service rate across all servers and resets
48 % the per-server multiplicity to all-1.
49 if M <= 0
50 error('numStations must be positive');
51 end
52 if isempty(obj.serviceRates)
53 sample = 1.0;
54 else
55 sample = obj.serviceRates(1, 1);
56 end
57 obj.numStations = M;
58 obj.serviceRates = repmat(sample, M, 1);
59 obj.stationCounts = ones(M, 1);
60 end
61
62 function obj = setArrivalRate(obj, lambda)
63 % SETARRIVALRATE Single-class arrival rate.
64 if ~isscalar(lambda) || lambda <= 0
65 error('arrival rate must be a positive scalar');
66 end
67 obj.arrivalRates = lambda;
68 end
69
70 function obj = setArrivalRates(obj, lambdas)
71 % SETARRIVALRATES Per-class arrival rates (length R).
72 if any(lambdas <= 0)
73 error('arrival rates must be positive');
74 end
75 obj.arrivalRates = lambdas(:)';
76 end
77
78 function obj = setServiceRate(obj, mu)
79 % SETSERVICERATE Single service rate, broadcast across all servers.
80 if ~isscalar(mu) || mu <= 0
81 error('service rate must be a positive scalar');
82 end
83 obj.serviceRates = repmat(mu, obj.numStations, 1);
84 end
85
86 function obj = setServiceRates(obj, rates)
87 % SETSERVICERATES Per-(server, class) service rates as a (M x R) matrix.
88 if size(rates, 1) ~= obj.numStations
89 error('serviceRates outer dim must equal numStations');
90 end
91 if any(rates(:) <= 0)
92 error('service rates must be positive');
93 end
94 obj.serviceRates = rates;
95 end
96
97 function obj = setDispatching(obj, dispatching)
98 obj.dispatching = dispatching;
99 end
100
101 function obj = setScheduling(obj, scheduling)
102 obj.scheduling = scheduling;
103 end
104
105 function obj = setStationCounts(obj, counts)
106 if numel(counts) ~= obj.numStations
107 error('counts length must equal numStations');
108 end
109 obj.stationCounts = counts(:);
110 end
111
112 function obj = setProbabilities(obj, probs)
113 % SETPROBABILITIES PROB dispatching with per-server probabilities.
114 %
115 % If PROBS is a row vector of length numStations it is broadcast to
116 % every class; otherwise it must be a (R x numStations) matrix.
117 if isvector(probs)
118 if numel(probs) ~= obj.numStations
119 error('probs length must equal numStations');
120 end
121 obj.dispatchProbs = probs(:)'; % single row, broadcast at build
122 else
123 if size(probs, 2) ~= obj.numStations
124 error('probs must have numStations columns');
125 end
126 obj.dispatchProbs = probs;
127 end
128 obj.dispatching = RoutingStrategy.PROB;
129 end
130
131 function obj = setWeights(obj, weights)
132 % SETWEIGHTS WRROBIN dispatching with per-server integer weights.
133 if numel(weights) ~= obj.numStations
134 error('weights length must equal numStations');
135 end
136 if any(abs(weights - round(weights)) > 1e-9)
137 error('WRROBIN weights must be integers');
138 end
139 obj.dispatchWeights = weights(:)';
140 obj.dispatching = RoutingStrategy.WRROBIN;
141 end
142
143 function obj = setArrivalSCV(obj, scv)
144 % SETARRIVALSCV SCV of the arrival process (open clusters only).
145 %
146 % Either a scalar (broadcast to all classes) or a row vector of
147 % length R. SCV != 1 swaps the per-class Exp distribution for
148 % APH.fitMeanAndSCV(1/rate, scv).
149 R = numel(obj.arrivalRates);
150 if isscalar(scv)
151 obj.arrivalScvs = scv * ones(1, R);
152 else
153 if numel(scv) ~= R
154 error('arrivalSCV length must equal number of classes');
155 end
156 obj.arrivalScvs = scv(:)';
157 end
158 if any(obj.arrivalScvs <= 0)
159 error('SCV must be positive');
160 end
161 end
162
163 function obj = setServiceSCV(obj, scv)
164 % SETSERVICESCV SCV of the per-server service distribution.
165 %
166 % Either a scalar (broadcast), a length-M vector (per-server,
167 % broadcast across classes), or a (M x R) matrix.
168 if obj.closed
169 R = numel(obj.population);
170 else
171 R = numel(obj.arrivalRates);
172 end
173 M = obj.numStations;
174 if isscalar(scv)
175 obj.serviceScvs = scv * ones(M, R);
176 elseif isvector(scv) && numel(scv) == M
177 obj.serviceScvs = repmat(scv(:), 1, R);
178 elseif size(scv, 1) == M && size(scv, 2) == R
179 obj.serviceScvs = scv;
180 else
181 error('serviceSCV must be a scalar, length-M vector, or (M x R) matrix');
182 end
183 if any(obj.serviceScvs(:) <= 0)
184 error('SCV must be positive');
185 end
186 end
187
188 function obj = setKChoices(obj, k, memory)
189 % SETKCHOICES KCHOICES dispatching: pick best of K random servers.
190 if nargin < 3, memory = false; end
191 if k < 1 || k ~= round(k)
192 error('k must be a positive integer');
193 end
194 obj.kChoicesK = k;
195 obj.kChoicesMemory = logical(memory);
196 obj.dispatching = RoutingStrategy.KCHOICES;
197 end
198
199 function obj = setClosed(obj, population, thinkTime)
200 obj.closed = true;
201 if isscalar(population)
202 obj.population = population;
203 obj.thinkTimes = thinkTime;
204 else
205 if numel(population) ~= numel(thinkTime)
206 error('population and thinkTime must have the same length');
207 end
208 obj.population = population(:)';
209 obj.thinkTimes = thinkTime(:)';
210 end
211 end
212
213 function model = build(obj)
214 % BUILD Construct the configured Network model.
215 M = obj.numStations;
216 if obj.closed
217 R = numel(obj.population);
218 else
219 R = numel(obj.arrivalRates);
220 end
221
222 strategy = cell(M, 1);
223 for i = 1:M
224 strategy{i} = obj.scheduling;
225 end
226
227 % Replicate single-rate vector to all classes if needed.
228 sr = obj.serviceRates;
229 if size(sr, 2) == 1 && R > 1
230 sr = repmat(sr, 1, R);
231 end
232 if any(sr(:) <= 0)
233 error('service rates must be positive');
234 end
235 D = 1.0 ./ sr;
236
237 % Strategies that need per-destination parameters cannot be passed
238 % to the static factory (which calls setRouting(class, strategy)
239 % with no extras). Build with RAND and reapply in post-processing.
240 needsPostProcess = ...
241 (obj.dispatching == RoutingStrategy.PROB && ~isempty(obj.dispatchProbs)) ...
242 || (obj.dispatching == RoutingStrategy.WRROBIN && ~isempty(obj.dispatchWeights)) ...
243 || (obj.dispatching == RoutingStrategy.KCHOICES && ~isempty(obj.kChoicesK));
244 if needsPostProcess
245 factoryDispatch = RoutingStrategy.RAND;
246 else
247 factoryDispatch = obj.dispatching;
248 end
249
250 if obj.closed
251 model = MNetwork.clusterClosed(obj.population, obj.thinkTimes, ...
252 D, strategy, obj.stationCounts, factoryDispatch);
253 else
254 model = MNetwork.cluster(obj.arrivalRates, D, strategy, ...
255 obj.stationCounts, factoryDispatch);
256 end
257
258 obj.applyDispatcherConfig(model, R);
259 obj.applyDistributionScvs(model, R);
260 end
261
262 function applyDistributionScvs(obj, model, R)
263 jobclasses = model.classes;
264 % Arrival SCVs (open clusters only).
265 if ~obj.closed && ~isempty(obj.arrivalScvs)
266 src = model.getNodeByName('Source');
267 for r = 1:R
268 scv = obj.arrivalScvs(r);
269 if scv ~= 1
270 src.setArrival(jobclasses{r}, ...
271 APH.fitMeanAndSCV(1.0 / obj.arrivalRates(r), scv));
272 end
273 end
274 end
275 % Service SCVs.
276 if ~isempty(obj.serviceScvs)
277 sr = obj.serviceRates;
278 if size(sr, 2) == 1 && R > 1
279 sr = repmat(sr, 1, R);
280 end
281 for i = 1:obj.numStations
282 server = model.getNodeByName(['Station', num2str(i)]);
283 for r = 1:R
284 scv = obj.serviceScvs(i, r);
285 if scv ~= 1
286 server.setService(jobclasses{r}, ...
287 APH.fitMeanAndSCV(1.0 / sr(i, r), scv));
288 end
289 end
290 end
291 end
292 end
293
294 function applyDispatcherConfig(obj, model, R)
295 if obj.dispatching == RoutingStrategy.PROB && ~isempty(obj.dispatchProbs)
296 dispatcher = model.getNodeByName('Dispatcher');
297 jobclasses = model.classes;
298 for r = 1:R
299 if size(obj.dispatchProbs, 1) == 1
300 probsRow = obj.dispatchProbs;
301 else
302 probsRow = obj.dispatchProbs(r, :);
303 end
304 for i = 1:obj.numStations
305 server = model.getNodeByName(['Station', num2str(i)]);
306 dispatcher.setProbRouting(jobclasses{r}, server, probsRow(i));
307 end
308 end
309 elseif obj.dispatching == RoutingStrategy.WRROBIN && ~isempty(obj.dispatchWeights)
310 dispatcher = model.getNodeByName('Dispatcher');
311 jobclasses = model.classes;
312 for r = 1:R
313 for i = 1:obj.numStations
314 server = model.getNodeByName(['Station', num2str(i)]);
315 dispatcher.setRouting(jobclasses{r}, RoutingStrategy.WRROBIN, ...
316 server, obj.dispatchWeights(i));
317 end
318 end
319 elseif obj.dispatching == RoutingStrategy.KCHOICES && ~isempty(obj.kChoicesK)
320 dispatcher = model.getNodeByName('Dispatcher');
321 jobclasses = model.classes;
322 for r = 1:R
323 dispatcher.setRouting(jobclasses{r}, RoutingStrategy.KCHOICES, ...
324 obj.kChoicesK, obj.kChoicesMemory);
325 end
326 end
327 end
328
329 function out = compareDispatching(obj, solverFcn, policies)
330 % OUT = COMPAREDISPATCHING(SOLVERFCN, POLICIES) returns a containers.Map
331 % from each policy in POLICIES to the AvgTable produced by SOLVERFCN(model).
332 out = containers.Map('KeyType', 'char', 'ValueType', 'any');
333 saved = obj.dispatching;
334 cleaner = onCleanup(@() obj.restoreDispatching(saved));
335 for k = 1:numel(policies)
336 obj.dispatching = policies(k);
337 out(char(string(policies(k)))) = solverFcn(obj.build());
338 end
339 end
340
341 function out = compareScheduling(obj, solverFcn, disciplines)
342 out = containers.Map('KeyType', 'char', 'ValueType', 'any');
343 saved = obj.scheduling;
344 cleaner = onCleanup(@() obj.restoreScheduling(saved));
345 for k = 1:numel(disciplines)
346 obj.scheduling = disciplines(k);
347 out(char(string(disciplines(k)))) = solverFcn(obj.build());
348 end
349 end
350
351 function out = sweepArrivalRate(obj, rates, solverFcn)
352 if obj.closed
353 error('sweepArrivalRate is only defined for open clusters');
354 end
355 if numel(obj.arrivalRates) ~= 1
356 error('sweepArrivalRate requires a single class');
357 end
358 out = containers.Map('KeyType', 'double', 'ValueType', 'any');
359 saved = obj.arrivalRates;
360 cleaner = onCleanup(@() obj.restoreArrivalRates(saved));
361 for r = rates(:)'
362 obj.arrivalRates = r;
363 out(r) = solverFcn(obj.build());
364 end
365 end
366
367 function out = sweepNumStations(obj, counts, solverFcn)
368 out = containers.Map('KeyType', 'double', 'ValueType', 'any');
369 savedM = obj.numStations;
370 savedRates = obj.serviceRates;
371 savedCounts = obj.stationCounts;
372 cleaner = onCleanup(@() obj.restoreServers(savedM, savedRates, savedCounts));
373 perClass = savedRates(1, :);
374 for m = counts(:)'
375 obj.numStations = m;
376 obj.serviceRates = repmat(perClass, m, 1);
377 obj.stationCounts = ones(m, 1);
378 out(m) = solverFcn(obj.build());
379 end
380 end
381 end
382
383 methods (Access = private)
384 function restoreDispatching(obj, saved)
385 obj.dispatching = saved;
386 end
387 function restoreScheduling(obj, saved)
388 obj.scheduling = saved;
389 end
390 function restoreArrivalRates(obj, saved)
391 obj.arrivalRates = saved;
392 end
393 function restoreServers(obj, M, rates, counts)
394 obj.numStations = M;
395 obj.serviceRates = rates;
396 obj.stationCounts = counts;
397 end
398 end
399end