1function [simElem, simDoc] = saveRegions(self, simElem, simDoc)
2% [SIMELEM, SIMDOC] = SAVEREGIONS(SIMELEM, SIMDOC)
4% Copyright (c) 2012-2026, Imperial College London
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
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"/>
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
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};
43 blockingRegion = simDoc.createElement(
'blockingRegion');
44 blockingRegion.setAttribute(
'name', [
'LPSRegion', num2str(lpsRegionIdx)]);
45 blockingRegion.setAttribute(
'type',
'default');
47 regionNode = simDoc.createElement(
'regionNode');
48 regionNode.setAttribute(
'nodeName', nodeName);
49 blockingRegion.appendChild(regionNode);
51 globalConstraint = simDoc.createElement(
'globalConstraint');
52 globalConstraint.setAttribute(
'maxJobs', num2str(lpsLimit));
53 blockingRegion.appendChild(globalConstraint);
55 globalMemoryConstraint = simDoc.createElement(
'globalMemoryConstraint');
56 globalMemoryConstraint.setAttribute(
'maxMemory',
'-1');
57 blockingRegion.appendChild(globalMemoryConstraint);
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).
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}));
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);
86 simElem.appendChild(blockingRegion);
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');
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.
108 for i=1:length(self.model.regions{r}.nodes)
109 istr = sn.nodeToStation(self.model.regions{r}.nodes{i}.index);
111 regionStations(end+1) = istr; %#ok<AGROW>
112 covered(istr) =
true;
115 regionClassCap = Inf(1,sn.nclasses);
116 for i=1:length(regionStations)
117 istr = regionStations(i);
119 if ~isfinite(classCapCon(istr,c))
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));
129 jmtClassCapAssert(sn, istr, c);
130 regionDrops = self.model.regions{r}.dropRule(c) == DropStrategy.DROP;
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))));
139 regionClassCap(c) = classCapCon(istr,c);
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);
150 % 2. globalConstraint
151 globalConstraint = simDoc.createElement(
'globalConstraint');
152 globalConstraint.setAttribute(
'maxJobs', num2str(self.model.regions{r}.globalMaxJobs));
153 blockingRegion.appendChild(globalConstraint);
155 % 3. globalMemoryConstraint
156 globalMemoryConstraint = simDoc.createElement(
'globalMemoryConstraint');
157 globalMemoryConstraint.setAttribute(
'maxMemory', num2str(self.model.regions{r}.globalMaxMemory));
158 blockingRegion.appendChild(globalMemoryConstraint);
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)
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);
170 classMaxJobs = min(classMaxJobs, regionClassCap(c));
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);
180 % 5. All classMemoryConstraint elements (
for all classes)
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);
190 % 6. All dropRules elements (
for all classes)
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');
198 dropRuleElem.setAttribute(
'dropThisClass',
'false');
200 blockingRegion.appendChild(dropRuleElem);
203 % 7. classWeight elements (drive
the JMT
'FCR Capacity' measure, i.e.
the
204 % weighted occupation / Total Weight)
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);
214 % 8. All classSize elements (
for all classes)
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);
224 simElem.appendChild(blockingRegion);
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,:)))
240 classCapRegionIdx = classCapRegionIdx + 1;
241 ind = sn.stationToNode(ist);
243 blockingRegion = simDoc.createElement(
'blockingRegion');
244 blockingRegion.setAttribute(
'name', [
'ClassCapRegion',num2str(classCapRegionIdx)]);
245 blockingRegion.setAttribute(
'type',
'default');
247 regionNode = simDoc.createElement(
'regionNode');
248 regionNode.setAttribute(
'nodeName', sn.nodenames{ind});
249 blockingRegion.appendChild(regionNode);
251 globalConstraint = simDoc.createElement(
'globalConstraint');
252 globalConstraint.setAttribute(
'maxJobs',
'-1');
253 blockingRegion.appendChild(globalConstraint);
255 globalMemoryConstraint = simDoc.createElement(
'globalMemoryConstraint');
256 globalMemoryConstraint.setAttribute(
'maxMemory',
'-1');
257 blockingRegion.appendChild(globalMemoryConstraint);
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);
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);
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);
289 simElem.appendChild(blockingRegion);
293function classCapCon = jmtClassCapCon(sn)
294% CLASSCAPCON = JMTCLASSCAPCON(SN)
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
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)
313% Population bound implied by
the closed classes of each chain (Inf when open).
314chainpop = Inf(1, sn.nclasses);
316 inchain = sn.inchain{c};
317 chainpop(inchain) = sum(sn.njobs(inchain));
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
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))
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);
344function jmtClassCapAssert(sn, ist, r)
345% JMTCLASSCAPASSERT(SN, IST, R)
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".
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
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.
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}));
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
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)));