LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
saveRegions.m
1function [simElem, simDoc] = saveRegions(self, simElem, simDoc)
2% [SIMELEM, SIMDOC] = SAVEREGIONS(SIMELEM, SIMDOC)
3
4% Copyright (c) 2012-2026, Imperial College London
5% All rights reserved.
6
7sn = self.getStruct;
8
9% Per-class station capacities (sn.classcap) have no JMT counterpart on the
10% Queue section: all jmt.engine.NodeSections.Queue constructors take a scalar
11% "size" and only the Storage (Place) section carries a per-class capacities
12% array. The per-class constraint is therefore exported as a synthetic
13% single-station blocking region, since jmt.engine.QueueNet.BlockingRegion is
14% the only JMT object with a maxCapacityPerClass vector. See jmtClassCapCon
15% below for when a constraint is emitted, and jmtClassCapAssert for which
16% capacities a region can express at all.
17classCapCon = jmtClassCapCon(sn);
18covered = false(sn.nstations,1); % stations already inside a region emitted below
19
20% <blockingRegion name="FCRegion1" type="default">
21% <regionNode nodeName="Queue 1"/>
22% <regionNode nodeName="Queue 2"/>
23% <globalConstraint maxJobs="2"/>
24% <globalMemoryConstraint maxMemory="-1"/>
25% <classConstraint jobClass="Class1" maxJobsPerClass="-1"/>
26% <classMemoryConstraint jobClass="Class1" maxMemoryPerClass="-1"/>
27% <dropRules dropThisClass="false" jobClass="Class1"/>
28% <classWeight jobClass="Class1" weight="1"/>
29% <classSize jobClass="Class1" size="1"/>
30% </blockingRegion>
31
32% First, create implicit FCR regions for LPS queues
33% LPS uses FCR to limit concurrent jobs at PS station
34lpsRegionIdx = length(self.model.regions); % Start numbering after explicit regions
35for i = 1:sn.nstations
36 ist = i;
37 if sn.sched(ist) == SchedStrategy.LPS
38 lpsRegionIdx = lpsRegionIdx + 1;
39 lpsLimit = sn.schedparam(ist, 1); % LPS limit stored in first column
40 ind = sn.stationToNode(ist);
41 nodeName = sn.nodenames{ind};
42
43 blockingRegion = simDoc.createElement('blockingRegion');
44 blockingRegion.setAttribute('name', ['LPSRegion', num2str(lpsRegionIdx)]);
45 blockingRegion.setAttribute('type', 'default');
46
47 regionNode = simDoc.createElement('regionNode');
48 regionNode.setAttribute('nodeName', nodeName);
49 blockingRegion.appendChild(regionNode);
50
51 globalConstraint = simDoc.createElement('globalConstraint');
52 globalConstraint.setAttribute('maxJobs', num2str(lpsLimit));
53 blockingRegion.appendChild(globalConstraint);
54
55 globalMemoryConstraint = simDoc.createElement('globalMemoryConstraint');
56 globalMemoryConstraint.setAttribute('maxMemory', '-1');
57 blockingRegion.appendChild(globalMemoryConstraint);
58
59 % A JMT node belongs to at most one blocking region
60 % (jmt.engine.QueueNet.NetNode holds a single region reference), so a
61 % per-class capacity at an LPS station would have to be merged into the
62 % LPS region rather than emitted as a second, overlapping one. That
63 % merge is never expressible: a region carries one drop rule per class
64 % shared by all of its constraints, the LPS region needs blocking
65 % (dropThisClass=false) to hold jobs at the station, and an expressible
66 % per-class capacity is always a loss constraint (see jmtClassCapAssert).
67 covered(ist) = true;
68 for c = 1:sn.nclasses
69 if isfinite(classCapCon(ist,c))
70 jmtClassCapAssert(sn, ist, c); % rejects the non-loss cases first
71 line_error(mfilename, sprintf(['Station %s has both LPS scheduling and a finite capacity %d for open class %s. ', ...
72 'JMT expresses both through a single blocking region, which admits only one drop rule per class, ', ...
73 'but LPS requires blocking while the open-class capacity requires dropping. Remove the per-class ', ...
74 'capacity or use a non-LPS scheduling strategy.'], nodeName, classCapCon(ist,c), sn.classnames{c}));
75 end
76 end
77
78 for c = 1:sn.nclasses
79 % dropRules - LPS uses blocking (waitq), not drop
80 dropRuleElem = simDoc.createElement('dropRules');
81 dropRuleElem.setAttribute('jobClass', sn.classnames{c});
82 dropRuleElem.setAttribute('dropThisClass', 'false');
83 blockingRegion.appendChild(dropRuleElem);
84 end
85
86 simElem.appendChild(blockingRegion);
87 end
88end
89
90% Now save explicit user-defined regions
91% JMT XML schema requires elements in specific order:
92% regionNode*, globalConstraint, globalMemoryConstraint,
93% classConstraint*, classMemoryConstraint*, dropRules*, classWeight*, classSize*, classSoftDeadline*
94for r=1:length(self.model.regions)
95 blockingRegion = simDoc.createElement('blockingRegion');
96 blockingRegion.setAttribute('name', ['FCRegion',num2str(r)]);
97 blockingRegion.setAttribute('type', 'default');
98
99 % Stations of this user-defined region, and the per-class station
100 % capacities that have to be merged into it. A JMT node belongs to at most
101 % one blocking region, so an overlapping synthetic region is not an option.
102 % Merging is faithful only when the region spans exactly the constrained
103 % station: a region constrains the jobs inside the WHOLE node set, so for a
104 % multi-node region no choice of maxJobsPerClass reproduces a per-station
105 % limit. The drop rule is shared by every constraint of a class within a
106 % region, so the region rule and the capacity rule must also agree.
107 regionStations = [];
108 for i=1:length(self.model.regions{r}.nodes)
109 istr = sn.nodeToStation(self.model.regions{r}.nodes{i}.index);
110 if istr > 0
111 regionStations(end+1) = istr; %#ok<AGROW>
112 covered(istr) = true;
113 end
114 end
115 regionClassCap = Inf(1,sn.nclasses);
116 for i=1:length(regionStations)
117 istr = regionStations(i);
118 for c=1:sn.nclasses
119 if ~isfinite(classCapCon(istr,c))
120 continue
121 end
122 if length(regionStations) > 1
123 line_error(mfilename, sprintf(['Station %s carries a finite capacity %d for class %s and also belongs to ', ...
124 'the multi-station finite capacity region FCRegion%d. JMT constrains a blocking region as a whole and ', ...
125 'allows a node to belong to only one region, so a per-station class capacity cannot be expressed ', ...
126 'alongside it. Remove the per-class capacity or shrink the region to that single station.'], ...
127 sn.nodenames{sn.stationToNode(istr)}, classCapCon(istr,c), sn.classnames{c}, r));
128 end
129 jmtClassCapAssert(sn, istr, c);
130 regionDrops = self.model.regions{r}.dropRule(c) == DropStrategy.DROP;
131 if ~regionDrops
132 line_error(mfilename, sprintf(['Station %s carries a finite capacity %d for class %s and also belongs to ', ...
133 'region FCRegion%d, whose drop rule for that class is "%s". A JMT blocking region admits a single drop ', ...
134 'rule per class, shared by all of its constraints, so the two cannot be merged: the per-class capacity ', ...
135 'of an open class is a loss constraint. Set the region drop rule for that class to DROP.'], ...
136 sn.nodenames{sn.stationToNode(istr)}, classCapCon(istr,c), sn.classnames{c}, r, ...
137 DropStrategy.toText(self.model.regions{r}.dropRule(c))));
138 end
139 regionClassCap(c) = classCapCon(istr,c);
140 end
141 end
142
143 % 1. regionNode elements
144 for i=1:length(self.model.regions{r}.nodes)
145 regionNode = simDoc.createElement('regionNode');
146 regionNode.setAttribute('nodeName', self.model.regions{r}.nodes{i}.getName);
147 blockingRegion.appendChild(regionNode);
148 end
149
150 % 2. globalConstraint
151 globalConstraint = simDoc.createElement('globalConstraint');
152 globalConstraint.setAttribute('maxJobs', num2str(self.model.regions{r}.globalMaxJobs));
153 blockingRegion.appendChild(globalConstraint);
154
155 % 3. globalMemoryConstraint
156 globalMemoryConstraint = simDoc.createElement('globalMemoryConstraint');
157 globalMemoryConstraint.setAttribute('maxMemory', num2str(self.model.regions{r}.globalMaxMemory));
158 blockingRegion.appendChild(globalMemoryConstraint);
159
160 % 4. All classConstraint elements (for all classes; JMT accepts constraints
161 % on zero-population closed classes, needed when jobs switch into them
162 % inside the region - cross-validated against exact CTMC/LDES)
163 for c=1:sn.nclasses
164 % The two constraints apply to the same node set here, so the tighter
165 % one subsumes the other and min() is exact.
166 classMaxJobs = self.model.regions{r}.classMaxJobs(c);
167 if classMaxJobs == Region.UNBOUNDED
168 classMaxJobs = regionClassCap(c);
169 else
170 classMaxJobs = min(classMaxJobs, regionClassCap(c));
171 end
172 if isfinite(classMaxJobs) && classMaxJobs ~= Region.UNBOUNDED
173 classConstraint = simDoc.createElement('classConstraint');
174 classConstraint.setAttribute('jobClass', self.model.regions{r}.classes{c}.getName);
175 classConstraint.setAttribute('maxJobsPerClass', num2str(classMaxJobs));
176 blockingRegion.appendChild(classConstraint);
177 end
178 end
179
180 % 5. All classMemoryConstraint elements (for all classes)
181 for c=1:sn.nclasses
182 if self.model.regions{r}.classMaxMemory(c) ~= Region.UNBOUNDED
183 classMemoryConstraint = simDoc.createElement('classMemoryConstraint');
184 classMemoryConstraint.setAttribute('jobClass', self.model.regions{r}.classes{c}.getName);
185 classMemoryConstraint.setAttribute('maxMemoryPerClass', num2str(self.model.regions{r}.classMaxMemory(c)));
186 blockingRegion.appendChild(classMemoryConstraint);
187 end
188 end
189
190 % 6. All dropRules elements (for all classes)
191 for c=1:sn.nclasses
192 % Always write dropRules element - JMT defaults to drop when not specified
193 dropRuleElem = simDoc.createElement('dropRules');
194 dropRuleElem.setAttribute('jobClass', self.model.regions{r}.classes{c}.getName);
195 if self.model.regions{r}.dropRule(c) == DropStrategy.DROP
196 dropRuleElem.setAttribute('dropThisClass', 'true');
197 else
198 dropRuleElem.setAttribute('dropThisClass', 'false');
199 end
200 blockingRegion.appendChild(dropRuleElem);
201 end
202
203 % 7. classWeight elements (drive the JMT 'FCR Capacity' measure, i.e. the
204 % weighted occupation / Total Weight)
205 for c=1:sn.nclasses
206 if self.model.regions{r}.classWeight(c) ~= 1
207 classWeightElem = simDoc.createElement('classWeight');
208 classWeightElem.setAttribute('jobClass', self.model.regions{r}.classes{c}.getName);
209 classWeightElem.setAttribute('weight', num2str(self.model.regions{r}.classWeight(c)));
210 blockingRegion.appendChild(classWeightElem);
211 end
212 end
213
214 % 8. All classSize elements (for all classes)
215 for c=1:sn.nclasses
216 if self.model.regions{r}.classSize(c) ~= 1
217 classSizeElem = simDoc.createElement('classSize');
218 classSizeElem.setAttribute('jobClass', self.model.regions{r}.classes{c}.getName);
219 classSizeElem.setAttribute('size', num2str(self.model.regions{r}.classSize(c)));
220 blockingRegion.appendChild(classSizeElem);
221 end
222 end
223
224 simElem.appendChild(blockingRegion);
225end
226
227% Lastly, export the per-class station capacities that no region above already
228% carries, each as a synthetic single-station blocking region. The region node
229% set is the constrained station alone, so the region occupation of class c is
230% by construction the number of class-c jobs at that station, which is exactly
231% what sn.classcap bounds in State.afterEventStation. The global constraint is
232% left unbounded: the station total sn.cap is already exported as the Queue
233% "size" (see saveBufferCapacity), and refreshCapacity guarantees
234% classcap(i,r) <= cap(i), so the region can never contradict the total.
235classCapRegionIdx = 0;
236for ist = 1:sn.nstations
237 if covered(ist) || ~any(isfinite(classCapCon(ist,:)))
238 continue
239 end
240 classCapRegionIdx = classCapRegionIdx + 1;
241 ind = sn.stationToNode(ist);
242
243 blockingRegion = simDoc.createElement('blockingRegion');
244 blockingRegion.setAttribute('name', ['ClassCapRegion',num2str(classCapRegionIdx)]);
245 blockingRegion.setAttribute('type', 'default');
246
247 regionNode = simDoc.createElement('regionNode');
248 regionNode.setAttribute('nodeName', sn.nodenames{ind});
249 blockingRegion.appendChild(regionNode);
250
251 globalConstraint = simDoc.createElement('globalConstraint');
252 globalConstraint.setAttribute('maxJobs', '-1');
253 blockingRegion.appendChild(globalConstraint);
254
255 globalMemoryConstraint = simDoc.createElement('globalMemoryConstraint');
256 globalMemoryConstraint.setAttribute('maxMemory', '-1');
257 blockingRegion.appendChild(globalMemoryConstraint);
258
259 % Validate every constrained class before emitting anything, so that an
260 % inexpressible capacity errors out instead of half-writing a region.
261 for c = 1:sn.nclasses
262 if isfinite(classCapCon(ist,c))
263 jmtClassCapAssert(sn, ist, c);
264 end
265 end
266
267 for c = 1:sn.nclasses
268 if isfinite(classCapCon(ist,c))
269 classConstraint = simDoc.createElement('classConstraint');
270 classConstraint.setAttribute('jobClass', sn.classnames{c});
271 classConstraint.setAttribute('maxJobsPerClass', num2str(classCapCon(ist,c)));
272 blockingRegion.appendChild(classConstraint);
273 end
274 end
275
276 % Only the constrained classes need a drop rule: an unconstrained class has
277 % neither a class constraint nor a global one, so BlockingRegion.isBlocked
278 % is always false for it and its flag would never be read. Every constraint
279 % emitted here is a loss constraint (jmtClassCapAssert rejects the rest).
280 for c = 1:sn.nclasses
281 if isfinite(classCapCon(ist,c))
282 dropRuleElem = simDoc.createElement('dropRules');
283 dropRuleElem.setAttribute('jobClass', sn.classnames{c});
284 dropRuleElem.setAttribute('dropThisClass', 'true');
285 blockingRegion.appendChild(dropRuleElem);
286 end
287 end
288
289 simElem.appendChild(blockingRegion);
290end
291end
292
293function classCapCon = jmtClassCapCon(sn)
294% CLASSCAPCON = JMTCLASSCAPCON(SN)
295%
296% Per-class station capacities that must be exported as JMT blocking-region
297% class constraints. classCapCon(i,r) is finite only when sn.classcap(i,r) is a
298% genuine buffer limit that the rest of the exported model does not already
299% imply; it is Inf otherwise, so that models without a per-class capacity emit
300% byte-identical XML.
301%
302% refreshCapacity derives classcap(i,r) as
303% min(chain population of r, station.classCap(r), station.cap)
304% so a value equal to the chain population or to the station total is not a
305% per-class buffer at all: the closed population, respectively the Queue "size"
306% written by saveBufferCapacity, already enforces it. Only a strictly tighter
307% value carries information that JMT would otherwise lose.
308classCapCon = Inf(sn.nstations, sn.nclasses);
309if isempty(sn.classcap)
310 return
311end
312
313% Population bound implied by the closed classes of each chain (Inf when open).
314chainpop = Inf(1, sn.nclasses);
315for c = 1:sn.nchains
316 inchain = sn.inchain{c};
317 chainpop(inchain) = sum(sn.njobs(inchain));
318end
319
320for ist = 1:sn.nstations
321 ind = sn.stationToNode(ist);
322 % A Source has no buffer, and a Place carries its per-class capacities in
323 % the JMT Storage section, which does have a capacities array.
324 if sn.nodetype(ind) == NodeType.Source || sn.nodetype(ind) == NodeType.Place
325 continue
326 end
327 for r = 1:sn.nclasses
328 % A class disabled at the station gets classcap 0 from refreshCapacity;
329 % it is not routed there, and a zero constraint is not a buffer limit.
330 if isnan(sn.rates(ist,r))
331 continue
332 end
333 % Both Inf and intmax mean "unbounded": MATLAB leaves classcap at Inf,
334 % while a struct marshalled from the JAR carries Integer.MAX_VALUE,
335 % which jline.util.Utils.isInf also reads as infinite.
336 if isfinite(sn.classcap(ist,r)) && sn.classcap(ist,r) < intmax && ...
337 sn.classcap(ist,r) < min(sn.cap(ist), chainpop(r))
338 classCapCon(ist,r) = sn.classcap(ist,r);
339 end
340 end
341end
342end
343
344function jmtClassCapAssert(sn, ist, r)
345% JMTCLASSCAPASSERT(SN, IST, R)
346%
347% Asserts that LINE's sn.classcap semantics for class r at station ist is
348% expressible as a JMT blocking-region constraint, raising a descriptive error
349% when it is not. Every constraint the caller emits is therefore a loss
350% constraint, i.e. dropThisClass="true".
351%
352% LINE enforces classcap in State.afterEventStation by not enabling the
353% arrival; sn.droprule is read there only to select BAS/BBS/RSRD blocking, so
354% WAITQ and DROP behave identically for a capacity limit. The resulting
355% reference semantics, verified against SolverCTMC, is
356% open class -> the arrival is lost, upstream is unaffected
357% closed class -> the job stays at the upstream station and the population is
358% conserved
359% which is the same predicate the CTMC analyzer applies in canDropClass. Only
360% the open case has a faithful blocking-region counterpart, dropThisClass=true:
361% JMT discards the arrival at the region input station, exactly as LINE does.
362%
363% The closed case is rejected rather than mapped to dropThisClass=false. JMT
364% does not hold a blocked job at the upstream station: it parks it in the
365% region's synthetic input station, where it belongs to no station and leaves
366% the upstream server free. Against SolverCTMC on a two-chain closed model that
367% costs the population count (sum of queue lengths 4.79 instead of 5) and moves
368% throughput by ~29%, so the region does not express this constraint at all.
369if ~isinf(sn.njobs(r))
370 line_error(mfilename, sprintf(['Station %s carries a finite capacity %d for closed class %s. LINE holds a blocked ', ...
371 'closed job at its upstream station, whereas JMT can only express a per-class capacity as a blocking region, ', ...
372 'which parks the job in the region input station instead, freeing the upstream server and losing it from the ', ...
373 'population count. SolverJMT therefore cannot reproduce this model; use SolverCTMC, SolverSSA or SolverLDES, ', ...
374 'or express the limit as the station capacity.'], sn.nodenames{sn.stationToNode(ist)}, ...
375 sn.classcap(ist,r), sn.classnames{r}));
376end
377% The blocking strategies below have no blocking-region counterpart either: a
378% region drops or defers the arrival, it cannot hold a job in the upstream
379% server.
380switch sn.droprule(ist,r)
381 case {DropStrategy.BAS, DropStrategy.BBS, DropStrategy.RSRD, ...
382 DropStrategy.RETRIAL, DropStrategy.RETRIAL_WITH_LIMIT}
383 line_error(mfilename, sprintf(['Station %s applies drop strategy "%s" to class %s and also carries a finite ', ...
384 'capacity %d for it. JMT exports a per-class capacity as a blocking region, which can only drop or defer an ', ...
385 'arrival and cannot reproduce that strategy. Remove the per-class capacity or use the station capacity ', ...
386 'instead, which is exported with its drop strategy.'], sn.nodenames{sn.stationToNode(ist)}, ...
387 DropStrategy.toText(sn.droprule(ist,r)), sn.classnames{r}, sn.classcap(ist,r)));
388end
389end
Definition Station.m:245