1function [simElem, simDoc] = saveRegions(self, simElem, simDoc)
2% [SIMELEM, SIMDOC] = SAVEREGIONS(SIMELEM, SIMDOC)
4% Copyright (c) 2012-2026, Imperial College London
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
13% see _kb/06-solver-catalog.md (Wrappers: JMT per-class capacity export) for the blockingRegion XML shape
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
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};
26 blockingRegion = simDoc.createElement(
'blockingRegion');
27 blockingRegion.setAttribute(
'name', [
'LPSRegion', num2str(lpsRegionIdx)]);
28 blockingRegion.setAttribute(
'type',
'default');
30 regionNode = simDoc.createElement(
'regionNode');
31 regionNode.setAttribute(
'nodeName', nodeName);
32 blockingRegion.appendChild(regionNode);
34 globalConstraint = simDoc.createElement(
'globalConstraint');
35 globalConstraint.setAttribute(
'maxJobs', num2str(lpsLimit));
36 blockingRegion.appendChild(globalConstraint);
38 globalMemoryConstraint = simDoc.createElement(
'globalMemoryConstraint');
39 globalMemoryConstraint.setAttribute(
'maxMemory',
'-1');
40 blockingRegion.appendChild(globalMemoryConstraint);
42 % see _kb/06-solver-catalog.md (Wrappers: JMT per-class capacity export, LPS row)
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}));
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);
62 simElem.appendChild(blockingRegion);
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');
73 % see _kb/06-solver-catalog.md (Wrappers: JMT per-class capacity export, single/multi-node FCR rows)
75 for i=1:length(self.model.regions{r}.nodes)
76 istr = sn.nodeToStation(self.model.regions{r}.nodes{i}.index);
78 regionStations(end+1) = istr; %#ok<AGROW>
82 regionClassCap = Inf(1,sn.nclasses);
83 for i=1:length(regionStations)
84 istr = regionStations(i);
86 if ~isfinite(classCapCon(istr,c))
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));
96 jmtClassCapAssert(sn, istr, c);
97 regionDrops = self.model.regions{r}.dropRule(c) == DropStrategy.DROP;
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))));
106 regionClassCap(c) = classCapCon(istr,c);
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);
117 % 2. globalConstraint
118 globalConstraint = simDoc.createElement(
'globalConstraint');
119 globalConstraint.setAttribute(
'maxJobs', num2str(self.model.regions{r}.globalMaxJobs));
120 blockingRegion.appendChild(globalConstraint);
122 % 3. globalMemoryConstraint
123 globalMemoryConstraint = simDoc.createElement(
'globalMemoryConstraint');
124 globalMemoryConstraint.setAttribute(
'maxMemory', num2str(self.model.regions{r}.globalMaxMemory));
125 blockingRegion.appendChild(globalMemoryConstraint);
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)
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);
136 classMaxJobs = min(classMaxJobs, regionClassCap(c));
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);
146 % 5. All classMemoryConstraint elements (
for all classes)
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);
156 % 6. All dropRules elements (
for all classes)
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');
164 dropRuleElem.setAttribute(
'dropThisClass',
'false');
166 blockingRegion.appendChild(dropRuleElem);
169 % 7. classWeight elements (drive the JMT
'FCR Capacity' measure, i.e. the
170 % weighted occupation / Total Weight)
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);
180 % 8. All classSize elements (
for all classes)
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);
190 simElem.appendChild(blockingRegion);
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,:)))
199 classCapRegionIdx = classCapRegionIdx + 1;
200 ind = sn.stationToNode(ist);
202 blockingRegion = simDoc.createElement(
'blockingRegion');
203 blockingRegion.setAttribute(
'name', [
'ClassCapRegion',num2str(classCapRegionIdx)]);
204 blockingRegion.setAttribute(
'type',
'default');
206 regionNode = simDoc.createElement(
'regionNode');
207 regionNode.setAttribute(
'nodeName', sn.nodenames{ind});
208 blockingRegion.appendChild(regionNode);
210 globalConstraint = simDoc.createElement(
'globalConstraint');
211 globalConstraint.setAttribute(
'maxJobs',
'-1');
212 blockingRegion.appendChild(globalConstraint);
214 globalMemoryConstraint = simDoc.createElement(
'globalMemoryConstraint');
215 globalMemoryConstraint.setAttribute(
'maxMemory',
'-1');
216 blockingRegion.appendChild(globalMemoryConstraint);
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);
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);
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);
245 simElem.appendChild(blockingRegion);
249function classCapCon = jmtClassCapCon(sn)
250% CLASSCAPCON = JMTCLASSCAPCON(SN)
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
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)
269% Population bound implied by the closed classes of each chain (Inf when open).
270chainpop = Inf(1, sn.nclasses);
272 inchain = sn.inchain{c};
273 chainpop(inchain) = sum(sn.njobs(inchain));
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
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))
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);
300function jmtClassCapAssert(sn, ist, r)
301% JMTCLASSCAPASSERT(SN, IST, R)
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".
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
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.
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}));
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
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)));