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