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