LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
Station.m
1classdef Station < StatefulNode
2 % An abstract class for nodes where jobs station
3 %
4 % Copyright (c) 2012-2026, Imperial College London
5 % All rights reserved.
6
7 properties
8 numberOfServers;
9 cap;
10 dropRule;
11 classCap;
12 lldScaling; % limited load-dependence scaling factors
13 lcdScaling; % limited class-dependence scaling factors (product-form beta_{i,r})
14 lcdScalingPeak; % peak (max) class-dependent rate scaling per class, used to normalize Util (T*S/peak)
15 ljdScaling; % limited joint-dependence scaling factors (non-product-form eta_i)
16 ljdScalingPeak; % peak (max) joint-dependent rate scaling per class, used to normalize Util (T*S/peak)
17 stationIndex;
18 patienceDistributions; % per-class patience distributions (cell array indexed by class)
19 end
20
21 methods(Hidden)
22 %Constructor
23 function self = Station(name)
24 % SELF = STATION(NAME)
25
26 self@StatefulNode(name);
27 self.cap = Inf;
28 self.classCap = [];
29 self.lldScaling = [];
30 self.patienceDistributions = [];
31 end
32
33 % don't expose to avoid accidental call without checking the queue
34 % scheduling discipline
35 function setLimitedLoadDependence(self, alpha)
36 % SETLIMITEDLOADDEPENDENCE(self, alpha)
37 % alpha(ni) is the service rate scaling when there are ni>=1
38 % jobs in the system
39 self.lldScaling = alpha;
40 end
41
42 % don't expose to avoid accidental call without checking the queue
43 % scheduling discipline
44 function setLimitedClassDependence(self, gamma, peakRatePerClass)
45 % SETLIMITEDCLASSDEPENDENCE(self, gamma, peakRatePerClass)
46 %
47 % gamma(ni) is a function handle, where ni=[ni1,...,niR]
48 % is the service rate scaling when there are nir jobs at
49 % station i in class r.
50 % peakRatePerClass is the peak (maximum) value of the rate
51 % scaling per class (e.g. the effective number of servers). It is
52 % REQUIRED and is used to normalize utilization as Util = T*S/peak,
53 % matching the T*S/c convention of ordinary multiserver stations.
54 % A scalar is broadcast to every class.
55 if ~isa(gamma,'function_handle')
56 line_error(mfilename, 'Class dependence must be specified through a function handle.');
57 end
58 if nargin < 3 || isempty(peakRatePerClass)
59 line_error(mfilename, 'Class dependence requires an explicit peak rate: setClassDependence(beta, peakRatePerClass). Pass a scalar (identical peak for every class) or a per-class vector.');
60 end
61 if ~isnumeric(peakRatePerClass) || any(peakRatePerClass(:) <= 0)
62 line_error(mfilename, 'peakRatePerClass must be a positive scalar or per-class vector.');
63 end
64 self.lcdScaling = gamma;
65 self.lcdScalingPeak = peakRatePerClass(:)';
66 end
67
68 % don't expose to avoid accidental call without checking the queue
69 % scheduling discipline
70 function setLimitedJointDependence(self, eta, peakRatePerClass)
71 % SETLIMITEDJOINTDEPENDENCE(self, eta, peakRatePerClass)
72 %
73 % eta(ni) is a function handle, where ni=[ni1,...,niR] is the
74 % joint per-class population at the station. It returns either a
75 % scalar service-rate scaling shared by every class, or a length-R
76 % per-class vector. Unlike setLimitedClassDependence (beta_{i,r},
77 % which must depend on the own-class marginal n_{i,r} and preserves
78 % BCMP product form), eta may read the joint vector arbitrarily and
79 % is therefore NON-product-form: solvers treat it as an
80 % approximation with no exactness/uniqueness guarantee.
81 % peakRatePerClass is REQUIRED and normalizes Util = T*S/peak; a
82 % scalar is broadcast to every class.
83 if ~isa(eta,'function_handle')
84 line_error(mfilename, 'Joint dependence must be specified through a function handle.');
85 end
86 if nargin < 3 || isempty(peakRatePerClass)
87 line_error(mfilename, 'Joint dependence requires an explicit peak rate: setJointDependence(eta, peakRatePerClass). Pass a scalar (identical peak for every class) or a per-class vector.');
88 end
89 if ~isnumeric(peakRatePerClass) || any(peakRatePerClass(:) <= 0)
90 line_error(mfilename, 'peakRatePerClass must be a positive scalar or per-class vector.');
91 end
92 self.ljdScaling = eta;
93 self.ljdScalingPeak = peakRatePerClass(:)';
94 end
95
96 % don't expose to avoid accidental call without checking the queue
97
98
99 % don't expose to avoid accidental call without checking the queue
100
101
102 end
103
104 methods
105
106 function self = removeJobClass(self, jobclass)
107 % SELF = REMOVEJOBCLASS(JOBCLASS)
108 %
109 % Drop the per-class capacity, drop rule and patience of JOBCLASS
110 % on top of the routing configuration handled by Node.
111
112 remaining = self.remainingClassIndexes(jobclass);
113 removeJobClass@Node(self, jobclass);
114 K = numel(remaining) + 1;
115 if numel(self.classCap) == K
116 self.classCap = self.classCap(remaining);
117 end
118 if numel(self.dropRule) == K
119 self.dropRule = self.dropRule(remaining);
120 end
121 if numel(self.patienceDistributions) == K
122 self.patienceDistributions = self.patienceDistributions(remaining);
123 end
124 end
125
126 function self = setDropRule(self, class, drop)
127 % SELF = SETDROPRULE(CLASS, DROPRULE)
128
129 self.dropRule(class) = drop;
130 end
131
132
133 function setNumServers(self, value)
134 % SETNUMSERVERS(VALUE)
135
136 self.numberOfServers = value;
137 end
138
139 function setNumberOfServers(self, value)
140 % SETNUMBEROFSERVERS(VALUE)
141
142 self.numberOfServers = value;
143 end
144
145 function value = getNumServers(self)
146 % VALUE = GETNUMSERVERS()
147
148 value = self.numberOfServers;
149 end
150
151 function value = getNumberOfServers(self)
152 % VALUE = GETNUMBEROFSERVERS()
153
154 value = self.numberOfServers;
155 end
156
157 function setCapacity(self, value)
158 % SETCAPACITY(VALUE)
159
160 self.cap = value;
161 end
162
163 function setCap(self, value)
164 % SETCAP(VALUE)
165 % Alias for setCapacity() for backwards compatibility
166
167 self.setCapacity(value);
168 end
169
170 function setChainCapacity(self, values)
171 % SETCHAINCAPACITY(VALUES)
172
173 sn = self.model.getStruct;
174 if numel(values) ~= sn.nchains
175 line_error(mfilename,'The method requires in input a capacity value for each chain.');
176 end
177 for c = 1:sn.nchains
178 inchain = sn.inchain{c};
179 for r = inchain
180 if ~self.isServiceDisabled(r)
181 self.classCap(r) = values(c);
182 else
183 self.classCap(r) = Inf;
184 end
185 end
186 end
187 self.cap = min(sum(self.classCap(self.classCap>0)), self.cap);
188 end
189
190
191 function isD = isServiceDefined(self, class)
192 K = size(self.model.getClasses(),2);
193 isD = true(1, K);
194 switch self.server.className
195 case 'ServiceTunnel'
196 %noop
197 otherwise
198 for r=1:K
199 if isempty(self.server.serviceProcess{1,r})
200 isD(r) = false;
201 end
202 end
203 end
204 end
205
206 function isD = isServiceDisabled(self, class)
207 % ISD = ISSERVICEDISABLED(CLASS)
208 if nargin>=2
209 switch self.server.className
210 case 'ServiceTunnel'
211 isD = false;
212 otherwise
213 isD = self.server.serviceProcess{1,class}{end}.isDisabled();
214 end
215 else
216 K = size(self.model.getClasses(),2);
217 isD = false(1, K);
218 switch self.server.className
219 case 'ServiceTunnel'
220 %noop
221 otherwise
222 %isD = cellfun(@(sp) sp{end}.isDisabled, self.server.serviceProcess);
223 for r=1:K
224 isD(r) = self.server.serviceProcess{1,r}{end}.isDisabled();
225 end
226 end
227 end
228 end
229
230 function isI = isServiceImmediate(self, class)
231 % ISI = ISSERVICEIMMEDIATE(CLASS)
232
233 isI = self.server.serviceProcess{1,class}{end}.isImmediate();
234 end
235
236 function R = getNumberOfServiceClasses(self)
237 % R = GETNUMBEROFSERVICECLASSES()
238
239 R = size(self.server.serviceProcess,2);
240 end
241
242 function [p] = getSelfLoopProbabilities(self)
243 % [P] = GETSELFLOOPPROBABILITIES()
244
245 R = getNumberOfServiceClasses(self);
246 p = zeros(1,R);
247 for k=1:R
248 nOutLinks = length(self.output.outputStrategy{k}{end});
249 switch RoutingStrategy.toText(self.output.outputStrategy{k}{2})
250 case 'Random'
251 p(k) = 1 / nOutLinks;
252 case RoutingStrategy.PROB
253 for t=1:nOutLinks % for all outgoing links
254 if strcmp(self.output.outputStrategy{k}{end}{t}{1}.name, self.name)
255 p(k) = self.output.outputStrategy{k}{end}{t}{2};
256 break
257 end
258 end
259 end
260 end
261 end
262
263 function [map, mu, phi] = getSourceRates(self)
264 % [PH,MU,PHI] = GETSOURCERATES()
265
266 nclasses = size(self.input.sourceClasses,2);
267 map = cell(1,nclasses);
268 mu = cell(1,nclasses);
269 phi = cell(1,nclasses);
270 for r=1:nclasses
271 if isempty(self.input.sourceClasses{r})
272 self.input.sourceClasses{r} = {[],ServiceStrategy.LI,Disabled.getInstance()};
273 map{r} = {[NaN],[NaN]};
274 mu{r} = NaN;
275 phi{r} = NaN;
276 elseif ~self.input.sourceClasses{r}{end}.isDisabled()
277 switch class(self.input.sourceClasses{r}{end})
278 case {'Replayer', 'Trace'}
279 aph = self.input.sourceClasses{r}{end}.fitAPH;
280 map{r} = aph.getProcess();
281 mu{r} = aph.getMu;
282 phi{r} = aph.getPhi;
283 case {'Exp','Coxian','Erlang','HyperExp','Markovian','APH','PH','ME','CME','RAP'}
284 map{r} = self.input.sourceClasses{r}{end}.getProcess;
285 mu{r} = self.input.sourceClasses{r}{end}.getMu;
286 phi{r} = self.input.sourceClasses{r}{end}.getPhi;
287 case {'Det','Uniform','Pareto','Gamma','Lognormal','Weibull'}
288 map{r} = self.input.sourceClasses{r}{end}.getProcess();
289 mu{r} = [1/self.input.sourceClasses{r}{end}.getMean];
290 phi{r} = [1];
291 case {'Bernoulli','Binomial','Poisson'}
292 % see _kb/04-networkstruct.md (node/process construction notes) for rationale
293 map{r} = self.input.sourceClasses{r}{end}.getProcess();
294 mu{r} = [self.input.sourceClasses{r}{end}.getRate];
295 phi{r} = [1];
296 case 'Geometric'
297 % see _kb/04-networkstruct.md (node/process construction notes) for rationale
298 map{r} = self.input.sourceClasses{r}{end}.getProcess();
299 mu{r} = [self.input.sourceClasses{r}{end}.getRate];
300 phi{r} = [1];
301 case 'MMPP2'
302 map{r} = self.input.sourceClasses{r}{end}.getProcess();
303 mu{r} = self.input.sourceClasses{r}{end}.getMu;
304 phi{r} = self.input.sourceClasses{r}{end}.getPhi;
305 case 'MAP'
306 map{r} = self.input.sourceClasses{r}{end}.getProcess();
307 mu{r} = self.input.sourceClasses{r}{end}.getMu;
308 phi{r} = self.input.sourceClasses{r}{end}.getPhi;
309 case 'NHPP'
310 map{r} = self.input.sourceClasses{r}{end}.getProcess();
311 mu{r} = self.input.sourceClasses{r}{end}.getTimeAverageRate();
312 phi{r} = 1;
313 case 'BMAP'
314 % {D0, D1, D_batch1, ..., D_batchK}: same layout
315 % as the JAR MatrixCell for batch arrivals
316 map{r} = self.input.sourceClasses{r}{end}.getProcess();
317 mapAggr = self.input.sourceClasses{r}{end}.toMAP;
318 mu{r} = mapAggr.getMu;
319 phi{r} = mapAggr.getPhi;
320 case 'MarkedMAP'
321 % see _kb/04-networkstruct.md (node/process construction notes) for rationale
322 map{r} = self.input.sourceClasses{r}{end}.getProcess();
323 mu{r} = self.input.sourceClasses{r}{end}.getMu;
324 phi{r} = self.input.sourceClasses{r}{end}.getPhi;
325 otherwise
326 % leave everything empty
327 end
328 else
329 map{r} = {[NaN],[NaN]};
330 mu{r} = NaN;
331 phi{r} = NaN;
332 end
333 end
334 end
335
336 function [map,mu,phi] = getServiceRates(self)
337 % [PH,MU,PHI] = GETSERVICERATES()
338
339 nclasses = size(self.server.serviceProcess,2);
340 map = cell(1,nclasses);
341 mu = cell(1,nclasses);
342 phi = cell(1,nclasses);
343 for r=1:nclasses
344 serviceProcess_r = self.server.serviceProcess{r};
345 if isempty(serviceProcess_r)
346 serviceProcess_r = {[],ServiceStrategy.LI,Disabled.getInstance()};
347 map{r} = {[NaN],[NaN]};
348 mu{r} = NaN;
349 phi{r} = NaN;
350 elseif serviceProcess_r{end}.isImmediate()
351 map{r} = {[-GlobalConstants.Immediate],[GlobalConstants.Immediate]};
352 mu{r} = [GlobalConstants.Immediate];
353 phi{r} = [1];
354 elseif ~serviceProcess_r{end}.isDisabled()
355 switch class(serviceProcess_r{end})
356 case {'Det','Uniform','Pareto','Gamma','Weibull','Lognormal'}
357 map{r} = serviceProcess_r{end}.getProcess();
358 mu{r} = [serviceProcess_r{end}.getRate];
359 phi{r} = [1];
360 case {'Bernoulli','Binomial','Poisson'}
361 % Counting distributions used as a service time; see
362 % the matching arm in getSourceRates above.
363 map{r} = serviceProcess_r{end}.getProcess();
364 mu{r} = [serviceProcess_r{end}.getRate];
365 phi{r} = [1];
366 case 'Geometric'
367 % Lattice-valued service time supported on
368 % {1,2,...}; see the matching arm in
369 % getSourceRates above.
370 map{r} = serviceProcess_r{end}.getProcess();
371 mu{r} = [serviceProcess_r{end}.getRate];
372 phi{r} = [1];
373 case {'Replayer', 'Trace'}
374 aph = serviceProcess_r{end}.fitAPH;
375 map{r} = aph.getProcess();
376 mu{r} = aph.getMu;
377 phi{r} = aph.getPhi;
378 case {'Exp','Coxian','Erlang','HyperExp','Markovian','APH','MAP','PH','ME','CME','RAP'}
379 map{r} = serviceProcess_r{end}.getProcess();
380 mu{r} = serviceProcess_r{end}.getMu;
381 phi{r} = serviceProcess_r{end}.getPhi;
382 case 'NHPP'
383 % Mirrors the Source arm above. Without this case the
384 % switch falls through with map/mu/phi unassigned, so a
385 % time-varying service rate reached sn.proc empty.
386 map{r} = serviceProcess_r{end}.getProcess();
387 mu{r} = serviceProcess_r{end}.getTimeAverageRate();
388 phi{r} = 1;
389 case 'MMPP2'
390 map{r} = serviceProcess_r{end}.getProcess();
391 mu{r} = serviceProcess_r{end}.getMu;
392 phi{r} = serviceProcess_r{end}.getPhi;
393 end
394 else
395 map{r} = {[NaN],[NaN]};
396 mu{r} = NaN;
397 phi{r} = NaN;
398 end
399 end
400 end
401
402 function summary(self)
403 % SUMMARY()
404
405 line_printf('\nNode: <strong>%s</strong>',self.getName);
406 line_printf('\nScheduling: %s',self.schedStrategy);
407 line_printf('\nNumber of Servers: %d',self.numberOfServers);
408 for r=1:length(self.output.outputStrategy)
409 classes = self.model.getClasses();
410 line_printf('\nRouting %s: %s',classes{r}.name,self.output.outputStrategy{r}{2});
411 end
412 % self.input.summary;
413 % self.server.summary;
414 % self.output.summary;
415 end
416
417 end
418end
Definition fjtag.m:161