1classdef Cluster < handle
2 % CLUSTER Builder
for cluster models with comparison helpers.
4 % Mirrors
the Java jline.gen.Cluster and
the Python
5 % line_solver.gen.Cluster builders.
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();
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)
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.
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;
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.
49 error('numStations must be positive');
51 if isempty(obj.serviceRates)
54 sample = obj.serviceRates(1, 1);
57 obj.serviceRates = repmat(sample, M, 1);
58 obj.stationCounts = ones(M, 1);
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');
66 obj.arrivalRates = lambda;
69 function obj = setArrivalRates(obj, lambdas)
70 % SETARRIVALRATES Per-class arrival rates (length R).
72 error('arrival rates must be positive');
74 obj.arrivalRates = lambdas(:)';
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');
82 obj.serviceRates = repmat(mu, obj.numStations, 1);
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');
91 error('service rates must be positive');
93 obj.serviceRates = rates;
96 function obj = setDispatching(obj, dispatching)
97 obj.dispatching = dispatching;
100 function obj = setScheduling(obj, scheduling)
101 obj.scheduling = scheduling;
104 function obj = setStationServers(obj, counts)
105 if numel(counts) ~= obj.numStations
106 error('counts length must equal numStations');
108 obj.stationCounts = counts(:);
111 function obj = setProbabilities(obj, probs)
112 % SETPROBABILITIES PROB dispatching with per-server probabilities.
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.
117 if numel(probs) ~= obj.numStations
118 error('probs length must equal numStations');
120 obj.dispatchProbs = probs(:)'; % single row, broadcast at build
122 if size(probs, 2) ~= obj.numStations
123 error('probs must have numStations columns');
125 obj.dispatchProbs = probs;
127 obj.dispatching = RoutingStrategy.PROB;
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');
135 if any(abs(weights - round(weights)) > 1e-9)
136 error('WRROBIN weights must be integers');
138 obj.dispatchWeights = weights(:)';
139 obj.dispatching = RoutingStrategy.WRROBIN;
142 function obj = setArrivalSCV(obj, scv)
143 % SETARRIVALSCV SCV of
the arrival process (open clusters only).
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);
150 obj.arrivalScvs = scv * ones(1, R);
153 error('arrivalSCV length must equal number of classes');
155 obj.arrivalScvs = scv(:)';
157 if any(obj.arrivalScvs <= 0)
158 error('SCV must be positive');
162 function obj = setServiceSCV(obj, scv)
163 % SETSERVICESCV SCV of
the per-server service distribution.
165 % Either a scalar (broadcast), a length-M vector (per-server,
166 % broadcast across classes), or a (M x R) matrix.
168 R = numel(obj.population);
170 R = numel(obj.arrivalRates);
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;
180 error('serviceSCV must be a scalar, length-M vector, or (M x R) matrix');
182 if any(obj.serviceScvs(:) <= 0)
183 error('SCV must be positive');
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');
193 obj.dispatching = RoutingStrategy.SQ;
196 function obj = setClosed(obj, population, thinkTime)
198 if isscalar(population)
199 obj.population = population;
200 obj.thinkTimes = thinkTime;
202 if numel(population) ~= numel(thinkTime)
203 error('population and thinkTime must have
the same length');
205 obj.population = population(:)';
206 obj.thinkTimes = thinkTime(:)';
210 function model = build(obj)
211 % BUILD Construct
the configured Network model.
214 R = numel(obj.population);
216 R = numel(obj.arrivalRates);
219 strategy = cell(M, 1);
221 strategy{i} = obj.scheduling;
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);
230 error(
'service rates must be positive');
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));
242 factoryDispatch = RoutingStrategy.RAND;
244 factoryDispatch = obj.dispatching;
248 model = MNetwork.clusterClosed(obj.population, obj.thinkTimes, ...
249 D, strategy, obj.stationCounts, factoryDispatch);
251 model = MNetwork.cluster(obj.arrivalRates, D, strategy, ...
252 obj.stationCounts, factoryDispatch);
255 obj.applyDispatcherConfig(model, R);
256 obj.applyDistributionScvs(model, R);
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');
265 scv = obj.arrivalScvs(r);
267 src.setArrival(jobclasses{r}, ...
268 APH.fitMeanAndSCV(1.0 / obj.arrivalRates(r), scv));
273 if ~isempty(obj.serviceScvs)
274 sr = obj.serviceRates;
275 if size(sr, 2) == 1 && R > 1
276 sr = repmat(sr, 1, R);
278 for i = 1:obj.numStations
279 server = model.getNodeByName(['
Station', num2str(i)]);
281 scv = obj.serviceScvs(i, r);
283 server.setService(jobclasses{r}, ...
284 APH.fitMeanAndSCV(1.0 / sr(i, r), scv));
291 function applyDispatcherConfig(obj, model, R)
292 if obj.dispatching == RoutingStrategy.PROB && ~isempty(obj.dispatchProbs)
293 dispatcher = model.getNodeByName('Dispatcher');
294 jobclasses = model.classes;
296 if size(obj.dispatchProbs, 1) == 1
297 probsRow = obj.dispatchProbs;
299 probsRow = obj.dispatchProbs(r, :);
301 for i = 1:obj.numStations
302 server = model.getNodeByName(['
Station', num2str(i)]);
303 dispatcher.setProbRouting(jobclasses{r}, server, probsRow(i));
306 elseif obj.dispatching == RoutingStrategy.WRROBIN && ~isempty(obj.dispatchWeights)
307 dispatcher = model.getNodeByName('Dispatcher');
308 jobclasses = model.classes;
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));
316 elseif obj.dispatching == RoutingStrategy.SQ && ~isempty(obj.sqD)
317 dispatcher = model.getNodeByName('Dispatcher');
318 jobclasses = model.classes;
320 dispatcher.setRouting(jobclasses{r}, RoutingStrategy.SQ, obj.sqD);
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());
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());
347 function out = sweepArrivalRate(obj, rates, solverFcn)
349 error(
'sweepArrivalRate is only defined for open clusters');
351 if numel(obj.arrivalRates) ~= 1
352 error(
'sweepArrivalRate requires a single class');
354 out = containers.Map(
'KeyType',
'double',
'ValueType',
'any');
355 saved = obj.arrivalRates;
356 cleaner = onCleanup(@() obj.restoreArrivalRates(saved));
358 obj.arrivalRates = r;
359 out(r) = solverFcn(obj.build());
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, :);
372 obj.serviceRates = repmat(perClass, m, 1);
373 obj.stationCounts = ones(m, 1);
374 out(m) = solverFcn(obj.build());
379 methods (Access =
private)
380 function restoreDispatching(obj, saved)
381 obj.dispatching = saved;
383 function restoreScheduling(obj, saved)
384 obj.scheduling = saved;
386 function restoreArrivalRates(obj, saved)
387 obj.arrivalRates = saved;
389 function restoreServers(obj, M, rates, counts)
391 obj.serviceRates = rates;
392 obj.stationCounts = counts;