LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
jmt_writer.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_IO_JMT_WRITER_H
6#define LINE_IO_JMT_WRITER_H
7
8/**
9 * @file
10 * @ingroup line_io
11 * Port of `@@JMTIO`: a refreshed `NetworkStruct` written out as a JMT `.jsimg`
12 * simulation model.
13 *
14 * THE REFERENCE WALKS NODE OBJECTS; THIS PORT WALKS THE STRUCT. MATLAB's
15 * `writeJSIM` iterates `model.nodes{i}` and asks each for its three SECTIONS
16 * (input, server, output), which are objects the node's constructor installed.
17 * This port has no node objects -- `network_builder` produces a `NetworkStruct`
18 * directly -- so `jmt_sections` re-derives the same triple from the node type
19 * and the scheduling strategy, transcribing the constructors of `Queue.m`,
20 * `Source.m`, `Sink.m`, `Router.m`, `ClassSwitch.m`, `Cache.m`, `Logger.m`,
21 * `Fork.m`, `Join.m`, `Place.m` and `Transition.m`. That table is the one place
22 * where this port can drift from the reference without a compiler error, so it
23 * names its source file for each row.
24 *
25 * WHAT IS DELIBERATELY REPRODUCED RATHER THAN CORRECTED. Two places in the
26 * reference emit less than they appear to, and all four codebases agree on the
27 * result, so this port agrees with them and says so at the site:
28 * - `saveForkStrategy` emits ONE OutPath entry (the last connected node),
29 * not one per branch -- harmless only because `isSimplifiedFork` is true.
30 * - the analytic `MMPP2Par` branch of `saveServiceStrategy` is unreachable,
31 * because the MAP branch catches MMPP2 first.
32 * Changing either here would make the C++ row the odd one out, which is the
33 * opposite of what a port is for.
34 */
35
36#include <algorithm>
37#include <cmath>
38#include <iostream>
39#include <limits>
40#include <map>
41#include <memory>
42#include <set>
43#include <string>
44#include <vector>
45
46#include "line/io/jmt_dist.h"
48#include "line/util/error.h"
49#include "line/util/xml.h"
50
51namespace line {
52namespace io {
53
55using lang::NodeType;
60
61/**
62 * The simulation controls the JSIM header carries, MATLAB's `JMTIO` properties.
63 *
64 * `max_events = -1` and `sim_conf_int = 0.99` / `sim_max_rel_err = 0.03` are the
65 * reference's constructor defaults; `max_samples` is `options.samples`, which
66 * `runAnalyzer` raises to 5000 before it reaches here.
67 */
69 std::string file_name = "model"; ///< base name; the header echoes it plus `.jsimg`
70 std::string log_path; ///< `model.getLogPath`, the `logPath` attribute
71 long seed = 23000;
72 double max_samples = 10000.0;
73 double max_events = -1.0;
74 double max_simulated_time = std::numeric_limits<double>::infinity();
75 double sim_conf_int = 0.99;
76 double sim_max_rel_err = 0.03;
77};
78
79/**
80 * `sn.connmatrix`: which ordered pairs of nodes the model LINKED.
81 *
82 * The reference carries the matrix on the struct, recorded by `Network.addLink`.
83 * This port records the links only through the routing blocks `P`, so the
84 * connection set is their union support. That is the same set for every model
85 * built the way `link(P)` builds one -- `addLink` is called from `link` for each
86 * nonzero entry -- and it is what the JSIM `<connection>` list, the ClassSwitch
87 * row sum, the WRROBIN and PROB destination lists and the SPN input/output
88 * vectors all read.
89 *
90 * `Peff` IS NOT USED HERE, deliberately: it has the RAND/RROBIN expansion and
91 * the class-switch folding applied, so a Router's declared links would come
92 * back as the links of the stations it routes to, and the exported topology
93 * would not be the user's.
94 */
95template <class T>
96std::vector<std::vector<bool>> jmt_conn_matrix(const qn::NetworkStruct<T>& sn) {
97 const std::size_t I = sn.nodes.size();
98 std::vector<std::vector<bool>> C(I, std::vector<bool>(I, false));
99 const T zero = num_traits<T>::from_int(0);
100 for (const auto& kv : sn.P) {
101 const Matrix<T>& B = kv.second;
102 for (std::size_t a = 0; a < B.rows() && a < I; ++a)
103 for (std::size_t b = 0; b < B.cols() && b < I; ++b)
104 if (!(B(a, b) == zero)) C[a][b] = true;
105 }
106 return C;
107}
108
109/**
110 * Port of `getExportableClasses`.
111 *
112 * A closed class with no customers is dropped from the exported model UNLESS
113 * jobs can still reach it: as a cache hit/miss class, or through a class switch
114 * from a populated class of the same chain. Exporting it anyway would give JMT
115 * a class it can never see and, for a closed class, a reference station with
116 * zero population -- which JMT reports as an unreachable measure rather than as
117 * an empty one.
118 */
119template <class T>
121 const std::size_t K = sn.nclasses;
122 std::set<std::size_t> cache_classes; // 1-based
123 for (const auto& kv : sn.nodeparam) {
124 const qn::CacheParam<T>& cp = kv.second;
125 for (std::size_t r = 0; r < cp.hitclass.size(); ++r)
126 if (cp.hitclass[r] > 0) cache_classes.insert(cp.hitclass[r]);
127 for (std::size_t r = 0; r < cp.missclass.size(); ++r)
128 if (cp.missclass[r] > 0) cache_classes.insert(cp.missclass[r]);
129 }
130 std::set<std::size_t> switch_classes;
131 for (std::size_t c = 0; c < sn.chains.size(); ++c) {
132 std::vector<std::size_t> in;
133 for (std::size_t r = 0; r < K; ++r)
134 if (sn.chains[c][r]) in.push_back(r + 1);
135 if (in.size() <= 1) continue;
136 bool has_jobs = false;
137 for (std::size_t r : in)
138 if (sn.classes[r - 1].population > 0.0) has_jobs = true;
139 if (has_jobs)
140 for (std::size_t r : in) switch_classes.insert(r);
141 }
142 std::vector<bool> keep(K, true);
143 for (std::size_t r = 1; r <= K; ++r) {
144 const double n = sn.classes[r - 1].population;
145 if (std::isfinite(n) && n == 0.0 && !cache_classes.count(r) && !switch_classes.count(r))
146 keep[r - 1] = false;
147 }
148 return keep;
149}
150
151/**
152 * The three JMT section class names of a node: input, server, output.
153 *
154 * An empty entry is a section the node does not have -- only a Sink, whose
155 * `getSections` returns `{'', JobSink, ''}`. The names are LINE's section class
156 * names; `jmt_write_jsim` overwrites the ones JMT spells differently, exactly
157 * where the reference does.
158 */
159template <class T>
161 std::string input, server, output;
162};
163
164template <class T>
167 const qn::NodeDef& nd = sn.nodes[ind - 1];
168 const std::size_t ist = nd.station;
169 switch (nd.nodetype) {
170 case NodeType::Source: // Source.m:66-68
171 s.input = "RandomSource";
172 s.server = "ServiceTunnel";
173 s.output = "Dispatcher";
174 break;
175 case NodeType::Sink: // Sink.m:28, getSections
176 s.server = "JobSink";
177 break;
178 case NodeType::Router: // Router.m:62-66
179 s.input = "Buffer";
180 s.server = "ServiceTunnel";
181 s.output = "Dispatcher";
182 break;
183 case NodeType::ClassSwitch: // ClassSwitch.m:32-38
184 s.input = "Buffer";
185 s.server = "StatelessClassSwitcher";
186 s.output = "Dispatcher";
187 break;
188 case NodeType::Cache: // Cache.m:57-58; the server section is a
189 s.input = "Buffer"; // CacheClassSwitcher whose className is 'Cache'
190 s.server = "Cache";
191 s.output = "Dispatcher";
192 break;
193 case NodeType::Logger: // Logger.m:40-47
194 s.input = "Buffer";
195 s.server = "LogTunnel";
196 s.output = "Dispatcher";
197 break;
198 case NodeType::Fork: // Fork.m:106-109
199 s.input = "Buffer";
200 s.server = "ServiceTunnel";
201 s.output = "Forker";
202 break;
203 case NodeType::Join: // Join.m:34-36
204 s.input = "Joiner";
205 s.server = "ServiceTunnel";
206 s.output = "Dispatcher";
207 break;
208 case NodeType::Transition: // Transition.m:29-36
209 s.input = "Enabling";
210 s.server = "Timing";
211 s.output = "Firing";
212 break;
213 case NodeType::Place: // Place.m:27-31 and installQueueServer
214 s.input = "Storage";
215 s.output = "Linkage";
216 s.server = "ServiceTunnel";
217 if (ist != 0) {
218 bool queueing = false;
219 for (std::size_t r = 0; r < sn.nclasses; ++r)
220 if (!sn.service[ist - 1][r].disabled) queueing = true;
221 if (queueing) {
222 const SchedStrategy sch = sn.stations[ist - 1].sched;
223 if (sch == SchedStrategy::INF)
224 s.server = "InfiniteServer";
225 else if (sch == SchedStrategy::PS || sch == SchedStrategy::DPS ||
226 sch == SchedStrategy::GPS || sch == SchedStrategy::LPS)
227 s.server = "SharedServer";
228 else
229 s.server = "Server";
230 }
231 }
232 break;
233 case NodeType::Queue:
234 case NodeType::Delay:
235 default: { // Queue.m:48-53 and the schedStrategy switch at :79-110
236 s.input = "Buffer";
237 s.output = "Dispatcher";
238 const SchedStrategy sch =
239 ist != 0 ? sn.stations[ist - 1].sched : SchedStrategy::FCFS;
240 switch (sch) {
241 case SchedStrategy::PS:
242 case SchedStrategy::DPS:
243 case SchedStrategy::GPS:
244 case SchedStrategy::PSPRIO:
245 case SchedStrategy::DPSPRIO:
246 case SchedStrategy::GPSPRIO:
247 case SchedStrategy::LPS:
248 s.server = "SharedServer";
249 break;
250 case SchedStrategy::LCFSPR:
251 case SchedStrategy::LCFSPRPRIO:
252 case SchedStrategy::FCFSPR:
253 case SchedStrategy::FCFSPRPRIO:
254 case SchedStrategy::LCFSPI:
255 case SchedStrategy::LCFSPIPRIO:
256 case SchedStrategy::FCFSPI:
257 case SchedStrategy::FCFSPIPRIO:
258 case SchedStrategy::EDF:
259 s.server = "PreemptiveServer";
260 break;
261 case SchedStrategy::INF:
262 s.server = "InfiniteServer";
263 break;
264 case SchedStrategy::POLLING:
265 s.server = "PollingServer";
266 break;
267 default:
268 s.server = "Server";
269 break;
270 }
271 break;
272 }
273 }
274 return s;
275}
276
277// ---------------------------------------------------------------------------
278// The metric handles, MATLAB `@@MNetwork/getAvgHandles.m`
279// ---------------------------------------------------------------------------
280
281/** The measure kinds `saveMetrics` requests, in the order it requests them. */
282enum class JmtMetricKind { QLen, Util, RespT, Tput, ArvR, Tard, SysTard };
283
284/** The JMT `type` attribute, MATLAB `MetricType.toText`. */
285inline const char* jmt_metric_text(JmtMetricKind k) {
286 switch (k) {
287 case JmtMetricKind::QLen: return "Number of Customers";
288 case JmtMetricKind::Util: return "Utilization";
289 case JmtMetricKind::RespT: return "Response Time";
290 case JmtMetricKind::Tput: return "Throughput";
291 case JmtMetricKind::ArvR: return "Arrival Rate";
292 case JmtMetricKind::Tard: return "Tardiness";
293 case JmtMetricKind::SysTard: return "System Tardiness";
294 }
295 return "Unknown Metric";
296}
297
298/**
299 * Port of the `disabled` rules in `getAvgHandles`, per (station, class).
300 *
301 * The rules are per kind: a Source or a Sink reports no queue length, response
302 * time, utilization or tardiness but DOES report throughput and arrival rate;
303 * a Fork or a Join reports no utilization; a station whose class has no service
304 * process reports nothing, EXCEPT that a cache hit/miss class keeps its
305 * throughput and arrival rate -- a job only ever passes through such a class,
306 * it is never served in it, and disabling the measure would leave the cache's
307 * hit rate unobservable.
308 *
309 * `has_service_tunnel` reproduces the reference's test on the SECTION object:
310 * a Source, a Join and an ordinary Place have a ServiceTunnel and are therefore
311 * exempt from the service-defined test entirely.
312 */
313template <class T>
314bool jmt_metric_enabled(const qn::NetworkStruct<T>& sn, JmtMetricKind kind, std::size_t ist,
315 std::size_t r, const std::vector<bool>& is_cache_class) {
316 const std::size_t ind = sn.station_to_node[ist - 1];
317 const NodeType ty = sn.nodes[ind - 1].nodetype;
318 const bool is_source = ty == NodeType::Source;
319 const bool is_sink = ty == NodeType::Sink;
320 const JmtSections<T> sec = jmt_sections(sn, ind);
321 const bool tunnel = sec.server == "ServiceTunnel" || sec.server == "JobSink";
322 const bool service_defined = !sn.disabled[ist - 1][r - 1];
323
324 switch (kind) {
328 if (is_source || is_sink) return false;
329 return tunnel || service_defined;
331 if (is_source || is_sink) return false;
332 if (ty == NodeType::Join || ty == NodeType::Fork) return false;
333 return tunnel || service_defined;
336 if (tunnel) return true;
337 return service_defined || is_cache_class[r - 1];
339 return true;
340 }
341 return false;
342}
343
344/** The classes a Cache switches jobs into; they keep their Tput/ArvR measures. */
345template <class T>
346std::vector<bool> jmt_cache_classes(const qn::NetworkStruct<T>& sn) {
347 std::vector<bool> f(sn.nclasses, false);
348 for (const auto& kv : sn.nodeparam) {
349 const qn::CacheParam<T>& cp = kv.second;
350 for (std::size_t r = 0; r < cp.hitclass.size(); ++r)
351 if (cp.hitclass[r] > 0 && cp.hitclass[r] <= sn.nclasses) f[cp.hitclass[r] - 1] = true;
352 for (std::size_t r = 0; r < cp.missclass.size(); ++r)
353 if (cp.missclass[r] > 0 && cp.missclass[r] <= sn.nclasses) f[cp.missclass[r] - 1] = true;
354 }
355 return f;
356}
357
358// ---------------------------------------------------------------------------
359// Small shared emitters
360// ---------------------------------------------------------------------------
361
362/** `<parameter array="true" classPath="CP" name="NAME">` */
363inline xml::Element& jmt_param(xml::Element& section, const char* class_path, const char* name,
364 bool array) {
365 xml::Element& p = section.add_child("parameter");
366 if (array) p.set_attr("array", "true");
367 p.set_attr("classPath", class_path);
368 p.set_attr("name", name);
369 return p;
370}
371
372/** `<parameter classPath="CP" name="NAME"><value>V</value></parameter>` */
373inline void jmt_param_value(xml::Element& section, const char* class_path, const char* name,
374 const std::string& value) {
375 xml::Element& p = jmt_param(section, class_path, name, false);
376 p.add_text_child("value", value);
377}
378
379/** `<refClass>NAME</refClass>`, the per-class marker of an array parameter. */
380inline void jmt_ref_class(xml::Element& parent, const std::string& class_name) {
381 parent.add_text_child("refClass", class_name);
382}
383
384/** MATLAB `DropStrategy.toText`, the strings JMT's dropRule field expects. */
385inline const char* jmt_drop_text(DropStrategy d) {
386 switch (d) {
387 case DropStrategy::WAITQ: return "waiting queue";
388 case DropStrategy::DROP: return "drop";
389 case DropStrategy::BAS: return "BAS blocking";
390 case DropStrategy::BBS: return "BBS blocking";
391 case DropStrategy::RSRD: return "RSRD blocking";
392 case DropStrategy::RETRIAL: return "retrial";
393 case DropStrategy::RETRIAL_WITH_LIMIT: return "retrial with limit";
394 }
395 throw InputError("SolverJMT: unrecognized drop strategy");
396}
397
398/**
399 * Whether JMT's queue section can read this drop strategy at all.
400 *
401 * It recognizes exactly four `dropStrategies` strings -- 'drop', 'BAS blocking',
402 * 'waiting queue', 'retrial' (a lookupswitch on String.hashCode in
403 * `jmt/engine/NodeSections/Queue.class`; the Storage section of a Place is even
404 * narrower and drops 'retrial'). An unrecognized value falls through the default arm
405 * with NO flag set, so BBS, RSRD and retrial-with-limit are not approximated, they are
406 * IGNORED.
407 */
408inline bool jmt_reads_drop(DropStrategy d) {
409 return d == DropStrategy::DROP || d == DropStrategy::BAS || d == DropStrategy::WAITQ ||
410 d == DropStrategy::RETRIAL;
411}
412
413/** MATLAB `HeteroSchedPolicy.toJMTText`: JMT's long descriptive identifiers. */
415 switch (p) {
416 case lang::HeteroSchedPolicy::ORDER: return "Order (Assign according to order below)";
417 case lang::HeteroSchedPolicy::ALIS: return "ALIS (Assign Longest Idle Server)";
418 case lang::HeteroSchedPolicy::ALFS: return "ALFS (Assign Least Flexible Server)";
419 case lang::HeteroSchedPolicy::FAIRNESS: return "Fairness (Move back server type when used)";
420 case lang::HeteroSchedPolicy::FSF: return "FSF (Fastest Servers First)";
421 case lang::HeteroSchedPolicy::RAIS: return "RAIS (Random Assignment to Idle Servers)";
422 }
423 return "Order (Assign according to order below)";
424}
425
426/**
427 * The writer itself. One instance per exported model; it holds the derived
428 * tables (connections, exportable classes, cache classes) that nearly every
429 * handler reads, so they are computed once rather than per section as the
430 * reference recomputes them.
431 */
432template <class T>
434public:
436 : sn_(sn),
437 opt_(opt),
438 conn_(jmt_conn_matrix(sn)),
440 cacheclass_(jmt_cache_classes(sn)) {}
441
442 /** Port of `@@JMTIO/writeJSIM.m`; returns the serialized document. */
443 std::string write_jsim() {
444 std::unique_ptr<xml::Element> sim = xml::element("sim");
445 save_xml_header(*sim);
446 save_classes(*sim);
447 for (std::size_t ind = 1; ind <= sn_.nodes.size(); ++ind) save_node(*sim, ind);
448 save_metrics(*sim);
449 save_links(*sim);
450 save_regions(*sim);
451 save_preload(*sim);
452 return xml::serialize(*sim);
453 }
454
455 /**
456 * The buffer-capacity refusals of `save_buffer_capacity`, as a SENTENCE
457 * rather than an exception; empty when every buffer is exportable.
458 *
459 * ONE PREDICATE, TWO CALLERS. `save_buffer_capacity` raises it while writing
460 * the JSIM document, and `jmt::jmt_method_refusal` returns it so that
461 * `findSolver` never offers a jmt row on a model the writer will refuse. It
462 * was reachable only from inside the writer, which is why the gate could not
463 * see it and offered `jmt.jsim` on a binding buffer.
464 *
465 * WHAT MAKES A BUFFER BIND is not that `sn.cap` is finite: `refresh_capacity`
466 * DERIVES a finite cap for every station nobody capped. It is that the cap is
467 * strictly below the population that can REACH the station, and an
468 * infinite-server station has no buffer at all -- the same two tests
469 * `save_buffer_capacity` applies before it exports anything.
470 *
471 * `jmva_engine` selects the verdict, because the two engines fail a binding
472 * buffer for OPPOSITE reasons. JSIM exports it whenever JMT can read the drop
473 * rule, so only `assert_station_cap_exportable`'s cases go. JMVA has no
474 * capacity element in its document at all -- `write_jmva` emits a station
475 * type, a per-chain demand and a per-chain visit count and nothing else -- so
476 * ANY binding buffer would be solved as if it were unbounded: measured on a
477 * closed Delay+FCFS model, N=4, cap 2, every jmva method reported 2.19 jobs
478 * at a station that can hold 2, against the exact 1.33.
479 */
480 std::string buffer_capacity_refusal(bool jmva_engine) const {
481 for (std::size_t ist = 1; ist <= sn_.nstations; ++ist) {
482 // A SOURCE AND A SINK HAVE NO BUFFER THAT CAN BIND. The Source IS the
483 // external world and the Sink absorbs, so neither ever holds a job a
484 // capacity could refuse, yet `refresh_capacity` writes them a row like
485 // any other station. Excluded on NODE TYPE, as
486 // `qn::binding_capacity_reason` excludes them, and not by name.
487 const qn::NodeType ty = sn_.nodes[sn_.station_to_node[ist - 1] - 1].nodetype;
488 if (ty == qn::NodeType::Source || ty == qn::NodeType::Sink) continue;
489 // UNBOUNDED IS inf HERE, `Station<T>::cap` defaulting to infinity.
490 // The JAR cannot: its Station.cap is an int whose "no bound" value is
491 // Integer.MAX_VALUE, and refreshCapacity SUMS that sentinel across
492 // the classes served, so a mixed station comes out as
493 // 2147483647 + N there and needs SaveHandlers.jmtCapIsUnbounded.
494 if (!std::isfinite(sn_.cap[ist - 1])) continue;
495 if (sn_.cap[ist - 1] >= reachable_population(ist)) continue;
496 if (!std::isfinite(sn_.stations[ist - 1].nservers)) continue;
497 if (jmva_engine)
498 return "SolverJMT: station '" + nname(sn_.station_to_node[ist - 1]) +
499 "' carries a finite capacity " + jmt_int(sn_.cap[ist - 1]) +
500 " that binds. The JMVA document has no capacity element at all, so the "
501 "analytical engine would solve the model as if the buffer were unbounded "
502 "and report that as the answer. Use the 'jsim' method, which exports the "
503 "buffer with its drop rule when JMT can express it, or SolverCTMC, "
504 "SolverSSA or SolverLDES";
505 try {
506 assert_station_cap_exportable(ist);
507 } catch (const UnsupportedError& e) {
508 return std::string(e.what());
509 }
510 }
511 return std::string();
512 }
513
514private:
515 const qn::NetworkStruct<T>& sn_;
516 JmtWriteOptions opt_;
517 std::vector<std::vector<bool>> conn_;
518 std::vector<bool> keep_, cacheclass_;
519
520 double d(const T& x) const { return num_traits<T>::to_double(x); }
521 const std::string& cname(std::size_t r) const { return sn_.classes[r - 1].name; }
522 const std::string& nname(std::size_t i) const { return sn_.nodes[i - 1].name; }
523
524 /** The 1-based node indices `ind` is linked TO. */
525 std::vector<std::size_t> outputs_of(std::size_t ind) const {
526 std::vector<std::size_t> v;
527 for (std::size_t j = 1; j <= sn_.nodes.size(); ++j)
528 if (conn_[ind - 1][j - 1]) v.push_back(j);
529 return v;
530 }
531
532 /** The 1-based node indices linked TO `ind`. */
533 std::vector<std::size_t> inputs_of(std::size_t ind) const {
534 std::vector<std::size_t> v;
535 for (std::size_t j = 1; j <= sn_.nodes.size(); ++j)
536 if (conn_[j - 1][ind - 1]) v.push_back(j);
537 return v;
538 }
539
540 // -- header and classes ------------------------------------------------
541
542 /** Port of `saveXMLHeader`. */
543 void save_xml_header(xml::Element& sim) {
544 sim.set_attr("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
545 sim.set_attr("name", opt_.file_name + ".jsimg");
546 sim.set_attr("xsi:noNamespaceSchemaLocation", "SIMmodeldefinition.xsd");
547 sim.set_attr("disableStatisticStop", "true");
548 sim.set_attr("logDecimalSeparator", ".");
549 sim.set_attr("logDelimiter", ";");
550 sim.set_attr("logPath", opt_.log_path);
551 sim.set_attr("logReplaceMode", "0");
552 sim.set_attr("maxSamples",
553 std::isnan(opt_.max_samples) ? "10000" : jmt_int(opt_.max_samples));
554 sim.set_attr("maxEvents", jmt_int(opt_.max_events));
555 if (std::isfinite(opt_.max_simulated_time)) {
556 char buf[64];
557 std::snprintf(buf, sizeof(buf), "%.3f", opt_.max_simulated_time);
558 sim.set_attr("maxSimulated", buf);
559 }
560 sim.set_attr("polling", "1.0");
561 sim.set_attr("seed", jmt_int(static_cast<double>(opt_.seed)));
562 }
563
564 /**
565 * Port of `saveClasses`.
566 *
567 * THE PRIORITY IS INVERTED. LINE orders priorities with the SMALLEST value
568 * most urgent and JMT with the largest, so the exported value is
569 * `max(prio) - prio(r)`. Exporting the raw number reverses the service
570 * order of every priority model without any diagnostic.
571 */
572 void save_classes(xml::Element& sim) {
573 int maxprio = 0;
574 for (std::size_t r = 0; r < sn_.nclasses; ++r)
575 maxprio = std::max(maxprio, sn_.classes[r].prio);
576 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
577 if (!keep_[r - 1]) continue;
578 const qn::JobClass& cl = sn_.classes[r - 1];
579 xml::Element& uc = sim.add_child("userClass");
580 uc.set_attr("name", cl.name);
581 const bool open = !std::isfinite(cl.population);
582 uc.set_attr("type", open ? "open" : "closed");
583 // `sn.classdeadline` has no counterpart in this port's struct, so
584 // the soft deadline is the reference's "no deadline" value. EDD and
585 // EDF therefore export as the put strategies JMT does not yet
586 // implement either, which is the reference's own state.
587 uc.set_attr("softDeadline", "0.0");
588 uc.set_attr("priority", jmt_int(static_cast<double>(maxprio - cl.prio)));
589 const std::size_t refst = cl.refstat;
590 const std::string refname = nname(sn_.station_to_node[refst - 1]);
591 if (!open) {
592 uc.set_attr("customers", jmt_int(cl.population));
593 uc.set_attr("referenceSource", refname);
594 } else if (sn_.disabled[refst - 1][r - 1]) {
595 // An open class with no arrival process at its reference
596 // station enters the model by class switching only; JMT names
597 // that source 'ClassSwitch'.
598 uc.set_attr("referenceSource", "ClassSwitch");
599 } else {
600 uc.set_attr("referenceSource", refname);
601 }
602 }
603 }
604
605 // -- the node/section walk ---------------------------------------------
606
607 /** Port of the section loop of `writeJSIM`. */
608 void save_node(xml::Element& sim, std::size_t ind) {
609 const JmtSections<T> sec = jmt_sections(sn_, ind);
610 xml::Element& node = sim.add_child("node");
611 node.set_attr("name", nname(ind));
612 const std::string parts[3] = {sec.input, sec.server, sec.output};
613 for (int k = 0; k < 3; ++k) {
614 if (parts[k].empty()) continue;
615 std::string cls = parts[k];
616 // `writeJSIM` promotes a Server to a PreemptiveServer for the
617 // preemptive disciplines. `jmt_sections` already does that from the
618 // scheduling strategy, so the promotion is not repeated here; the
619 // remaining rewrites are the LINE-name to JMT-name ones.
620 xml::Element& xs = node.add_child("section");
621 xs.set_attr("className", cls);
622 if (cls == "Buffer") {
623 xs.set_attr("className", "Queue");
624 save_buffer_capacity(xs, ind);
625 save_drop_strategy(xs, ind);
626 if (has_retrial(ind)) {
627 // With retrial JMT selects a different Queue constructor,
628 // which takes the retrial distributions BEFORE the get and
629 // put strategies and takes no impatience at all.
630 save_retrial_distributions(xs, ind);
631 save_get_strategy(xs, ind);
632 save_put_strategy(xs, ind);
633 } else {
634 save_get_strategy(xs, ind);
635 save_put_strategy(xs, ind);
636 save_impatience(xs, ind);
637 }
638 } else if (cls == "Server") {
639 save_number_of_servers(xs, ind);
640 save_server_visits(xs);
641 save_service_strategy(xs, ind);
642 save_delay_off_strategy(xs, ind);
643 // Job parallelism and heterogeneous pools. SimLoader picks the Server
644 // constructor by the positional types of the parameters, so these five
645 // must follow the service strategies as one block.
646 save_class_parallelism(xs, ind);
647 save_server_type_names(xs, ind);
648 save_servers_per_type(xs, ind);
649 save_server_compatibilities(xs, ind);
650 save_hetero_sched_policy(xs, ind);
651 warn_hetero_rates(ind);
652 warn_switchover_on_non_polling(ind);
653 } else if (cls == "PreemptiveServer") {
654 save_number_of_servers(xs, ind);
655 save_server_visits(xs);
656 save_service_strategy(xs, ind);
657 save_delay_off_strategy(xs, ind);
658 } else if (cls == "SharedServer") {
659 xs.set_attr("className", "PSServer");
660 save_number_of_servers(xs, ind);
661 save_server_visits(xs);
662 save_service_strategy(xs, ind);
663 save_delay_off_strategy(xs, ind);
664 save_preemptive_strategy(xs, ind);
665 save_preemptive_weights(xs, ind);
666 } else if (cls == "InfiniteServer") {
667 xs.set_attr("className", "Delay");
668 save_service_strategy(xs, ind);
669 } else if (cls == "PollingServer") {
670 xs.set_attr("className", polling_server_class(ind));
671 save_number_of_servers(xs, ind);
672 save_server_visits(xs);
673 save_service_strategy(xs, ind);
674 save_switchover_strategy(xs, ind);
675 } else if (cls == "RandomSource") {
676 save_arrival_strategy(xs, ind);
677 } else if (cls == "Dispatcher") {
678 xs.set_attr("className", "Router");
679 save_routing_strategy(xs, ind);
680 } else if (cls == "StatelessClassSwitcher") {
681 xs.set_attr("className", "ClassSwitch");
682 save_class_switch_strategy(xs, ind);
683 } else if (cls == "Cache") {
684 save_cache_strategy(xs, ind);
685 } else if (cls == "LogTunnel") {
686 save_log_tunnel(xs, ind);
687 } else if (cls == "Joiner") {
688 xs.set_attr("className", "Join");
689 save_join_strategy(xs, ind);
690 } else if (cls == "Forker") {
691 xs.set_attr("className", "Fork");
692 save_fork_strategy(xs, ind);
693 } else if (cls == "Storage") {
694 save_total_capacity(xs, ind);
695 save_place_capacities(xs, ind);
696 save_drop_rule(xs, ind);
697 save_get_strategy(xs, ind);
698 save_put_strategies(xs, ind);
699 } else if (cls == "Enabling") {
700 save_enabling_conditions(xs, ind);
701 save_inhibiting_conditions(xs, ind);
702 } else if (cls == "Firing") {
703 save_firing_outcomes(xs, ind);
704 } else if (cls == "Timing") {
705 save_mode_names(xs, ind);
706 save_numbers_of_servers(xs, ind);
707 save_timing_strategies(xs, ind);
708 save_firing_priorities(xs, ind);
709 save_firing_weights(xs, ind);
710 }
711 // ServiceTunnel, JobSink and Linkage carry no parameters.
712 }
713 }
714
715 /** JMT's polling server class, by discipline; DECREMENTING has none. */
716 const char* polling_server_class(std::size_t ind) const {
717 const std::size_t ist = sn_.nodes[ind - 1].station;
718 const typename qn::NetworkStruct<T>::PollingParam pp = sn_.effective_polling(ist);
719 switch (pp.ptype) {
720 case PollingType::GATED: return "GatedPollingServer";
721 case PollingType::EXHAUSTIVE: return "ExhaustivePollingServer";
722 case PollingType::KLIMITED: return "LimitedPollingServer";
723 case PollingType::DECREMENTING:
724 throw UnsupportedError(
725 "SolverJMT: JMT does not support the decrementing (semiexhaustive) polling "
726 "discipline; use the LDES solver");
727 }
728 throw UnsupportedError("SolverJMT: unknown polling discipline");
729 }
730
731 /** True when any class of the station declares a retrial orbit. */
732 bool has_retrial(std::size_t ind) const {
733 const std::size_t ist = sn_.nodes[ind - 1].station;
734 if (ist == 0) return false;
735 const auto it = sn_.retrialparam.find(ist);
736 if (it == sn_.retrialparam.end()) return false;
737 for (const lang::Distrib<T>& p : it->second.retrial_proc)
738 if (!p.disabled) return true;
739 return false;
740 }
741
742 /**
743 * The reference WARNS that a switchover time on a non-polling queue is
744 * dropped; a warning is not available here, and silently dropping a
745 * declared service-order cost would answer a different model, so it is
746 * refused by name.
747 */
748 /**
749 * Port of the `writeJSIM` guard on a switchover declared away from a
750 * polling server: JMT's ordinary Server has no SwitchoverStrategy, so the
751 * times are WARNED ABOUT AND DROPPED, exactly as the reference does. This
752 * threw instead until it was found to be the harsher rule of the two -- the
753 * reference solves switchover_basic and reports a table, so a refusal here
754 * left the C++ row with nothing to compare rather than with a documented
755 * difference.
756 */
757 void warn_switchover_on_non_polling(std::size_t ind) const {
758 const std::size_t ist = sn_.nodes[ind - 1].station;
759 if (ist == 0) return;
760 for (const lang::Distrib<T>& s : sn_.stations[ist - 1].switchover)
761 if (!s.disabled) {
762 std::cerr << "[LINE] Warning: JMT does not support switchover times for "
763 << "non-polling queues. Switchover times will be ignored for node '"
764 << nname(ind) << "'." << std::endl;
765 return;
766 }
767 }
768
769 // -- Buffer section -----------------------------------------------------
770
771 /**
772 * Port of `saveBufferCapacity`.
773 *
774 * LINE's `cap` is Kendall's K, the WHOLE system capacity, and so is JMT's
775 * `size`, so the number crosses unchanged. -1 is JMT's "unbounded", and it
776 * is also what a capacity the population cannot REACH means: such a bound
777 * can never bind, and exporting it would make JMT reject arrivals that LINE
778 * admits at the instant the last job arrives.
779 *
780 * The test is `>=` and not `!=`: `refresh_capacity` DERIVES `sn.cap` for a
781 * station the user never capped, as the sum over the classes served there
782 * of the chain population, so a multi-class station gets (#classes) x N --
783 * 8 on a two-class model of 4 jobs. Under `!=` only the single-class case
784 * matched, and every multi-class one fell through to
785 * `assert_station_cap_exportable` and was refused as a "binding" buffer
786 * nobody declared. An open class carries an infinite population and so
787 * makes `total` infinite, which is what keeps a DECLARED cap in a mixed
788 * model refused: the open stream can fill the buffer under a closed job.
789 */
790 void save_buffer_capacity(xml::Element& section, std::size_t ind) {
791 const std::size_t ist = sn_.nodes[ind - 1].station;
792 std::string v = "-1";
793 if (ist != 0 && std::isfinite(sn_.cap[ist - 1])) {
794 const double total = reachable_population(ist);
795 const double ns = sn_.stations[ist - 1].nservers;
796 if (sn_.cap[ist - 1] < total && std::isfinite(ns)) {
797 assert_station_cap_exportable(ist);
798 v = jmt_int(sn_.cap[ist - 1]);
799 }
800 }
801 jmt_param_value(section, "java.lang.Integer", "size", v);
802 }
803
804 /**
805 * The most jobs that can be present at station `ist` (1-based).
806 *
807 * Read the way `refresh_capacity` derives the capacity itself: per CHAIN,
808 * because a chain's whole population can reach a station that serves any one
809 * of its classes (class switching moves jobs between them), and a chain none
810 * of whose classes is served there cannot put a single job on it.
811 *
812 * Deliberately NOT read off the clamped per-class capacities: comparing a
813 * capacity against a quantity derived from it would make every
814 * user-declared buffer look non-binding. Infinite when an open chain is
815 * served here, which is what the model total gave before and which sends
816 * the station to `assert_station_cap_exportable`, where the open classes
817 * are skipped by name.
818 *
819 * A class that never visits this station cannot fill it, so summing every
820 * class's population made a capacity that is exactly the reachable
821 * population look like a buffer -- which is what a SELF-LOOPING CLASS does.
822 * See the MATLAB twin in JMTIO/saveBufferCapacity.m.
823 */
824 double reachable_population(std::size_t ist) const {
825 double n = 0.0;
826 for (std::size_t c = 0; c < sn_.nchains; ++c) {
827 bool served = false;
828 double chain_jobs = 0.0;
829 for (std::size_t r : sn_.inchain[c]) {
830 if (!sn_.disabled[ist - 1][r - 1]) served = true;
831 chain_jobs += sn_.classes[r - 1].population;
832 }
833 if (served) n += chain_jobs;
834 }
835 return n;
836 }
837
838 /**
839 * True when station `ist` is the RECEIVING side of a true-BAS relation for
840 * class `r`, i.e. an arrival of `r` that finds `ist` full must block an
841 * upstream station rather than be lost.
842 *
843 * LINE accepts the BAS declaration in two places -- on the blocking
844 * (upstream) station, as `cqn_bas_blocking` does, or on the full
845 * destination, as a model read back from JMT does -- and
846 * `NetworkStruct::refresh_local_vars` resolves both into
847 * `sn.isbasdestination` (BUG-83). Reading `sn.droprule` at the capped
848 * station sees only the second form, which is what made SolverJMT refuse
849 * the first one.
850 */
851 bool is_bas_destination(std::size_t ist, std::size_t r) const {
852 if (ist == 0 || sn_.isbasdestination.size() < ist) return false;
853 if (sn_.isbasdestination[ist - 1].size() < r) return false;
854 return sn_.isbasdestination[ist - 1][r - 1];
855 }
856
857 /**
858 * The JMT dropStrategy/dropRule string for station `ist`, class `r`.
859 *
860 * Beyond `jmt_drop_text` this resolves the two ways LINE can declare BAS
861 * blocking onto the one way JMT can read it. JMT's queue section says what
862 * happens to an arrival that finds THIS buffer full, so it only understands
863 * the rule on the destination; a WAITQ slot that `is_bas_destination` marks
864 * is therefore written out as 'BAS blocking'. A node with no buffer of its
865 * own gets JMT's 'drop', as the reference does for a NaN station index.
866 *
867 * It also keeps the written file VALID: a strategy `jmt_reads_drop` rejects
868 * is spelled 'waiting queue', JMT's own no-limit default. That substitution
869 * is only ever reached where the rule cannot be consulted (infinite size, or
870 * a closed capacity equal to the population): a buffer that can actually
871 * fill under one of those is refused outright by
872 * `assert_station_cap_exportable`.
873 */
874 const char* drop_strategy_text(std::size_t ist, std::size_t r) const {
875 if (ist == 0) return "drop";
876 const DropStrategy d = sn_.droprule[ist - 1][r - 1];
877 if (d == DropStrategy::WAITQ && is_bas_destination(ist, r))
878 return jmt_drop_text(DropStrategy::BAS);
879 if (!jmt_reads_drop(d)) return jmt_drop_text(DropStrategy::WAITQ);
880 return jmt_drop_text(d);
881 }
882
883 /**
884 * Refuses a binding station capacity JMT cannot express, on two counts.
885 *
886 * (1) THE RULE IS ONE JMT CANNOT READ -- BBS, RSRD or retrial-with-limit;
887 * see `jmt_reads_drop`. Such a value is not approximated, it is IGNORED, so
888 * the capacity stops being enforced and JMT returns the unconstrained
889 * answer.
890 *
891 * (2) THE RULE IS WAITQ AND A CLOSED CLASS CAN REACH THE LIMIT, the same
892 * reason `assert_class_cap_exportable` below refuses the per-class one: JMT
893 * cannot hold a blocked closed job at its upstream station. Note this is the
894 * case where NO blocking rule is declared. A model that does declare BAS is
895 * exported as JMT "BAS blocking", which is the same queueing model, under
896 * either declaration form -- see `is_bas_destination`.
897 *
898 * That refusal advised expressing the limit as the STATION capacity instead,
899 * and measured on 2026-08-19 the advice was wrong, and neither of the two
900 * strategies a WAITQ station maps onto reproduces the UNDECLARED case:
901 *
902 * waiting queue does not enforce `size` at all. On a closed 3-queue
903 * tandem, N=6, Exp(1) FCFS, cap 2 at Q2, JMT returned the
904 * UNCONSTRAINED [2.03 1.99 1.98], X = 0.750, against the
905 * exact [3.6090 0.9711 1.4199], X = 0.6522.
906 * BAS blocking enforces it, but completes the service BEFORE blocking,
907 * so the blocked job moves the instant room frees -- a
908 * different queueing model, not a rounding: same fixture,
909 * [2.871 1.373 1.756], X = 0.7126.
910 *
911 * With no rule declared LINE instead disables the upstream departure while
912 * the destination is full, which for exponential service is repetitive
913 * service (RS) and is what SolverCTMC, SolverSSA and SolverLDES all agree
914 * on. So THAT model is refused rather than exported as either of the two
915 * things JMT can say. See BUG-81. A declared-BAS model is a different model
916 * and is exported, not refused: blocking after service is precisely what
917 * JMT's "BAS blocking" does.
918 *
919 * That the limit CAN be reached is the caller's to establish and is not
920 * retested here: `save_buffer_capacity` reaches this method only for a
921 * capacity strictly below the total population, which is the one thing that
922 * makes a buffer a buffer.
923 */
924 void assert_station_cap_exportable(std::size_t ist) const {
925 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
926 if (sn_.disabled[ist - 1][r - 1]) continue; // not served here
927 const DropStrategy dr = sn_.droprule[ist - 1][r - 1];
928 // Unmappable for EITHER class type, so this test precedes the open-class skip
929 if (!jmt_reads_drop(dr))
930 throw UnsupportedError(
931 "SolverJMT: station '" + nname(sn_.station_to_node[ist - 1]) +
932 "' applies drop strategy '" + jmt_drop_text(dr) + "' to class '" + cname(r) +
933 "' and carries a finite capacity " + jmt_int(sn_.cap[ist - 1]) +
934 " it can reach. JMT's queue section reads only 'drop', 'waiting queue', "
935 "'BAS blocking' and 'retrial'; it does not approximate anything else, it "
936 "ignores it, so the capacity would stop being enforced and the run would "
937 "return the unconstrained answer. Use SolverCTMC, SolverSSA or SolverLDES");
938 if (!std::isfinite(sn_.classes[r - 1].population)) continue; // open: JMT loses it too
939 if (dr != DropStrategy::WAITQ)
940 continue; // a mappable declared blocking rule is exported as itself
941 if (is_bas_destination(ist, r))
942 continue; // BAS declared on the UPSTREAM station: drop_strategy_text
943 // moves it onto this one, which is where JMT reads it
944 throw UnsupportedError(
945 "SolverJMT: station '" + nname(sn_.station_to_node[ist - 1]) +
946 "' carries a finite capacity " + jmt_int(sn_.cap[ist - 1]) +
947 " that binds for the closed class '" + cname(r) +
948 "'. LINE blocks a closed job that finds no room -- the upstream departure is "
949 "disabled and the job stays where it is -- and no JMT drop strategy reproduces "
950 "that: 'waiting queue' does not enforce the size at all, and 'BAS blocking' "
951 "completes the service before blocking, which is a different queueing model. "
952 "Use SolverCTMC, SolverSSA or SolverLDES, or declare DropStrategy.BAS if "
953 "blocking after service is the model you want, which SolverJMT does export");
954 }
955 }
956
957 /** Port of `saveDropStrategy`: the per-class rule of a full buffer. */
958 void save_drop_strategy(xml::Element& section, std::size_t ind) {
959 const std::size_t ist = sn_.nodes[ind - 1].station;
960 xml::Element& p = jmt_param(section, "java.lang.String", "dropStrategies", true);
961 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
962 if (!keep_[r - 1]) continue;
963 jmt_ref_class(p, cname(r));
964 const char* txt = drop_strategy_text(ist, r);
965 xml::Element& sp = p.add_child("subParameter");
966 sp.set_attr("classPath", "java.lang.String");
967 sp.set_attr("name", "dropStrategy");
968 sp.add_text_child("value", txt);
969 }
970 }
971
972 /**
973 * Port of `saveGetStrategy`: FCFS everywhere except a polling queue, whose
974 * discipline decides which of JMT's three polling get strategies serves it.
975 */
976 void save_get_strategy(xml::Element& section, std::size_t ind) {
977 const std::size_t ist = sn_.nodes[ind - 1].station;
978 const bool polling = sn_.nodes[ind - 1].nodetype == NodeType::Queue && ist != 0 &&
979 sn_.stations[ist - 1].sched == SchedStrategy::POLLING;
980 if (!polling) {
981 xml::Element& p = section.add_child("parameter");
982 p.set_attr("classPath", "jmt.engine.NetStrategies.QueueGetStrategies.FCFSstrategy");
983 p.set_attr("name", "FCFSstrategy");
984 return;
985 }
986 const typename qn::NetworkStruct<T>::PollingParam pp = sn_.effective_polling(ist);
987 xml::Element& p = section.add_child("parameter");
988 switch (pp.ptype) {
989 case PollingType::GATED:
990 p.set_attr("classPath",
991 "jmt.engine.NetStrategies.QueueGetStrategies.GatedPollingGetStrategy");
992 break;
993 case PollingType::EXHAUSTIVE:
994 p.set_attr(
995 "classPath",
996 "jmt.engine.NetStrategies.QueueGetStrategies.ExhaustivePollingGetStrategy");
997 break;
998 case PollingType::KLIMITED: {
999 p.set_attr("classPath",
1000 "jmt.engine.NetStrategies.QueueGetStrategies.LimitedPollingGetStrategy");
1001 xml::Element& k = p.add_child("subParameter");
1002 k.set_attr("classPath", "java.lang.Integer");
1003 k.set_attr("name", "pollingKValue");
1004 k.add_text_child("value", jmt_int(static_cast<double>(pp.pk)));
1005 break;
1006 }
1007 case PollingType::DECREMENTING:
1008 throw UnsupportedError(
1009 "SolverJMT: JMT does not support the decrementing (semiexhaustive) polling "
1010 "discipline; use the LDES solver");
1011 }
1012 p.set_attr("name", "FCFSstrategy");
1013 }
1014
1015 /**
1016 * Port of `savePutStrategy`: the discipline expressed as WHERE an arrival
1017 * is inserted in the buffer.
1018 *
1019 * JMT has no scheduling-strategy field: FCFS is a tail insertion, LCFS a
1020 * head insertion, SJF/SRPT an ordered one, and a preemptive discipline its
1021 * own put strategy. Everything unlisted -- PS above all, whose sharing is
1022 * expressed by the PSServer section instead -- is a tail insertion.
1023 */
1024 void save_put_strategy(xml::Element& section, std::size_t ind) {
1025 const std::size_t ist = sn_.nodes[ind - 1].station;
1026 xml::Element& p =
1027 jmt_param(section, "jmt.engine.NetStrategies.QueuePutStrategy", "QueuePutStrategy", true);
1028 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
1029 if (!keep_[r - 1]) continue;
1030 jmt_ref_class(p, cname(r));
1031 const char* nm = "TailStrategy";
1032 if (ist != 0) {
1033 switch (sn_.stations[ist - 1].sched) {
1034 case SchedStrategy::SIRO: nm = "RandStrategy"; break;
1035 case SchedStrategy::LJF: nm = "LJFStrategy"; break;
1036 case SchedStrategy::SJF: nm = "SJFStrategy"; break;
1037 case SchedStrategy::LEPT: nm = "LEPTStrategy"; break;
1038 case SchedStrategy::SEPT: nm = "SEPTStrategy"; break;
1039 case SchedStrategy::LCFS: nm = "HeadStrategy"; break;
1040 case SchedStrategy::LCFSPRIO: nm = "HeadStrategyPriority"; break;
1041 case SchedStrategy::LCFSPR: nm = "LCFSPRStrategy"; break;
1042 case SchedStrategy::LCFSPI: nm = "LCFSPIStrategy"; break;
1043 case SchedStrategy::LCFSPRPRIO: nm = "LCFSPRStrategyPriority"; break;
1044 case SchedStrategy::LCFSPIPRIO: nm = "LCFSPIStrategyPriority"; break;
1045 case SchedStrategy::FCFSPR: nm = "FCFSPRStrategy"; break;
1046 case SchedStrategy::FCFSPI: nm = "FCFSPIStrategy"; break;
1047 case SchedStrategy::FCFSPRPRIO: nm = "FCFSPRStrategyPriority"; break;
1048 case SchedStrategy::FCFSPIPRIO: nm = "FCFSPIStrategyPriority"; break;
1049 case SchedStrategy::HOL: nm = "TailStrategyPriority"; break;
1050 case SchedStrategy::EDD: nm = "EDDStrategy"; break;
1051 case SchedStrategy::EDF: nm = "EDFStrategy"; break;
1052 case SchedStrategy::SRPT: nm = "SRPTStrategy"; break;
1053 case SchedStrategy::SRPTPRIO: nm = "SRPTStrategyPriority"; break;
1054 default: nm = "TailStrategy"; break;
1055 }
1056 }
1057 xml::Element& sp = p.add_child("subParameter");
1058 sp.set_attr("classPath",
1059 std::string("jmt.engine.NetStrategies.QueuePutStrategies.") + nm);
1060 sp.set_attr("name", nm);
1061 }
1062 }
1063
1064 // -- Server section -----------------------------------------------------
1065
1066 /**
1067 * Port of `saveNumberOfServers`.
1068 *
1069 * LPS EXPORTS AS ONE SERVER. Its admission limit is not a server count but
1070 * a cap on the number in service, which `save_regions` expresses as an
1071 * implicit finite capacity region; exporting the limit as the server count
1072 * would give a c-server FCFS queue instead of limited processor sharing.
1073 * A load-dependent station exports `max(nservers, max lldscaling)`, since
1074 * `min(1:N, c)` scaling IS a c-server queue.
1075 */
1076 void save_number_of_servers(xml::Element& section, std::size_t ind) {
1077 const std::size_t ist = sn_.nodes[ind - 1].station;
1078 double maxjobs = 1.0;
1079 if (ist != 0) {
1080 if (sn_.stations[ist - 1].sched == SchedStrategy::LPS) {
1081 maxjobs = 1.0;
1082 } else {
1083 maxjobs = sn_.stations[ist - 1].nservers;
1084 for (const T& s : sn_.stations[ist - 1].lldscaling)
1085 maxjobs = std::max(maxjobs, d(s));
1086 }
1087 }
1088 jmt_param_value(section, "java.lang.Integer", "maxJobs", jmt_int(maxjobs));
1089 }
1090
1091 /** Port of `saveServerVisits`: one visit per class, always. */
1092 void save_server_visits(xml::Element& section) {
1093 xml::Element& p = jmt_param(section, "java.lang.Integer", "numberOfVisits", true);
1094 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
1095 if (!keep_[r - 1]) continue;
1096 jmt_ref_class(p, cname(r));
1097 xml::Element& sp = p.add_child("subParameter");
1098 sp.set_attr("classPath", "java.lang.Integer");
1099 sp.set_attr("name", "numberOfVisits");
1100 sp.add_text_child("value", "1");
1101 }
1102 }
1103
1104 /**
1105 * Server pools and job parallelism of a node, as the JMT Server section needs them.
1106 *
1107 * JMT's Server section takes classParallelism, serverNames, serversPerServerType,
1108 * serverCompatibilities and schedulingPolicy as one positional block of its
1109 * constructor (`jmt.engine.NodeSections.Server`), so the five are emitted
1110 * together or not at all, and always after the service strategies. A station
1111 * declaring parallelism alone is therefore given one synthetic pool holding all
1112 * of its servers, since the pool counts, not numberOfServers, size the server
1113 * pool once any pool is declared.
1114 */
1115 struct ServerPools {
1116 bool present = false;
1117 std::vector<std::string> names;
1118 std::vector<double> counts;
1119 std::vector<std::vector<bool>> compat; ///< [type][class]
1121 std::vector<std::size_t> parallelism; ///< per class, one-based class order
1122 };
1123
1124 ServerPools server_pools(std::size_t ind) const {
1125 ServerPools pools;
1126 const std::size_t ist = sn_.nodes[ind - 1].station;
1127 if (ist == 0) return pools;
1128 const auto& st = sn_.stations[ist - 1];
1129 const bool has_types = !st.server_types.empty();
1130 bool has_parallelism = false;
1131 for (std::size_t n : st.server_parallelism) {
1132 if (n > 1) { has_parallelism = true; break; }
1133 }
1134 if (!has_types && !has_parallelism) return pools;
1135
1136 pools.present = true;
1137 pools.parallelism.assign(sn_.nclasses, 1);
1138 for (std::size_t r = 0; r < sn_.nclasses && r < st.server_parallelism.size(); ++r) {
1139 pools.parallelism[r] = st.server_parallelism[r] < 1 ? 1 : st.server_parallelism[r];
1140 }
1141 if (has_types) {
1142 pools.policy = st.hetero_policy;
1143 for (const auto& pool : st.server_types) {
1144 pools.names.push_back(pool.name);
1145 pools.counts.push_back(pool.count);
1146 std::vector<bool> row(sn_.nclasses, true);
1147 for (std::size_t r = 0; r < sn_.nclasses; ++r) {
1148 // An EMPTY compatibility row means "every class", the constructor
1149 // default; JMT has no such shorthand, so it is expanded here.
1150 row[r] = pool.compatible.empty()
1151 ? true
1152 : (r < pool.compatible.size() ? pool.compatible[r] : false);
1153 }
1154 pools.compat.push_back(row);
1155 }
1156 } else {
1157 pools.names.push_back(sn_.nodes[ind - 1].name + " - Server Type 1");
1158 pools.counts.push_back(st.nservers);
1159 pools.compat.push_back(std::vector<bool>(sn_.nclasses, true));
1160 }
1161 return pools;
1162 }
1163
1164 /** Port of `saveClassParallelism`: `Server.serverNumRequired`, per class. */
1165 void save_class_parallelism(xml::Element& section, std::size_t ind) {
1166 const ServerPools pools = server_pools(ind);
1167 if (!pools.present) return;
1168 xml::Element& p = jmt_param(section, "java.lang.Integer", "classParallelism", true);
1169 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
1170 if (!keep_[r - 1]) continue;
1171 jmt_ref_class(p, cname(r));
1172 xml::Element& sp = p.add_child("subParameter");
1173 sp.set_attr("classPath", "java.lang.Integer");
1174 sp.set_attr("name", "serverParallelism");
1175 sp.add_text_child("value", jmt_int(static_cast<double>(pools.parallelism[r - 1])));
1176 }
1177 }
1178
1179 /** Port of `saveServerTypeNames`. */
1180 void save_server_type_names(xml::Element& section, std::size_t ind) {
1181 const ServerPools pools = server_pools(ind);
1182 if (!pools.present) return;
1183 xml::Element& p = jmt_param(section, "java.lang.String", "serverNames", true);
1184 for (const std::string& name : pools.names) {
1185 xml::Element& sp = p.add_child("subParameter");
1186 sp.set_attr("classPath", "java.lang.String");
1187 sp.set_attr("name", "serverTypesNames");
1188 sp.add_text_child("value", name);
1189 }
1190 }
1191
1192 /** Port of `saveServersPerType`. */
1193 void save_servers_per_type(xml::Element& section, std::size_t ind) {
1194 const ServerPools pools = server_pools(ind);
1195 if (!pools.present) return;
1196 xml::Element& p = jmt_param(section, "java.lang.Integer", "serversPerServerType", true);
1197 for (double count : pools.counts) {
1198 xml::Element& sp = p.add_child("subParameter");
1199 sp.set_attr("classPath", "java.lang.Integer");
1200 sp.set_attr("name", "serverTypesNumOfServers");
1201 sp.add_text_child("value", jmt_int(count));
1202 }
1203 }
1204
1205 /** Port of `saveServerCompatibilities`. */
1206 void save_server_compatibilities(xml::Element& section, std::size_t ind) {
1207 const ServerPools pools = server_pools(ind);
1208 if (!pools.present) return;
1209 xml::Element& p = jmt_param(section, "java.lang.Object", "serverCompatibilities", true);
1210 for (const std::vector<bool>& row : pools.compat) {
1211 xml::Element& tn = p.add_child("subParameter");
1212 tn.set_attr("array", "true");
1213 tn.set_attr("classPath", "java.lang.Boolean");
1214 tn.set_attr("name", "serverTypesCompatibilities");
1215 for (std::size_t r = 0; r < sn_.nclasses; ++r) {
1216 xml::Element& cn = tn.add_child("subParameter");
1217 cn.set_attr("classPath", "java.lang.Boolean");
1218 cn.set_attr("name", "compatibilities");
1219 cn.add_text_child("value", row[r] ? "true" : "false");
1220 }
1221 }
1222 }
1223
1224 /** Port of `saveHeteroSchedPolicy`, via `HeteroSchedPolicy.toJMTText`. */
1225 void save_hetero_sched_policy(xml::Element& section, std::size_t ind) {
1226 const ServerPools pools = server_pools(ind);
1227 if (!pools.present) return;
1228 jmt_param_value(section, "java.lang.String", "schedulingPolicy",
1229 jmt_hetero_text(pools.policy));
1230 }
1231
1232 /**
1233 * Warns that per-server-type service rates cannot reach the JMT engine.
1234 *
1235 * JMT keys the ServiceStrategy array of a station by refClass, so its loader
1236 * (`jmt.engine.simEngine.SimLoader`) keeps one strategy per class however many
1237 * (type, class) entries are written, and every pool of a station ends up
1238 * serving at the class rate. Pool sizes, class compatibilities and the
1239 * assignment policy do cross; per-pool service laws do not.
1240 */
1241 void warn_hetero_rates(std::size_t ind) const {
1242 const std::size_t ist = sn_.nodes[ind - 1].station;
1243 if (ist == 0) return;
1244 const auto& pools = sn_.stations[ist - 1].server_types;
1245 if (pools.size() < 2) return;
1246 bool first_set = false, distinct = false;
1247 double first = 0.0;
1248 for (const auto& pool : pools) {
1249 for (const lang::Distrib<T>& sd : pool.service) {
1250 if (sd.disabled) continue;
1251 const double mean = num_traits<T>::to_double(sd.mean);
1252 if (!(mean > 0)) continue;
1253 if (!first_set) { first = mean; first_set = true; }
1254 else if (std::abs(mean - first) > 1e-12) { distinct = true; }
1255 }
1256 }
1257 if (!distinct) return;
1258 std::cerr << "[LINE] Warning: JMT keys service strategies by job class, so the "
1259 << "per-server-type service rates of station '" << sn_.nodes[ind - 1].name
1260 << "' cannot be exported; every pool will serve at the class service rate. "
1261 << "Use the LDES or CTMC solver for per-type rates." << std::endl;
1262 }
1263
1264 /**
1265 * Port of `saveServiceStrategy`.
1266 *
1267 * A pair the class never visits becomes a DisabledServiceTimeStrategy and
1268 * an Immediate becomes a ZeroServiceTimeStrategy; neither carries a
1269 * distribution. Everything else goes through the shared emitter.
1270 */
1271 void save_service_strategy(xml::Element& section, std::size_t ind) {
1272 const std::size_t ist = sn_.nodes[ind - 1].station;
1273 xml::Element& p = jmt_param(section, "jmt.engine.NetStrategies.ServiceStrategy",
1274 "ServiceStrategy", true);
1275 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
1276 if (!keep_[r - 1]) continue;
1277 jmt_ref_class(p, cname(r));
1278 xml::Element& sts = p.add_child("subParameter");
1279 const JmtDistView<T> v =
1280 ist == 0 ? JmtDistView<T>() : jmt_dist_view(sn_.service[ist - 1][r - 1]);
1281 if (v.type == lang::ProcessType::DISABLED) {
1282 sts.set_attr("classPath",
1283 "jmt.engine.NetStrategies.ServiceStrategies."
1284 "DisabledServiceTimeStrategy");
1285 sts.set_attr("name", "DisabledServiceTimeStrategy");
1286 continue;
1287 }
1288 if (v.type == lang::ProcessType::IMMEDIATE) {
1289 sts.set_attr("classPath",
1290 "jmt.engine.NetStrategies.ServiceStrategies.ZeroServiceTimeStrategy");
1291 sts.set_attr("name", "ZeroServiceTimeStrategy");
1292 continue;
1293 }
1294 sts.set_attr("classPath",
1295 "jmt.engine.NetStrategies.ServiceStrategies.ServiceTimeStrategy");
1296 sts.set_attr("name", "ServiceTimeStrategy");
1297 jmt_append_distribution(sts, v, "SolverJMT (service)");
1298 }
1299 }
1300
1301 /**
1302 * Port of `saveArrivalStrategy`.
1303 *
1304 * A CLOSED class has no arrival process at the Source and is exported as a
1305 * ServiceTimeStrategy with a literal `null` body, which is how JMT spells
1306 * "this class does not arrive here". So is an open class the Source
1307 * disables -- one that enters by class switching only.
1308 */
1309 void save_arrival_strategy(xml::Element& section, std::size_t ind) {
1310 const std::size_t ist = sn_.nodes[ind - 1].station;
1311 xml::Element& p = jmt_param(section, "jmt.engine.NetStrategies.ServiceStrategy",
1312 "ServiceStrategy", true);
1313 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
1314 if (!keep_[r - 1]) continue;
1315 jmt_ref_class(p, cname(r));
1316 xml::Element& sts = p.add_child("subParameter");
1317 sts.set_attr("classPath",
1318 "jmt.engine.NetStrategies.ServiceStrategies.ServiceTimeStrategy");
1319 sts.set_attr("name", "ServiceTimeStrategy");
1320 const bool closed = std::isfinite(sn_.classes[r - 1].population);
1321 const JmtDistView<T> v =
1322 ist == 0 ? JmtDistView<T>() : jmt_dist_view(sn_.service[ist - 1][r - 1]);
1323 if (closed || v.type == lang::ProcessType::DISABLED) {
1324 sts.add_text_child("value", "null");
1325 continue;
1326 }
1327 if (v.type == lang::ProcessType::IMMEDIATE) {
1328 sts.set_attr("classPath",
1329 "jmt.engine.NetStrategies.ServiceStrategies.ZeroServiceTimeStrategy");
1330 sts.set_attr("name", "ZeroServiceTimeStrategy");
1331 continue;
1332 }
1333 jmt_append_distribution(sts, v, "SolverJMT (arrival)");
1334 }
1335 }
1336
1337 /** Port of `savePreemptiveStrategy`: which PS variant a PSServer runs. */
1338 void save_preemptive_strategy(xml::Element& section, std::size_t ind) {
1339 const std::size_t ist = sn_.nodes[ind - 1].station;
1340 xml::Element& p = jmt_param(section, "jmt.engine.NetStrategies.PSStrategy", "PSStrategy",
1341 true);
1342 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
1343 if (!keep_[r - 1]) continue;
1344 jmt_ref_class(p, cname(r));
1345 const char* nm = nullptr;
1346 if (ist != 0) switch (sn_.stations[ist - 1].sched) {
1347 // LPS shares the server evenly among those admitted, so its
1348 // in-service discipline IS EPS; the limit is the region.
1349 case SchedStrategy::PS:
1350 case SchedStrategy::LPS: nm = "EPSStrategy"; break;
1351 case SchedStrategy::DPS: nm = "DPSStrategy"; break;
1352 case SchedStrategy::GPS: nm = "GPSStrategy"; break;
1353 case SchedStrategy::PSPRIO: nm = "EPSStrategyPriority"; break;
1354 case SchedStrategy::DPSPRIO: nm = "DPSStrategyPriority"; break;
1355 case SchedStrategy::GPSPRIO: nm = "GPSStrategyPriority"; break;
1356 default: nm = nullptr; break;
1357 }
1358 xml::Element& sp = p.add_child("subParameter");
1359 if (nm != nullptr) {
1360 sp.set_attr("classPath", std::string("jmt.engine.NetStrategies.PSStrategies.") + nm);
1361 sp.set_attr("name", nm);
1362 }
1363 }
1364 }
1365
1366 /** Port of `savePreemptiveWeights`: the DPS/GPS share of each class. */
1367 void save_preemptive_weights(xml::Element& section, std::size_t ind) {
1368 const std::size_t ist = sn_.nodes[ind - 1].station;
1369 xml::Element& p = jmt_param(section, "java.lang.Double", "serviceWeights", true);
1370 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
1371 if (!keep_[r - 1]) continue;
1372 jmt_ref_class(p, cname(r));
1373 double w = 0.0;
1374 if (ist != 0 && sn_.stations[ist - 1].schedparam.size() >= r)
1375 w = d(sn_.stations[ist - 1].schedparam[r - 1]);
1376 xml::Element& sp = p.add_child("subParameter");
1377 sp.set_attr("classPath", "java.lang.Double");
1378 sp.set_attr("name", "serviceWeight");
1379 sp.add_text_child("value", jmt_num(w));
1380 }
1381 }
1382
1383 /**
1384 * Port of `saveDelayOffStrategy`: the setup and delay-off times of a server
1385 * that powers down when idle.
1386 *
1387 * JMT casts each per-class entry to `ServiceStrategy[]` and reads element
1388 * [0], so the strategy sits inside a single-element array rather than
1389 * directly under the class. A class with no time declared gets a
1390 * deterministic zero, not an omission: an absent entry would leave JMT's
1391 * array short and shift every later class's setup time onto the wrong one.
1392 */
1393 void save_delay_off_strategy(xml::Element& section, std::size_t ind) {
1394 const std::size_t ist = sn_.nodes[ind - 1].station;
1395 if (ist == 0) return;
1396 const auto it = sn_.setupparam.find(ist);
1397 if (it == sn_.setupparam.end()) return;
1398 const qn::SetupDelayOffParam<T>& sp = it->second;
1399 bool any = false;
1400 for (const lang::Distrib<T>& s : sp.setup)
1401 if (!s.disabled) any = true;
1402 if (!any) return;
1403
1404 const char* names[2] = {"delayOffTime", "setUpTime"};
1405 for (int which = 0; which < 2; ++which) {
1406 const std::vector<lang::Distrib<T>>& tab = which == 0 ? sp.delayoff : sp.setup;
1407 xml::Element& p = jmt_param(section, "java.lang.Object", names[which], true);
1408 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
1409 jmt_ref_class(p, cname(r));
1410 xml::Element& row = p.add_child("subParameter");
1411 row.set_attr("array", "true");
1412 row.set_attr("classPath", "jmt.engine.NetStrategies.ServiceStrategy");
1413 row.set_attr("name", names[which]);
1414 xml::Element& sts = row.add_child("subParameter");
1415 sts.set_attr("classPath",
1416 "jmt.engine.NetStrategies.ServiceStrategies.ServiceTimeStrategy");
1417 sts.set_attr("name", "ServiceTimeStrategy");
1418 if (r <= tab.size() && !tab[r - 1].disabled)
1419 append_simple_distribution(sts, tab[r - 1]);
1420 else
1421 append_zero_time(sts);
1422 }
1423 }
1424 }
1425
1426 /** `appendZeroTimeXml`: a deterministic zero. */
1427 void append_zero_time(xml::Element& parent) {
1428 xml::Element& dn = parent.add_child("subParameter");
1429 dn.set_attr("classPath", "jmt.engine.random.DeterministicDistr");
1430 dn.set_attr("name", "Deterministic");
1431 xml::Element& par = parent.add_child("subParameter");
1432 par.set_attr("classPath", "jmt.engine.random.DeterministicDistrPar");
1433 par.set_attr("name", "distrPar");
1434 jmt_scalar(par, "java.lang.Double", "t", "0.0");
1435 }
1436
1437 /**
1438 * `appendDistributionXml`: the REDUCED emitter the setup/delay-off pair
1439 * uses -- Exp, Erlang, Det, Immediate, and everything else as a
1440 * deterministic time equal to its mean.
1441 *
1442 * The fallback is the reference's and is kept: a setup time is a small
1443 * fixed overhead in every model that has one, so replacing an exotic law by
1444 * its mean there is a documented simplification rather than a silent one,
1445 * and refusing would reject models JMT can otherwise run.
1446 */
1447 void append_simple_distribution(xml::Element& parent, const lang::Distrib<T>& dist) {
1448 if (dist.type == lang::ProcessType::IMMEDIATE) {
1449 append_zero_time(parent);
1450 return;
1451 }
1452 const double mean = d(dist.mean);
1453 if (dist.type == lang::ProcessType::EXP) {
1454 xml::Element& dn = parent.add_child("subParameter");
1455 dn.set_attr("classPath", "jmt.engine.random.Exponential");
1456 dn.set_attr("name", "Exponential");
1457 xml::Element& par = parent.add_child("subParameter");
1458 par.set_attr("classPath", "jmt.engine.random.ExponentialPar");
1459 par.set_attr("name", "distrPar");
1460 jmt_double(par, "lambda", mean > 0.0 ? 1.0 / mean : 0.0);
1461 return;
1462 }
1463 if (dist.type == lang::ProcessType::ERLANG) {
1464 const std::size_t ph = dist.phases();
1465 xml::Element& dn = parent.add_child("subParameter");
1466 dn.set_attr("classPath", "jmt.engine.random.Erlang");
1467 dn.set_attr("name", "Erlang");
1468 xml::Element& par = parent.add_child("subParameter");
1469 par.set_attr("classPath", "jmt.engine.random.ErlangPar");
1470 par.set_attr("name", "distrPar");
1471 jmt_double(par, "alpha", mean > 0.0 ? static_cast<double>(ph) / mean : 0.0);
1472 jmt_scalar(par, "java.lang.Long", "r", jmt_int(static_cast<double>(ph)));
1473 return;
1474 }
1475 xml::Element& dn = parent.add_child("subParameter");
1476 dn.set_attr("classPath", "jmt.engine.random.DeterministicDistr");
1477 dn.set_attr("name", "Deterministic");
1478 xml::Element& par = parent.add_child("subParameter");
1479 par.set_attr("classPath", "jmt.engine.random.DeterministicDistrPar");
1480 par.set_attr("name", "distrPar");
1481 jmt_double(par, "t", mean);
1482 }
1483
1484 // -- Dispatcher / ClassSwitch / Fork / Join ------------------------------
1485
1486 /**
1487 * Port of `saveRoutingStrategy`.
1488 *
1489 * A CLASS SWITCH NODE IS EXPORTED AS RANDOM ROUTING. It has exactly one
1490 * outgoing link, so uniform routing over it is the same routing, and it
1491 * avoids reading `rt` at a node whose class-switch mass the refresh has
1492 * already folded into the edges around it.
1493 */
1494 void save_routing_strategy(xml::Element& section, std::size_t ind) {
1495 const std::size_t K = sn_.nclasses;
1496 const bool is_cs = sn_.nodes[ind - 1].nodetype == NodeType::ClassSwitch;
1497 xml::Element& p =
1498 jmt_param(section, "jmt.engine.NetStrategies.RoutingStrategy", "RoutingStrategy", true);
1499 for (std::size_t r = 1; r <= K; ++r) {
1500 if (!keep_[r - 1]) continue;
1501 jmt_ref_class(p, cname(r));
1502 const RoutingStrategy rs =
1503 is_cs ? RoutingStrategy::RAND
1504 : (sn_.nodes[ind - 1].routing.size() >= r ? sn_.nodes[ind - 1].routing[r - 1]
1505 : RoutingStrategy::PROB);
1506 xml::Element& sp = p.add_child("subParameter");
1507 switch (rs) {
1508 case RoutingStrategy::RAND:
1509 sp.set_attr("classPath",
1510 "jmt.engine.NetStrategies.RoutingStrategies.RandomStrategy");
1511 sp.set_attr("name", "Random");
1512 break;
1513 case RoutingStrategy::RROBIN:
1514 sp.set_attr("classPath",
1515 "jmt.engine.NetStrategies.RoutingStrategies.RoundRobinStrategy");
1516 sp.set_attr("name", "Round Robin");
1517 break;
1518 case RoutingStrategy::JSQ:
1519 sp.set_attr("classPath",
1520 "jmt.engine.NetStrategies.RoutingStrategies."
1521 "ShortestQueueLengthRoutingStrategy");
1522 sp.set_attr("name", "Join the Shortest Queue (JSQ)");
1523 break;
1524 case RoutingStrategy::SQ: {
1525 sp.set_attr("classPath",
1526 "jmt.engine.NetStrategies.RoutingStrategies.PowerOfKRoutingStrategy");
1527 sp.set_attr("name", "Power of k");
1528 const int dpar = sn_.nodes[ind - 1].routing_param.size() >= r
1529 ? sn_.nodes[ind - 1].routing_param[r - 1]
1530 : 0;
1531 jmt_scalar(sp, "java.lang.Integer", "k", jmt_int(static_cast<double>(dpar)));
1532 // Always false: LINE implements SQ(d) only. JMT's
1533 // withMemory selects Anselmi & Dufour SQ(d,N), a different
1534 // policy.
1535 jmt_scalar(sp, "java.lang.Boolean", "withMemory", "false");
1536 break;
1537 }
1538 case RoutingStrategy::WRROBIN: {
1539 sp.set_attr("classPath",
1540 "jmt.engine.NetStrategies.RoutingStrategies."
1541 "WeightedRoundRobinStrategy");
1542 sp.set_attr("name", "Weighted Round Robin");
1543 xml::Element& arr =
1544 jmt_param_sub(sp, "jmt.engine.NetStrategies.RoutingStrategies.WeightEntry",
1545 "WeightEntryArray");
1546 const std::map<std::size_t, double>& w =
1547 sn_.nodes[ind - 1].routing_weights.size() >= r
1548 ? sn_.nodes[ind - 1].routing_weights[r - 1]
1549 : empty_weights_;
1550 for (std::size_t j : outputs_of(ind)) {
1551 const auto wi = w.find(j);
1552 xml::Element& e = arr.add_child("subParameter");
1553 e.set_attr("classPath",
1554 "jmt.engine.NetStrategies.RoutingStrategies.WeightEntry");
1555 e.set_attr("name", "WeightEntry");
1556 jmt_scalar(e, "java.lang.String", "stationName", nname(j));
1557 jmt_scalar(e, "java.lang.Integer", "weight",
1558 jmt_int(wi == w.end() ? 0.0 : wi->second));
1559 }
1560 break;
1561 }
1562 case RoutingStrategy::PROB: {
1563 sp.set_attr("classPath",
1564 "jmt.engine.NetStrategies.RoutingStrategies.EmpiricalStrategy");
1565 sp.set_attr("name", "Probabilities");
1566 xml::Element& arr = jmt_param_sub(sp, "jmt.engine.random.EmpiricalEntry",
1567 "EmpiricalEntryArray");
1568 for (std::size_t j : outputs_of(ind)) {
1569 const double pr = route_node(ind, r, j, r);
1570 if (!(pr > 0.0)) continue;
1571 xml::Element& e = arr.add_child("subParameter");
1572 e.set_attr("classPath", "jmt.engine.random.EmpiricalEntry");
1573 e.set_attr("name", "EmpiricalEntry");
1574 jmt_scalar(e, "java.lang.String", "stationName", nname(j));
1575 jmt_scalar(e, "java.lang.Double", "probability", jmt_fmt(pr));
1576 }
1577 break;
1578 }
1579 default:
1580 sp.set_attr(
1581 "classPath",
1582 "jmt.engine.NetStrategies.RoutingStrategies.DisabledRoutingStrategy");
1583 sp.set_attr("name", "Random");
1584 break;
1585 }
1586 }
1587 }
1588
1589 /** `<subParameter array="true" classPath=CP name=NAME>` */
1590 static xml::Element& jmt_param_sub(xml::Element& parent, const char* cp, const char* name) {
1591 xml::Element& e = parent.add_child("subParameter");
1592 e.set_attr("array", "true");
1593 e.set_attr("classPath", cp);
1594 e.set_attr("name", name);
1595 return e;
1596 }
1597
1598 /** `sn.rtnodes((i-1)K+r, (j-1)K+s)`, guarded against an unbuilt matrix. */
1599 double route_node(std::size_t i, std::size_t r, std::size_t j, std::size_t s) const {
1600 const std::size_t K = sn_.nclasses;
1601 const std::size_t a = (i - 1) * K + (r - 1), b = (j - 1) * K + (s - 1);
1602 if (sn_.rtnodes.rows() <= a || sn_.rtnodes.cols() <= b) return 0.0;
1603 return d(sn_.rtnodes(a, b));
1604 }
1605
1606 /**
1607 * Port of `saveClassSwitchStrategy`: the (K x K) switching matrix as JMT
1608 * reads it, each cell the mass class r leaves this node with as class s.
1609 *
1610 * The cell is the SUM over the node's outgoing links of `rtnodes`, not the
1611 * declared `csmatrix`: the refresh has already multiplied the switch by the
1612 * routing, and the sum recovers the switching probability independently of
1613 * how the mass was split over the links.
1614 */
1615 void save_class_switch_strategy(xml::Element& section, std::size_t ind) {
1616 const std::size_t K = sn_.nclasses;
1617 const std::vector<std::size_t> jset = outputs_of(ind);
1618 xml::Element& p = jmt_param(section, "java.lang.Object", "matrix", true);
1619 for (std::size_t r = 1; r <= K; ++r) {
1620 if (!keep_[r - 1]) continue;
1621 jmt_ref_class(p, cname(r));
1622 xml::Element& row = p.add_child("subParameter");
1623 row.set_attr("array", "true");
1624 row.set_attr("classPath", "java.lang.Float");
1625 row.set_attr("name", "row");
1626 for (std::size_t s = 1; s <= K; ++s) {
1627 if (!keep_[s - 1]) continue;
1628 jmt_ref_class(row, cname(s));
1629 double acc = 0.0;
1630 for (std::size_t j : jset) acc += route_node(ind, r, j, s);
1631 xml::Element& cell = row.add_child("subParameter");
1632 cell.set_attr("classPath", "java.lang.Float");
1633 cell.set_attr("name", "cell");
1634 cell.add_text_child("value", jmt_fmt(acc));
1635 }
1636 }
1637 }
1638
1639 /**
1640 * Port of `saveForkStrategy`.
1641 *
1642 * ONE OutPath ENTRY PER BRANCH. The reference used to build the entry
1643 * inside a loop over the connected nodes but append it outside, so only the
1644 * LAST link survived, and the JAR and this port transcribed the same shape.
1645 * It was harmless only because `isSimplifiedFork` is true, which makes JMT
1646 * send one job down every outgoing link and ignore the branch list -- and it
1647 * stops being harmless the moment a branch carries its own probability or
1648 * its own jobs-per-link. All four codebases now emit the full list.
1649 */
1650 void save_fork_strategy(xml::Element& section, std::size_t ind) {
1651 const qn::ForkParam<T>* fk = sn_.fork_param_of(ind);
1652 const double fan_out = sn_.nodes[ind - 1].tasks_per_link;
1653 jmt_param_value(section, "java.lang.Integer", "jobsPerLink", jmt_int(fan_out));
1654 jmt_param_value(section, "java.lang.Integer", "block", "-1");
1655
1656 // isSimplifiedFork lets JMT ignore the branch list and send one job down
1657 // every link. That is only the same model when every branch is certain
1658 // and carries the same number of tasks, so a variable forking level
1659 // switches it off and makes JMT read the per-branch entries below.
1660 bool simplified = true;
1661 if (fk != 0) {
1662 for (std::size_t k = 0; k < fk->fan_out_link.rows() && simplified; ++k)
1663 for (std::size_t r = 0; r < fk->fan_out_link.cols() && simplified; ++r) {
1664 const double p = num_traits<T>::to_double(fk->fan_out_prob(k, r));
1665 if (p == 0.0) continue; // link not taken
1666 if (p != 1.0) simplified = false;
1667 if (num_traits<T>::to_double(fk->fan_out_link(k, r)) != fan_out)
1668 simplified = false;
1669 if (!fk->fan_out_dist[k][r].disabled) simplified = false;
1670 }
1671 }
1672 jmt_param_value(section, "java.lang.Boolean", "isSimplifiedFork",
1673 simplified ? "true" : "false");
1674
1675 xml::Element& p =
1676 jmt_param(section, "jmt.engine.NetStrategies.ForkStrategy", "ForkStrategy", true);
1677 const std::vector<std::size_t> outs = outputs_of(ind);
1678 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
1679 if (!keep_[r - 1]) continue;
1680 jmt_ref_class(p, cname(r));
1681 xml::Element& cs = p.add_child("subParameter");
1682 cs.set_attr("classPath", "jmt.engine.NetStrategies.ForkStrategies.ProbabilitiesFork");
1683 cs.set_attr("name", "Branch Probabilities");
1684 xml::Element& arr = jmt_param_sub(
1685 cs, "jmt.engine.NetStrategies.ForkStrategies.OutPath", "EmpiricalEntryArray");
1686 const RoutingStrategy rs = sn_.nodes[ind - 1].routing.size() >= r
1687 ? sn_.nodes[ind - 1].routing[r - 1]
1688 : RoutingStrategy::PROB;
1689 if (rs != RoutingStrategy::PROB || outs.empty()) continue;
1690 for (std::size_t oi = 0; oi < outs.size(); ++oi) {
1691 xml::Element& entry = arr.add_child("subParameter");
1692 entry.set_attr("classPath", "jmt.engine.NetStrategies.ForkStrategies.OutPath");
1693 entry.set_attr("name", "OutPathEntry");
1694 xml::Element& unit = entry.add_child("subParameter");
1695 unit.set_attr("classPath", "jmt.engine.random.EmpiricalEntry");
1696 unit.set_attr("name", "outUnitProbability");
1697 jmt_scalar(unit, "java.lang.String", "stationName", nname(outs[oi]));
1698
1699 // branch activation probability: JMT's outUnitProbability
1700 const std::size_t k0 = outs[oi] - 1, r0 = r - 1;
1701 const bool has_fan = fk != 0;
1702 const double branch_p =
1703 has_fan ? num_traits<T>::to_double(fk->fan_out_prob(k0, r0)) : 1.0;
1704 jmt_scalar(unit, "java.lang.Double", "probability", jmt_fmt(branch_p));
1705
1706 // JobsPerLinkDis is an EmpiricalEntry ARRAY: one entry per point
1707 // of the jobs-per-link distribution. A deterministic fork emits
1708 // the single degenerate entry it always did.
1709 std::vector<double> pts, prs;
1710 if (has_fan && !fk->fan_out_dist[k0][r0].disabled) {
1711 const lang::Distrib<T>& d = fk->fan_out_dist[k0][r0];
1712 double tot = 0.0;
1713 for (std::size_t e = 0; e < d.params.size(); ++e)
1714 tot += num_traits<T>::to_double(d.params[e]);
1715 for (std::size_t e = 0; e < d.params.size(); ++e) {
1716 pts.push_back(d.trace.empty()
1717 ? static_cast<double>(e + 1)
1718 : num_traits<T>::to_double(d.trace[e]));
1719 prs.push_back(num_traits<T>::to_double(d.params[e]) / tot);
1720 }
1721 } else if (has_fan) {
1722 pts.push_back(num_traits<T>::to_double(fk->fan_out_link(k0, r0)));
1723 prs.push_back(1.0);
1724 } else {
1725 pts.push_back(fan_out);
1726 prs.push_back(1.0);
1727 }
1728
1729 xml::Element& jpl =
1730 jmt_param_sub(entry, "jmt.engine.random.EmpiricalEntry", "JobsPerLinkDis");
1731 for (std::size_t e = 0; e < pts.size(); ++e) {
1732 xml::Element& jple = jpl.add_child("subParameter");
1733 jple.set_attr("classPath", "jmt.engine.random.EmpiricalEntry");
1734 jple.set_attr("name", "EmpiricalEntry");
1735 jmt_scalar(jple, "java.lang.String", "numbers", jmt_int(pts[e]));
1736 jmt_scalar(jple, "java.lang.Double", "probability", jmt_fmt(prs[e]));
1737 }
1738 }
1739 }
1740 }
1741
1742 /**
1743 * Port of `saveJoinStrategy`.
1744 *
1745 * `numRequired` is -1 for a standard join -- JMT's "every sibling" -- and
1746 * the quorum for a partial one. The reference reads two separate fields
1747 * (`fanIn` and `joinRequired`); this port's `JoinDecl` carries the quorum
1748 * once, with 0 meaning "every sibling", which is the same information.
1749 */
1750 void save_join_strategy(xml::Element& section, std::size_t ind) {
1751 const auto it = sn_.joindecl.find(ind);
1752 const bool partial =
1753 it != sn_.joindecl.end() && it->second.strategy == JoinStrategy::PARTIAL;
1754 const double quorum = it != sn_.joindecl.end() ? it->second.quorum : 0.0;
1755 xml::Element& p =
1756 jmt_param(section, "jmt.engine.NetStrategies.JoinStrategy", "JoinStrategy", true);
1757 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
1758 jmt_ref_class(p, cname(r));
1759 xml::Element& sp = p.add_child("subParameter");
1760 if (partial) {
1761 sp.set_attr("classPath", "jmt.engine.NetStrategies.JoinStrategies.PartialJoin");
1762 sp.set_attr("name", "Quorum");
1763 } else {
1764 sp.set_attr("classPath", "jmt.engine.NetStrategies.JoinStrategies.NormalJoin");
1765 sp.set_attr("name", "Standard Join");
1766 }
1767 const double req = partial ? quorum : (quorum > 0.0 ? quorum : -1.0);
1768 jmt_scalar(sp, "java.lang.Integer", "numRequired", jmt_int(req));
1769 }
1770 }
1771
1772 /** Port of `saveLogTunnel`: the ten fields JMT's LogTunnel section takes. */
1773 void save_log_tunnel(xml::Element& section, std::size_t ind) {
1774 const qn::NodeDef::LoggerParam& lg = sn_.nodes[ind - 1].logger;
1775 std::string path = lg.file_path.empty() ? sn_.log_path : lg.file_path;
1776 if (!path.empty() && path[path.size() - 1] != '/') path += '/';
1777 const char* bools[7] = {"logExecTimestamp", "logLoggerName", "logTimeStamp",
1778 "logJobID", "logJobClass", "logTimeSameClass",
1779 "logTimeAnyClass"};
1780 const bool vals[7] = {lg.start_time, lg.logger_name, lg.timestamp, lg.job_id,
1781 lg.job_class, lg.time_same_class, lg.time_any_class};
1782 jmt_param_value(section, "java.lang.String", "logfileName", lg.file_name);
1783 jmt_param_value(section, "java.lang.String", "logfilePath", path);
1784 for (int j = 0; j < 7; ++j)
1785 jmt_param_value(section, "java.lang.Boolean", bools[j], vals[j] ? "true" : "false");
1786 jmt_param_value(section, "java.lang.Integer", "numClasses",
1787 jmt_int(static_cast<double>(sn_.nclasses)));
1788 }
1789
1790 // -- model-level blocks -------------------------------------------------
1791
1792 /**
1793 * Port of `saveMetrics`: one `<measure>` per enabled (station, class, kind),
1794 * plus the FCR and cache-hit-rate measures.
1795 *
1796 * `Residence Time` is NOT requested, as in the reference: JMT's definition
1797 * of it disagrees with LINE's on class-switching models, and the residence
1798 * time is recomputed from the response time and the visits instead.
1799 */
1800 void save_metrics(xml::Element& sim) {
1804 for (int k = 0; k < 5; ++k) save_metric(sim, kinds[k]);
1805 // Tardiness needs a class deadline, which this port's struct does not
1806 // carry; the measures are therefore not requested. See save_classes.
1807 save_fcr_metrics(sim);
1808 save_cache_hit_rate_metrics(sim);
1809 }
1810
1811 void save_metric(xml::Element& sim, JmtMetricKind kind) {
1812 for (std::size_t ist = 1; ist <= sn_.nstations; ++ist)
1813 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
1814 if (!keep_[r - 1]) continue;
1815 if (!jmt_metric_enabled(sn_, kind, ist, r, cacheclass_)) continue;
1816 xml::Element& m = sim.add_child("measure");
1817 m.set_attr("alpha", jmt_sig2(1.0 - opt_.sim_conf_int));
1818 m.set_attr("name", std::string("Performance_") +
1819 jmt_int(static_cast<double>(ist)));
1820 m.set_attr("nodeType", "station");
1821 m.set_attr("precision", jmt_sig2(opt_.sim_max_rel_err));
1822 m.set_attr("referenceNode", nname(sn_.station_to_node[ist - 1]));
1823 m.set_attr("referenceUserClass", cname(r));
1824 m.set_attr("type", jmt_metric_text(kind));
1825 m.set_attr("verbose", "false");
1826 }
1827 }
1828
1829 /** Port of `saveCacheHitRateMetrics`. */
1830 void save_cache_hit_rate_metrics(xml::Element& sim) {
1831 for (const auto& kv : sn_.nodeparam) {
1832 const std::size_t ind = kv.first;
1833 if (sn_.nodes[ind - 1].nodetype != NodeType::Cache) continue;
1834 const qn::CacheParam<T>& cp = kv.second;
1835 for (std::size_t r = 0; r < cp.hitclass.size(); ++r) {
1836 if (cp.hitclass[r] == 0) continue;
1837 xml::Element& m = sim.add_child("measure");
1838 m.set_attr("alpha", jmt_sig2(1.0 - opt_.sim_conf_int));
1839 m.set_attr("name", "CacheHitRate_" + nname(ind) + "_" + cname(cp.hitclass[r]));
1840 m.set_attr("nodeType", "station");
1841 m.set_attr("precision", jmt_sig2(opt_.sim_max_rel_err));
1842 m.set_attr("referenceNode", nname(ind));
1843 m.set_attr("referenceUserClass", cname(cp.hitclass[r]));
1844 m.set_attr("type", "Cache Hit Rate");
1845 m.set_attr("verbose", "false");
1846 }
1847 }
1848 }
1849
1850 /** Port of `saveFCRMetrics`: six aggregate measures per region. */
1851 void save_fcr_metrics(xml::Element& sim) {
1852 static const char* kinds[6] = {"Number of Customers", "Response Time", "Residence Time",
1853 "Throughput", "FCR Capacity", "FCR Memory"};
1854 int counter = 0;
1855 for (std::size_t f = 1; f <= sn_.regions.size(); ++f) {
1856 const std::string fcr = "FCRegion" + jmt_int(static_cast<double>(f));
1857 for (int k = 0; k < 6; ++k) {
1858 std::string flat(kinds[k]);
1859 flat.erase(std::remove(flat.begin(), flat.end(), ' '), flat.end());
1860 xml::Element& m = sim.add_child("measure");
1861 m.set_attr("alpha", jmt_sig2(1.0 - opt_.sim_conf_int));
1862 m.set_attr("name", "FCR_" + fcr + "_" + flat + "_" +
1863 jmt_int(static_cast<double>(counter)));
1864 m.set_attr("nodeType", "region");
1865 m.set_attr("precision", jmt_sig2(opt_.sim_max_rel_err));
1866 m.set_attr("referenceNode", fcr);
1867 m.set_attr("referenceUserClass", "");
1868 m.set_attr("type", kinds[k]);
1869 m.set_attr("verbose", "false");
1870 ++counter;
1871 }
1872 }
1873 }
1874
1875 /** Port of `saveLinks`: one `<connection>` per linked ordered pair. */
1876 void save_links(xml::Element& sim) {
1877 // Column-major, as MATLAB's `find` on the connection matrix returns.
1878 for (std::size_t j = 1; j <= sn_.nodes.size(); ++j)
1879 for (std::size_t i = 1; i <= sn_.nodes.size(); ++i) {
1880 if (!conn_[i - 1][j - 1]) continue;
1881 xml::Element& c = sim.add_child("connection");
1882 c.set_attr("source", nname(i));
1883 c.set_attr("target", nname(j));
1884 }
1885 }
1886
1887 /**
1888 * Port of the `preload` block of `writeJSIM`: where the jobs start.
1889 *
1890 * A Source has an infinite reservoir and a Join holds no jobs, so neither
1891 * takes a preload. Every other station reports its per-class count, and a
1892 * closed class with no customers anywhere is omitted -- JMT reads an
1893 * omitted class as zero, and writing the zero would also declare the class
1894 * present at a station it never visits.
1895 */
1896 void save_preload(xml::Element& sim) {
1897 // Collected before anything is emitted: the block is omitted entirely
1898 // when no station takes a preload, and an element already appended to
1899 // the document cannot be withdrawn.
1900 struct Row {
1901 std::size_t node;
1902 std::vector<std::pair<std::size_t, double>> pops; // (class, count)
1903 };
1904 std::vector<Row> rows;
1905 for (std::size_t ist = 1; ist <= sn_.nstations; ++ist) {
1906 const std::size_t ind = sn_.station_to_node[ist - 1];
1907 const NodeType ty = sn_.nodes[ind - 1].nodetype;
1908 if (ty == NodeType::Source || ty == NodeType::Join) continue;
1909 const std::vector<double> nir = initial_marginal(ist);
1910 Row row;
1911 row.node = ind;
1912 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
1913 const double n = sn_.classes[r - 1].population;
1914 if (std::isfinite(n) && n == 0.0 && nir[r - 1] == 0.0) continue;
1915 row.pops.emplace_back(r, nir[r - 1]);
1916 }
1917 if (!row.pops.empty()) rows.push_back(row);
1918 }
1919 if (rows.empty()) return;
1920 xml::Element& preload = sim.add_child("preload");
1921 for (const Row& row : rows) {
1922 xml::Element& st = preload.add_child("stationPopulations");
1923 st.set_attr("stationName", nname(row.node));
1924 for (const auto& pc : row.pops) {
1925 xml::Element& cp = st.add_child("classPopulation");
1926 cp.set_attr("population", jmt_int(pc.second));
1927 cp.set_attr("refClass", cname(pc.first));
1928 }
1929 }
1930 }
1931
1932 /**
1933 * The per-class job count a station starts with.
1934 *
1935 * A Place's declared initial marking is its token count; every other
1936 * station takes `model.initDefault`, which puts each closed class's whole
1937 * population at its reference station and nothing anywhere else. Open
1938 * classes start empty.
1939 */
1940 std::vector<double> initial_marginal(std::size_t ist) const {
1941 std::vector<double> nir(sn_.nclasses, 0.0);
1942 const std::size_t ind = sn_.station_to_node[ist - 1];
1943 const auto im = sn_.initmarking.find(ind);
1944 if (im != sn_.initmarking.end()) {
1945 for (std::size_t r = 0; r < sn_.nclasses && r < im->second.size(); ++r)
1946 nir[r] = d(im->second[r]);
1947 return nir;
1948 }
1949 for (std::size_t r = 0; r < sn_.nclasses; ++r) {
1950 const double n = sn_.classes[r].population;
1951 if (std::isfinite(n) && sn_.classes[r].refstat == ist) nir[r] = n;
1952 }
1953 return nir;
1954 }
1955
1956 // -- Place (Storage / Linkage) ------------------------------------------
1957
1958 /** Port of `saveTotalCapacity`: the token bound of a place, -1 unbounded. */
1959 void save_total_capacity(xml::Element& section, std::size_t ind) {
1960 const std::size_t ist = sn_.nodes[ind - 1].station;
1961 const std::string v =
1962 (ist == 0 || !std::isfinite(sn_.cap[ist - 1])) ? "-1" : jmt_int(sn_.cap[ist - 1]);
1963 jmt_param_value(section, "java.lang.Integer", "totalCapacity", v);
1964 }
1965
1966 /** Port of `savePlaceCapacities`: the per-colour token bound. */
1967 void save_place_capacities(xml::Element& section, std::size_t ind) {
1968 const std::size_t ist = sn_.nodes[ind - 1].station;
1969 xml::Element& p = jmt_param(section, "java.lang.Integer", "capacities", true);
1970 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
1971 if (!keep_[r - 1]) continue;
1972 jmt_ref_class(p, cname(r));
1973 const double c = ist == 0 ? std::numeric_limits<double>::infinity()
1974 : sn_.classcap[ist - 1][r - 1];
1975 xml::Element& sp = p.add_child("subParameter");
1976 sp.set_attr("classPath", "java.lang.Integer");
1977 sp.set_attr("name", "capacity");
1978 sp.add_text_child("value", std::isfinite(c) ? jmt_int(c) : "-1");
1979 }
1980 }
1981
1982 /**
1983 * Port of `saveDropRule`. It differs from `saveDropStrategy` only in the
1984 * parameter names JMT's Storage section expects (`dropRules`/`dropRule`).
1985 */
1986 void save_drop_rule(xml::Element& section, std::size_t ind) {
1987 const std::size_t ist = sn_.nodes[ind - 1].station;
1988 xml::Element& p = jmt_param(section, "java.lang.String", "dropRules", true);
1989 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
1990 if (!keep_[r - 1]) continue;
1991 jmt_ref_class(p, cname(r));
1992 xml::Element& sp = p.add_child("subParameter");
1993 sp.set_attr("classPath", "java.lang.String");
1994 sp.set_attr("name", "dropRule");
1995 sp.add_text_child("value", drop_strategy_text(ist, r));
1996 }
1997 }
1998
1999 /**
2000 * Port of `savePutStrategies` (plural), the Storage section's insertion
2001 * rule. It offers only the three orders a place can hold tokens in, which
2002 * is why it is a separate handler from `savePutStrategy`.
2003 */
2004 void save_put_strategies(xml::Element& section, std::size_t ind) {
2005 const std::size_t ist = sn_.nodes[ind - 1].station;
2006 xml::Element& p = jmt_param(section, "jmt.engine.NetStrategies.QueuePutStrategy",
2007 "QueuePutStrategy", true);
2008 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2009 if (!keep_[r - 1]) continue;
2010 jmt_ref_class(p, cname(r));
2011 const char* nm = "TailStrategy";
2012 if (ist != 0) {
2013 if (sn_.stations[ist - 1].sched == SchedStrategy::SIRO) nm = "RandStrategy";
2014 else if (sn_.stations[ist - 1].sched == SchedStrategy::LCFS) nm = "HeadStrategy";
2015 }
2016 xml::Element& sp = p.add_child("subParameter");
2017 sp.set_attr("classPath",
2018 std::string("jmt.engine.NetStrategies.QueuePutStrategies.") + nm);
2019 sp.set_attr("name", nm);
2020 }
2021 }
2022
2023 // -- Transition (Enabling / Timing / Firing) ----------------------------
2024
2025 /** The transition parameters of node `ind`; refuses a node that has none. */
2026 const qn::TransitionParam<T>& transparam(std::size_t ind) const {
2027 const auto it = sn_.transparam.find(ind);
2028 if (it == sn_.transparam.end())
2029 throw InputError("SolverJMT: transition '" + nname(ind) + "' carries no mode table");
2030 return it->second;
2031 }
2032
2033 /**
2034 * Port of `saveEnablingConditions`.
2035 *
2036 * A place appears in the vector only when it carries a POSITIVE enabling or
2037 * inhibiting entry for the mode IN SOME CLASS. An enabling entry of infinity
2038 * is JMT's -1, "any number of tokens".
2039 *
2040 * THE ENTRIES ARE PER CLASS, which is what JSIM's own vector format is: one
2041 * `enablingEntry` per class, in class order. Writing the same number under
2042 * every class -- what this writer did while the struct had no class
2043 * dimension -- exported a net that demands every colour on every arc, and
2044 * that is why a multiclass net had to be refused here.
2045 */
2046 void save_enabling_conditions(xml::Element& section, std::size_t ind) {
2047 const qn::TransitionParam<T>& tp = transparam(ind);
2048 xml::Element& p =
2049 jmt_param(section, "jmt.engine.NetStrategies.TransitionUtilities.TransitionMatrix",
2050 "enablingConditions", true);
2051 for (std::size_t m = 0; m < tp.nmodes; ++m) {
2052 xml::Element& cond = p.add_child("subParameter");
2053 cond.set_attr("classPath",
2054 "jmt.engine.NetStrategies.TransitionUtilities.TransitionMatrix");
2055 cond.set_attr("name", "enablingCondition");
2056 xml::Element& vecs = jmt_param_sub(
2057 cond, "jmt.engine.NetStrategies.TransitionUtilities.TransitionVector",
2058 "enablingVectors");
2059 for (std::size_t k = 1; k <= sn_.nodes.size(); ++k) {
2060 bool relevant = false;
2061 for (std::size_t r = 1; r <= sn_.nclasses && !relevant; ++r) {
2062 const double en = arc(tp.enabling, m, k, r);
2063 const double in = arc(tp.inhibiting, m, k, r);
2064 relevant = (std::isfinite(en) && en > 0.0) ||
2065 (std::isfinite(in) && in > 0.0);
2066 }
2067 if (!relevant) continue;
2068 xml::Element& vec = vecs.add_child("subParameter");
2069 vec.set_attr("classPath",
2070 "jmt.engine.NetStrategies.TransitionUtilities.TransitionVector");
2071 vec.set_attr("name", "enablingVector");
2072 jmt_scalar(vec, "java.lang.String", "stationName", nname(k));
2073 xml::Element& entries = jmt_param_sub(vec, "java.lang.Integer", "enablingEntries");
2074 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2075 const double en = arc(tp.enabling, m, k, r);
2076 jmt_ref_class(entries, cname(r));
2077 xml::Element& e = entries.add_child("subParameter");
2078 e.set_attr("classPath", "java.lang.Integer");
2079 e.set_attr("name", "enablingEntry");
2080 e.add_text_child("value", std::isfinite(en) ? jmt_int(en) : "-1");
2081 }
2082 }
2083 }
2084 }
2085
2086 /**
2087 * Port of `saveInhibitingConditions`.
2088 *
2089 * The vectors cover the transition's INPUT places only, and an infinite
2090 * entry -- "no inhibitor arc" -- is written as 0, which is what JMT reads as
2091 * "never inhibits". Note that the reference's sentinel is inverted between
2092 * the two handlers: infinity is -1 in the enabling block and 0 here.
2093 */
2094 void save_inhibiting_conditions(xml::Element& section, std::size_t ind) {
2095 const qn::TransitionParam<T>& tp = transparam(ind);
2096 const std::vector<std::size_t> inputs = inputs_of(ind);
2097 xml::Element& p =
2098 jmt_param(section, "jmt.engine.NetStrategies.TransitionUtilities.TransitionMatrix",
2099 "inhibitingConditions", true);
2100 for (std::size_t m = 0; m < tp.nmodes; ++m) {
2101 xml::Element& cond = p.add_child("subParameter");
2102 cond.set_attr("classPath",
2103 "jmt.engine.NetStrategies.TransitionUtilities.TransitionMatrix");
2104 cond.set_attr("name", "inhibitingCondition");
2105 xml::Element& vecs = jmt_param_sub(
2106 cond, "jmt.engine.NetStrategies.TransitionUtilities.TransitionVector",
2107 "inhibitingVectors");
2108 for (std::size_t k : inputs) {
2109 xml::Element& vec = vecs.add_child("subParameter");
2110 vec.set_attr("classPath",
2111 "jmt.engine.NetStrategies.TransitionUtilities.TransitionVector");
2112 vec.set_attr("name", "inhibitingVector");
2113 jmt_scalar(vec, "java.lang.String", "stationName", nname(k));
2114 xml::Element& entries = jmt_param_sub(vec, "java.lang.Integer", "inhibitingEntries");
2115 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2116 const double in = arc(tp.inhibiting, m, k, r);
2117 jmt_ref_class(entries, cname(r));
2118 xml::Element& e = entries.add_child("subParameter");
2119 e.set_attr("classPath", "java.lang.Integer");
2120 e.set_attr("name", "inhibitingEntry");
2121 e.add_text_child("value", std::isfinite(in) ? jmt_int(in) : "0");
2122 }
2123 }
2124 }
2125 }
2126
2127 /** Port of `saveFiringOutcomes`: the tokens a firing puts in each output. */
2128 void save_firing_outcomes(xml::Element& section, std::size_t ind) {
2129 const qn::TransitionParam<T>& tp = transparam(ind);
2130 const std::vector<std::size_t> outs = outputs_of(ind);
2131 xml::Element& p =
2132 jmt_param(section, "jmt.engine.NetStrategies.TransitionUtilities.TransitionMatrix",
2133 "firingOutcomes", true);
2134 for (std::size_t m = 0; m < tp.nmodes; ++m) {
2135 xml::Element& out = p.add_child("subParameter");
2136 out.set_attr("classPath",
2137 "jmt.engine.NetStrategies.TransitionUtilities.TransitionMatrix");
2138 out.set_attr("name", "firingOutcome");
2139 xml::Element& vecs = jmt_param_sub(
2140 out, "jmt.engine.NetStrategies.TransitionUtilities.TransitionVector",
2141 "firingVectors");
2142 for (std::size_t k : outs) {
2143 xml::Element& vec = vecs.add_child("subParameter");
2144 vec.set_attr("classPath",
2145 "jmt.engine.NetStrategies.TransitionUtilities.TransitionVector");
2146 vec.set_attr("name", "firingVector");
2147 jmt_scalar(vec, "java.lang.String", "stationName", nname(k));
2148 xml::Element& entries = jmt_param_sub(vec, "java.lang.Integer", "firingEntries");
2149 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2150 const double f = arc(tp.firing, m, k, r);
2151 jmt_ref_class(entries, cname(r));
2152 xml::Element& e = entries.add_child("subParameter");
2153 e.set_attr("classPath", "java.lang.Integer");
2154 e.set_attr("name", "firingEntry");
2155 e.add_text_child("value", jmt_int(f));
2156 }
2157 }
2158 }
2159 }
2160
2161 /** `enabling`/`inhibiting`/`firing` of mode m at 1-based node k, class r. */
2162 double arc(const std::vector<Matrix<T>>& tab, std::size_t m, std::size_t k,
2163 std::size_t r) const {
2164 if (m >= tab.size() || k == 0 || k > tab[m].rows() || r == 0 || r > tab[m].cols())
2165 return 0.0;
2166 return d(tab[m](k - 1, r - 1));
2167 }
2168
2169 /** Port of `saveModeNames`. */
2170 void save_mode_names(xml::Element& section, std::size_t ind) {
2171 const qn::TransitionParam<T>& tp = transparam(ind);
2172 xml::Element& p = jmt_param(section, "java.lang.String", "modeNames", true);
2173 for (std::size_t m = 0; m < tp.nmodes; ++m) {
2174 xml::Element& sp = p.add_child("subParameter");
2175 sp.set_attr("classPath", "java.lang.String");
2176 sp.set_attr("name", "modeName");
2177 sp.add_text_child("value", m < tp.modenames.size() ? tp.modenames[m] : std::string());
2178 }
2179 }
2180
2181 /** Port of `saveNumbersOfServers`: the firing concurrency of each mode. */
2182 void save_numbers_of_servers(xml::Element& section, std::size_t ind) {
2183 const qn::TransitionParam<T>& tp = transparam(ind);
2184 xml::Element& p = jmt_param(section, "java.lang.Integer", "numbersOfServers", true);
2185 for (std::size_t m = 0; m < tp.nmodes; ++m) {
2186 const double ns = m < tp.nmodeservers.size() ? tp.nmodeservers[m] : 1.0;
2187 xml::Element& sp = p.add_child("subParameter");
2188 sp.set_attr("classPath", "java.lang.Integer");
2189 sp.set_attr("name", "numberOfServers");
2190 sp.add_text_child("value", std::isfinite(ns) ? jmt_int(ns) : "-1");
2191 }
2192 }
2193
2194 /**
2195 * Port of `saveTimingStrategies`.
2196 *
2197 * An immediate mode is a ZeroServiceTimeStrategy and carries no
2198 * distribution; a timed one carries its firing process through the shared
2199 * emitter, under JMT's `timingStrategy` name rather than the
2200 * `ServiceTimeStrategy` name a queue's service uses.
2201 */
2202 void save_timing_strategies(xml::Element& section, std::size_t ind) {
2203 const qn::TransitionParam<T>& tp = transparam(ind);
2204 xml::Element& p = jmt_param(section, "jmt.engine.NetStrategies.ServiceStrategy",
2205 "timingStrategies", true);
2206 for (std::size_t m = 0; m < tp.nmodes; ++m) {
2207 xml::Element& sp = p.add_child("subParameter");
2208 const bool immediate = m < tp.timing.size() &&
2209 tp.timing[m] == lang::TimingStrategy::IMMEDIATE;
2210 if (immediate) {
2211 sp.set_attr("classPath",
2212 "jmt.engine.NetStrategies.ServiceStrategies.ZeroServiceTimeStrategy");
2213 sp.set_attr("name", "ZeroServiceTimeStrategy");
2214 continue;
2215 }
2216 sp.set_attr("classPath",
2217 "jmt.engine.NetStrategies.ServiceStrategies.ServiceTimeStrategy");
2218 sp.set_attr("name", "timingStrategy");
2219 if (m >= tp.firingproc.size())
2220 throw InputError("SolverJMT: timed mode '" +
2221 (m < tp.modenames.size() ? tp.modenames[m] : std::string()) +
2222 "' of transition '" + nname(ind) + "' has no firing process");
2223 jmt_append_distribution(sp, jmt_dist_view(tp.firingproc[m]),
2224 "SolverJMT (transition firing)");
2225 }
2226 }
2227
2228 /** Port of `saveFiringPriorities`. */
2229 void save_firing_priorities(xml::Element& section, std::size_t ind) {
2230 const qn::TransitionParam<T>& tp = transparam(ind);
2231 xml::Element& p = jmt_param(section, "java.lang.Integer", "firingPriorities", true);
2232 for (std::size_t m = 0; m < tp.nmodes; ++m) {
2233 const double v = m < tp.firingprio.size() ? tp.firingprio[m] : 1.0;
2234 xml::Element& sp = p.add_child("subParameter");
2235 sp.set_attr("classPath", "java.lang.Integer");
2236 sp.set_attr("name", "firingPriority");
2237 sp.add_text_child("value", std::isfinite(v) ? jmt_int(v) : "-1");
2238 }
2239 }
2240
2241 /**
2242 * Port of `saveFiringWeights`.
2243 *
2244 * THE REFERENCE PRINTS THE WEIGHT AS AN INTEGER (`int2str`) although the
2245 * parameter is a `java.lang.Double`, so a weight of 0.3 exports as 0 and
2246 * the mode never wins a race it should sometimes win. That is a defect and
2247 * not a convention -- no other weight in the file is rounded -- so this
2248 * port writes the weight itself.
2249 */
2250 void save_firing_weights(xml::Element& section, std::size_t ind) {
2251 const qn::TransitionParam<T>& tp = transparam(ind);
2252 xml::Element& p = jmt_param(section, "java.lang.Double", "firingWeights", true);
2253 for (std::size_t m = 0; m < tp.nmodes; ++m) {
2254 const double v = m < tp.fireweight.size() ? d(tp.fireweight[m]) : 1.0;
2255 xml::Element& sp = p.add_child("subParameter");
2256 sp.set_attr("classPath", "java.lang.Double");
2257 sp.set_attr("name", "firingWeight");
2258 sp.add_text_child("value", std::isfinite(v) ? jmt_fmt(v) : "-1");
2259 }
2260 }
2261
2262 // -- blocking regions ---------------------------------------------------
2263
2264 /**
2265 * Port of `jmtClassCapCon`: the per-(station, class) capacities that are a
2266 * REAL constraint and must therefore reach JMT as a blocking region.
2267 *
2268 * A capacity is real only when it binds: not at a Source or a Place, not on
2269 * a pair the class never visits, and not when it already exceeds the
2270 * station's own capacity or the population of the class's chain -- such a
2271 * bound can never be reached, and exporting it as a region would add a
2272 * region JMT then reports measures for.
2273 */
2274 std::vector<std::vector<double>> class_cap_constraints() const {
2275 const double inf = std::numeric_limits<double>::infinity();
2276 std::vector<std::vector<double>> con(sn_.nstations, std::vector<double>(sn_.nclasses, inf));
2277 if (sn_.classcap.empty()) return con;
2278 std::vector<double> chainpop(sn_.nclasses, inf);
2279 for (std::size_t c = 0; c < sn_.inchain.size(); ++c) {
2280 double tot = 0.0;
2281 bool open = false;
2282 for (std::size_t r : sn_.inchain[c]) {
2283 const double n = sn_.classes[r - 1].population;
2284 if (std::isfinite(n)) tot += n; else open = true;
2285 }
2286 for (std::size_t r : sn_.inchain[c]) chainpop[r - 1] = open ? inf : tot;
2287 }
2288 for (std::size_t ist = 1; ist <= sn_.nstations; ++ist) {
2289 const NodeType ty = sn_.nodes[sn_.station_to_node[ist - 1] - 1].nodetype;
2290 if (ty == NodeType::Source || ty == NodeType::Place) continue;
2291 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2292 if (sn_.disabled[ist - 1][r - 1]) continue;
2293 const double cc = sn_.classcap[ist - 1][r - 1];
2294 if (std::isfinite(cc) && cc < 2147483647.0 &&
2295 cc < std::min(sn_.cap[ist - 1], chainpop[r - 1]))
2296 con[ist - 1][r - 1] = cc;
2297 }
2298 }
2299 return con;
2300 }
2301
2302 /**
2303 * Port of `jmtClassCapAssert`: the two per-class capacities JMT cannot
2304 * reproduce, refused by name rather than exported as something else.
2305 *
2306 * A CLOSED class is the first. LINE holds a blocked closed job at its
2307 * upstream station; JMT's blocking region parks it in the region's input
2308 * station instead, which frees the upstream server and loses the job from
2309 * the population count -- a different model, not a different rounding.
2310 * A non-loss drop strategy is the second: a region can only drop or defer.
2311 */
2312 void assert_class_cap_exportable(std::size_t ist, std::size_t r) const {
2313 const std::string sname = nname(sn_.station_to_node[ist - 1]);
2314 if (std::isfinite(sn_.classes[r - 1].population))
2315 throw UnsupportedError(
2316 "SolverJMT: station '" + sname + "' carries a finite capacity " +
2317 jmt_int(sn_.classcap[ist - 1][r - 1]) + " for the closed class '" + cname(r) +
2318 "'. LINE holds a blocked closed job at its upstream station, whereas JMT can "
2319 "only express a per-class capacity as a blocking region, which parks the job in "
2320 "the region input station instead, freeing the upstream server and losing it "
2321 "from the population count. Use SolverCTMC, SolverSSA or SolverLDES, or express "
2322 "the limit as the station capacity");
2323 const DropStrategy dr = sn_.droprule[ist - 1][r - 1];
2324 if (dr == DropStrategy::BAS || dr == DropStrategy::BBS || dr == DropStrategy::RSRD ||
2325 dr == DropStrategy::RETRIAL || dr == DropStrategy::RETRIAL_WITH_LIMIT)
2326 throw UnsupportedError(
2327 "SolverJMT: station '" + sname + "' applies drop strategy '" + jmt_drop_text(dr) +
2328 "' to class '" + cname(r) +
2329 "' and also carries a finite capacity for it. JMT exports a per-class capacity "
2330 "as a blocking region, which can only drop or defer an arrival. Remove the "
2331 "per-class capacity, or use the station capacity, which is exported with its "
2332 "drop strategy");
2333 }
2334
2335 /**
2336 * Port of `saveRegions`: three kinds of `<blockingRegion>`, in this order.
2337 *
2338 * 1. ONE PER LPS STATION. Limited processor sharing is a cap on the number
2339 * IN SERVICE, which JMT has no station-level field for; the single-node
2340 * region is how the limit is expressed, and it is why
2341 * `save_number_of_servers` exports an LPS station as a single server.
2342 * 2. The model's declared finite capacity regions.
2343 * 3. ONE PER STATION whose per-class capacity is a real constraint and that
2344 * no region above already covers, since JMT allows a node to belong to
2345 * at most one region.
2346 */
2347 void save_regions(xml::Element& sim) {
2348 const std::vector<std::vector<double>> con = class_cap_constraints();
2349 std::vector<bool> covered(sn_.nstations, false);
2350 std::size_t lps_idx = sn_.regions.size();
2351
2352 for (std::size_t ist = 1; ist <= sn_.nstations; ++ist) {
2353 if (sn_.stations[ist - 1].sched != SchedStrategy::LPS) continue;
2354 ++lps_idx;
2355 const std::size_t ind = sn_.station_to_node[ist - 1];
2356 const double limit = sn_.stations[ist - 1].schedparam.empty()
2357 ? 1.0
2358 : d(sn_.stations[ist - 1].schedparam[0]);
2359 xml::Element& br = sim.add_child("blockingRegion");
2360 br.set_attr("name", "LPSRegion" + jmt_int(static_cast<double>(lps_idx)));
2361 br.set_attr("type", "default");
2362 br.add_child("regionNode").set_attr("nodeName", nname(ind));
2363 br.add_child("globalConstraint").set_attr("maxJobs", jmt_num(limit));
2364 br.add_child("globalMemoryConstraint").set_attr("maxMemory", "-1");
2365 covered[ist - 1] = true;
2366 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2367 if (!std::isfinite(con[ist - 1][r - 1])) continue;
2368 assert_class_cap_exportable(ist, r); // rejects the non-loss cases first
2369 throw UnsupportedError(
2370 "SolverJMT: station '" + nname(ind) +
2371 "' has both LPS scheduling and a finite capacity for the open class '" +
2372 cname(r) +
2373 "'. JMT expresses both through a single blocking region, which admits only "
2374 "one drop rule per class, but LPS requires blocking while the open-class "
2375 "capacity requires dropping. Remove the per-class capacity or use a non-LPS "
2376 "scheduling strategy");
2377 }
2378 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2379 xml::Element& dr = br.add_child("dropRules");
2380 dr.set_attr("jobClass", cname(r));
2381 dr.set_attr("dropThisClass", "false");
2382 }
2383 }
2384
2385 for (std::size_t f = 1; f <= sn_.regions.size(); ++f) {
2386 const typename qn::NetworkStruct<T>::Region& rg = sn_.regions[f - 1];
2387 std::vector<std::size_t> members;
2388 for (std::size_t ist = 1; ist <= sn_.nstations; ++ist)
2389 if (ist <= rg.members.size() && rg.members[ist - 1]) {
2390 members.push_back(ist);
2391 covered[ist - 1] = true;
2392 }
2393 std::vector<double> region_class_cap(sn_.nclasses,
2394 std::numeric_limits<double>::infinity());
2395 for (std::size_t ist : members)
2396 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2397 if (!std::isfinite(con[ist - 1][r - 1])) continue;
2398 if (members.size() > 1)
2399 throw UnsupportedError(
2400 "SolverJMT: station '" + nname(sn_.station_to_node[ist - 1]) +
2401 "' carries a finite capacity for class '" + cname(r) +
2402 "' and also belongs to the multi-station region FCRegion" +
2403 jmt_int(static_cast<double>(f)) +
2404 ". JMT constrains a blocking region as a whole and allows a node to "
2405 "belong to only one region, so a per-station class capacity cannot "
2406 "be expressed alongside it");
2407 assert_class_cap_exportable(ist, r);
2408 if (!(r <= rg.rule.size() && rg.rule[r - 1] == DropStrategy::DROP))
2409 throw UnsupportedError(
2410 "SolverJMT: station '" + nname(sn_.station_to_node[ist - 1]) +
2411 "' carries a finite capacity for class '" + cname(r) +
2412 "' and also belongs to region FCRegion" +
2413 jmt_int(static_cast<double>(f)) +
2414 ", whose drop rule for that class is not DROP. A JMT blocking "
2415 "region admits a single drop rule per class, shared by all of its "
2416 "constraints, and the per-class capacity of an open class is a loss "
2417 "constraint");
2418 region_class_cap[r - 1] = con[ist - 1][r - 1];
2419 }
2420
2421 xml::Element& br = sim.add_child("blockingRegion");
2422 br.set_attr("name", "FCRegion" + jmt_int(static_cast<double>(f)));
2423 br.set_attr("type", "default");
2424 for (std::size_t ist : members)
2425 br.add_child("regionNode")
2426 .set_attr("nodeName", nname(sn_.station_to_node[ist - 1]));
2427 // The global caps are stored per MEMBER station and are the same at
2428 // each; the first member therefore carries the region's own values.
2429 const double gmax =
2430 members.empty() ? -1.0 : rg.cap[members[0] - 1][sn_.nclasses];
2431 const double gmem = members.empty() ? -1.0 : rg.maxmem[members[0] - 1];
2432 br.add_child("globalConstraint").set_attr("maxJobs", jmt_num(gmax));
2433 br.add_child("globalMemoryConstraint").set_attr("maxMemory", jmt_num(gmem));
2434 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2435 double cmax = members.empty() ? -1.0 : rg.cap[members[0] - 1][r - 1];
2436 // -1 is the struct's "unbounded"; the per-station capacity then
2437 // stands alone, and otherwise the tighter of the two binds.
2438 if (cmax == -1.0)
2439 cmax = std::isfinite(region_class_cap[r - 1]) ? region_class_cap[r - 1] : -1.0;
2440 else if (std::isfinite(region_class_cap[r - 1]))
2441 cmax = std::min(cmax, region_class_cap[r - 1]);
2442 if (cmax == -1.0 || !std::isfinite(cmax)) continue;
2443 xml::Element& cc = br.add_child("classConstraint");
2444 cc.set_attr("jobClass", cname(r));
2445 cc.set_attr("maxJobsPerClass", jmt_num(cmax));
2446 }
2447 // NO `classMemoryConstraint` IS EMITTED. `add_region` already folds
2448 // the per-class memory budget into the per-class job cap by
2449 // dividing it by the class size, so the constraint above carries it;
2450 // emitting it again would apply the same budget twice.
2451 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2452 xml::Element& dr = br.add_child("dropRules");
2453 dr.set_attr("jobClass", cname(r));
2454 dr.set_attr("dropThisClass",
2455 (r <= rg.rule.size() && rg.rule[r - 1] == DropStrategy::DROP)
2456 ? "true"
2457 : "false");
2458 }
2459 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2460 if (r > rg.weight.size()) continue;
2461 const double w = d(rg.weight[r - 1]);
2462 if (w == 1.0) continue;
2463 xml::Element& cw = br.add_child("classWeight");
2464 cw.set_attr("jobClass", cname(r));
2465 cw.set_attr("weight", jmt_num(w));
2466 }
2467 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2468 if (r > rg.size.size()) continue;
2469 const double s = d(rg.size[r - 1]);
2470 if (s == 1.0) continue;
2471 xml::Element& cs = br.add_child("classSize");
2472 cs.set_attr("jobClass", cname(r));
2473 cs.set_attr("size", jmt_num(s));
2474 }
2475 }
2476
2477 std::size_t cap_idx = 0;
2478 for (std::size_t ist = 1; ist <= sn_.nstations; ++ist) {
2479 if (covered[ist - 1]) continue;
2480 bool any = false;
2481 for (std::size_t r = 1; r <= sn_.nclasses; ++r)
2482 if (std::isfinite(con[ist - 1][r - 1])) any = true;
2483 if (!any) continue;
2484 ++cap_idx;
2485 for (std::size_t r = 1; r <= sn_.nclasses; ++r)
2486 if (std::isfinite(con[ist - 1][r - 1])) assert_class_cap_exportable(ist, r);
2487 xml::Element& br = sim.add_child("blockingRegion");
2488 br.set_attr("name", "ClassCapRegion" + jmt_int(static_cast<double>(cap_idx)));
2489 br.set_attr("type", "default");
2490 br.add_child("regionNode")
2491 .set_attr("nodeName", nname(sn_.station_to_node[ist - 1]));
2492 br.add_child("globalConstraint").set_attr("maxJobs", "-1");
2493 br.add_child("globalMemoryConstraint").set_attr("maxMemory", "-1");
2494 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2495 if (!std::isfinite(con[ist - 1][r - 1])) continue;
2496 xml::Element& cc = br.add_child("classConstraint");
2497 cc.set_attr("jobClass", cname(r));
2498 cc.set_attr("maxJobsPerClass", jmt_num(con[ist - 1][r - 1]));
2499 }
2500 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2501 if (!std::isfinite(con[ist - 1][r - 1])) continue;
2502 xml::Element& dr = br.add_child("dropRules");
2503 dr.set_attr("jobClass", cname(r));
2504 dr.set_attr("dropThisClass", "true");
2505 }
2506 }
2507 }
2508
2509 // -- Cache --------------------------------------------------------------
2510
2511 /**
2512 * Port of `saveCacheStrategy`.
2513 *
2514 * THE LIST-TO-LIST MATRIX IS DERIVED FROM THE POLICY, not from `accost`:
2515 * LRU promotes an item one list up on a hit and the last list keeps it,
2516 * every other policy keeps it where it is. That is what the reference
2517 * emits, and JMT's own replacement object does the rest.
2518 *
2519 * SFIFO is exported as JMT's FIFO cache and every policy JMT has no object
2520 * for -- HLRU, CLIMB, QLRU -- is refused by name rather than falling back
2521 * to LRU as the reference's `otherwise` does: a q-LRU cache silently
2522 * simulated as LRU returns a hit rate that is wrong by the admission
2523 * probability, with nothing in the output to show for it.
2524 */
2525 void save_cache_strategy(xml::Element& section, std::size_t ind) {
2526 const auto it = sn_.nodeparam.find(ind);
2527 if (it == sn_.nodeparam.end())
2528 throw InputError("SolverJMT: cache node '" + nname(ind) + "' carries no parameters");
2529 const qn::CacheParam<T>& cp = it->second;
2530 const std::size_t K = sn_.nclasses;
2531
2532 jmt_param_value(section, "java.lang.Integer", "maxItems",
2533 jmt_int(static_cast<double>(cp.nitems)));
2534 xml::Element& cap = jmt_param(section, "java.lang.Integer", "cacheCapacity", true);
2535 for (std::size_t l = 0; l < cp.itemcap.size(); ++l) {
2536 xml::Element& sp = cap.add_child("subParameter");
2537 sp.set_attr("classPath", "java.lang.Integer");
2538 sp.set_attr("name", "capacity");
2539 sp.add_text_child("value", jmt_int(static_cast<double>(cp.itemcap[l])));
2540 }
2541
2542 const std::size_t nlev = cp.itemcap.size();
2543 const bool lru = cp.replacestrat == lang::ReplacementStrategy::LRU;
2544 xml::Element& mat = jmt_param(section, "java.lang.Object", "matrix", true);
2545 for (std::size_t a = 1; a <= nlev; ++a) {
2546 xml::Element& row = mat.add_child("subParameter");
2547 row.set_attr("array", "true");
2548 row.set_attr("classPath", "java.lang.Float");
2549 row.set_attr("name", "row");
2550 for (std::size_t b = 1; b <= nlev; ++b) {
2551 const bool one = lru ? (a < nlev ? b == a + 1 : b == nlev) : a == b;
2552 xml::Element& cell = row.add_child("subParameter");
2553 cell.set_attr("classPath", "java.lang.Float");
2554 cell.set_attr("name", "cell");
2555 cell.add_text_child("value", one ? "1.0" : "0.0");
2556 }
2557 }
2558
2559 xml::Element& jc = jmt_param(section, "jmt.engine.QueueNet.JobClass", "jobClasses", true);
2560 for (std::size_t r = 1; r <= K; ++r) {
2561 const bool used = (r <= cp.hitclass.size() && cp.hitclass[r - 1] > 0) ||
2562 (r <= cp.missclass.size() && cp.missclass[r - 1] > 0);
2563 if (!used) continue;
2564 xml::Element& sp = jc.add_child("subParameter");
2565 sp.set_attr("classPath", "jmt.engine.QueueNet.JobClass");
2566 sp.set_attr("name", "jobClass");
2567 sp.add_text_child("value", cname(r));
2568 }
2569 const char* switch_names[2] = {"hitClasses", "missClasses"};
2570 const char* entry_names[2] = {"hitClass", "missClass"};
2571 for (int w = 0; w < 2; ++w) {
2572 const std::vector<std::size_t>& tab = w == 0 ? cp.hitclass : cp.missclass;
2573 xml::Element& p =
2574 jmt_param(section, "jmt.engine.QueueNet.JobClass", switch_names[w], true);
2575 for (std::size_t r = 1; r <= K && r <= tab.size(); ++r) {
2576 if (tab[r - 1] == 0) continue;
2577 xml::Element& sp = p.add_child("subParameter");
2578 sp.set_attr("classPath", "jmt.engine.QueueNet.JobClass");
2579 sp.set_attr("name", entry_names[w]);
2580 sp.add_text_child("value", cname(tab[r - 1]));
2581 }
2582 }
2583
2584 const char* policy = nullptr;
2585 switch (cp.replacestrat) {
2587 policy = "jmt.engine.NetStrategies.CacheStrategies.LRUCache";
2588 break;
2591 policy = "jmt.engine.NetStrategies.CacheStrategies.FIFOCache";
2592 break;
2594 policy = "jmt.engine.NetStrategies.CacheStrategies.RandomCache";
2595 break;
2596 default:
2597 throw UnsupportedError(
2598 "SolverJMT: cache '" + nname(ind) +
2599 "' uses a replacement policy JMT has no cache object for (HLRU, CLIMB and "
2600 "QLRU); use SolverLDES, which simulates them directly");
2601 }
2602 xml::Element& rp = section.add_child("parameter");
2603 rp.set_attr("classPath", policy);
2604 rp.set_attr("name", "replacePolicy");
2605
2606 /*
2607 * The popularity is PARAMETRIC in JMT: a Zipf exponent or a uniform
2608 * range, never a pmf. `CacheParam::preadkind` is what the reader kept
2609 * for exactly this, and a class whose pmf was supplied directly exports
2610 * as `null` -- the reference's own behaviour for anything that is not a
2611 * Zipf or a DiscreteSampler.
2612 */
2613 xml::Element& pop = jmt_param(
2614 section, "jmt.engine.random.discrete.DiscreteDistribution", "popularity", true);
2615 for (std::size_t r = 1; r <= K; ++r) {
2616 jmt_ref_class(pop, cname(r));
2617 const bool reads = r <= cp.pread.size() && !cp.pread[r - 1].empty();
2618 const typename qn::CacheParam<T>::Popularity kind =
2619 r <= cp.preadkind.size() ? cp.preadkind[r - 1]
2620 : typename qn::CacheParam<T>::Popularity();
2621 xml::Element& sp = pop.add_child("subParameter");
2622 if (reads && kind.type == lang::ProcessType::ZIPF) {
2623 sp.set_attr("classPath", "jmt.engine.random.discrete.Zipf");
2624 sp.set_attr("name", "popularity");
2625 jmt_scalar(sp, "java.lang.Double", "alpha", jmt_fmt(kind.s));
2626 jmt_scalar(sp, "java.lang.Integer", "numberOfElements",
2627 jmt_int(static_cast<double>(kind.n)));
2628 } else if (reads && kind.type == lang::ProcessType::DISCRETESAMPLER) {
2629 sp.set_attr("classPath", "jmt.engine.random.discrete.Uniform");
2630 sp.set_attr("name", "popularity");
2631 jmt_scalar(sp, "java.lang.Integer", "min", "1");
2632 jmt_scalar(sp, "java.lang.Integer", "max",
2633 jmt_int(static_cast<double>(kind.n == 0 ? cp.nitems : kind.n)));
2634 } else {
2635 sp.set_attr("classPath", "jmt.engine.random.discrete.DiscreteDistribution");
2636 sp.set_attr("name", "null");
2637 sp.add_text_child("value", "null");
2638 }
2639 }
2640 }
2641
2642 // -- impatience, retrials and switchover --------------------------------
2643
2644 /**
2645 * Port of `saveImpatience`: the per-class Balking or Reneging strategy.
2646 *
2647 * BALKING AND RENEGING ARE MUTUALLY EXCLUSIVE per class here, as in the
2648 * reference: JMT's Impatience array holds one strategy per class, and
2649 * balking is checked first. A class with neither gets a Reneging strategy
2650 * whose body is the literal `null`.
2651 */
2652 void save_impatience(xml::Element& section, std::size_t ind) {
2653 const std::size_t ist = sn_.nodes[ind - 1].station;
2654 xml::Element& p = jmt_param(
2655 section, "jmt.engine.NetStrategies.ImpatienceStrategies.Impatience", "Impatience", true);
2656 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2657 if (!keep_[r - 1]) continue;
2658 jmt_ref_class(p, cname(r));
2659 xml::Element& sp = p.add_child("subParameter");
2660 const qn::Station<T>* st = ist == 0 ? nullptr : &sn_.stations[ist - 1];
2661 const bool balks = st != nullptr && r <= st->balking.size() &&
2662 st->balking[r - 1].strategy ==
2664 if (balks) {
2665 sp.set_attr("classPath", "jmt.engine.NetStrategies.ImpatienceStrategies.Balking");
2666 sp.set_attr("name", "Balking");
2667 save_balking_strategy(sp, st->balking[r - 1].thresholds, st->nservers);
2668 continue;
2669 }
2670 sp.set_attr("classPath", "jmt.engine.NetStrategies.ImpatienceStrategies.Reneging");
2671 sp.set_attr("name", "Reneging");
2672 const bool renegs = st != nullptr && r <= st->impatience.size() &&
2673 st->impatience[r - 1] == lang::ImpatienceType::RENEGING &&
2674 r <= st->patience.size() && !st->patience[r - 1].disabled;
2675 if (!renegs) {
2676 sp.add_text_child("value", "null");
2677 continue;
2678 }
2679 const JmtDistView<T> v = jmt_dist_view(st->patience[r - 1]);
2680 if (v.type == lang::ProcessType::UNIFORM) {
2681 // The reference exports a patience Uniform as [0, 2*mean],
2682 // matching the MEAN only, where the service-time emitter
2683 // matches the mean and the SCV. Kept: a patience law is
2684 // declared by its mean in every model that has one.
2685 xml::Element& dn = sp.add_child("subParameter");
2686 dn.set_attr("classPath", "jmt.engine.random.Uniform");
2687 dn.set_attr("name", "Uniform");
2688 xml::Element& par = sp.add_child("subParameter");
2689 par.set_attr("classPath", "jmt.engine.random.UniformPar");
2690 par.set_attr("name", "distrPar");
2691 jmt_scalar(par, "java.lang.Double", "min", "0.0");
2692 jmt_double(par, "max", v.rate > 0.0 ? 2.0 / v.rate : 0.0);
2693 continue;
2694 }
2695 jmt_append_distribution(sp, v, "SolverJMT (patience)");
2696 }
2697 }
2698
2699 /**
2700 * Port of `saveBalkingStrategy`.
2701 *
2702 * THE THRESHOLDS ARE SHIFTED DOWN BY THE SERVER COUNT. LINE evaluates
2703 * balking against the TOTAL station population, JMT's Balking against the
2704 * number WAITING; in the regime where balking matters the servers are busy,
2705 * so the two differ by exactly S and JMT waiting w is LINE total w + S.
2706 * A gap between two intervals gets an explicit zero-probability breakpoint,
2707 * since JMT selects the LAST range with `from <= queueLength` and would
2708 * otherwise let a queue length outside every interval inherit its
2709 * neighbour's balking probability.
2710 */
2711 void save_balking_strategy(xml::Element& balking,
2712 const std::vector<typename qn::Station<T>::BalkingThreshold>& th,
2713 double nservers) {
2714 struct Bp {
2715 double from, prob;
2716 };
2717 std::vector<typename qn::Station<T>::BalkingThreshold> sorted = th;
2718 std::sort(sorted.begin(), sorted.end(),
2719 [](const typename qn::Station<T>::BalkingThreshold& a,
2720 const typename qn::Station<T>::BalkingThreshold& b) {
2721 return a.min_jobs < b.min_jobs;
2722 });
2723 const double S =
2724 (!std::isfinite(nservers) || nservers < 1.0) ? 1.0 : nservers;
2725 std::vector<Bp> bps;
2726 for (std::size_t i = 0; i < sorted.size(); ++i) {
2727 const double lo = sorted[i].min_jobs;
2728 // -1 is the wire's unbounded upper end; every other value is a
2729 // closed interval, which is why the gap test below is `hi + 1`.
2730 const double hi = sorted[i].max_jobs < 0.0
2731 ? std::numeric_limits<double>::infinity()
2732 : sorted[i].max_jobs;
2733 const double pr = d(sorted[i].probability);
2734 bps.push_back(Bp{std::max(0.0, lo - S), pr});
2735 if (!std::isfinite(hi)) continue;
2736 const double next_lo = i + 1 < sorted.size()
2737 ? sorted[i + 1].min_jobs
2738 : std::numeric_limits<double>::infinity();
2739 if (hi + 1.0 < next_lo) bps.push_back(Bp{std::max(0.0, hi + 1.0 - S), 0.0});
2740 }
2741 xml::Element& ld = balking.add_child("subParameter");
2742 ld.set_attr("classPath", "jmt.engine.NetStrategies.ServiceStrategies.LoadDependentStrategy");
2743 ld.set_attr("name", "LoadDependentStrategy");
2744 xml::Element& arr = jmt_param_sub(
2745 ld, "jmt.engine.NetStrategies.ServiceStrategies.LDParameter", "LDParameter");
2746 for (const Bp& b : bps) {
2747 xml::Element& rn = arr.add_child("subParameter");
2748 rn.set_attr("classPath", "jmt.engine.NetStrategies.ServiceStrategies.LDParameter");
2749 rn.set_attr("name", "LDParameter");
2750 jmt_scalar(rn, "java.lang.Integer", "from", jmt_int(b.from));
2751 // A DUMMY distribution: only the `function` string below is read
2752 // for balking, but the LDParameter shape requires the pair.
2753 xml::Element& dn = rn.add_child("subParameter");
2754 dn.set_attr("classPath", "jmt.engine.random.Exponential");
2755 dn.set_attr("name", "Exponential");
2756 xml::Element& par = rn.add_child("subParameter");
2757 par.set_attr("classPath", "jmt.engine.random.ExponentialPar");
2758 par.set_attr("name", "distrPar");
2759 jmt_scalar(par, "java.lang.Double", "lambda", "1.0");
2760 jmt_scalar(rn, "java.lang.String", "function", jmt_fmt(b.prob));
2761 }
2762 jmt_scalar(balking, "java.lang.Boolean", "priorityActivated", "false");
2763 }
2764
2765 /**
2766 * Port of `saveRetrialDistributions`: the orbit delay of each class.
2767 *
2768 * A class with no orbit still gets an entry -- an Exp(1) placeholder -- so
2769 * that JMT's per-class array stays aligned; omitting it would shift every
2770 * later class's retrial delay onto the wrong class.
2771 */
2772 void save_retrial_distributions(xml::Element& section, std::size_t ind) {
2773 const std::size_t ist = sn_.nodes[ind - 1].station;
2774 const auto it = ist == 0 ? sn_.retrialparam.end() : sn_.retrialparam.find(ist);
2775 xml::Element& p = jmt_param(section, "jmt.engine.NetStrategies.ServiceStrategy",
2776 "retrialDistributions", true);
2777 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2778 if (!keep_[r - 1]) continue;
2779 jmt_ref_class(p, cname(r));
2780 xml::Element& sts = p.add_child("subParameter");
2781 sts.set_attr("classPath",
2782 "jmt.engine.NetStrategies.ServiceStrategies.ServiceTimeStrategy");
2783 sts.set_attr("name", "ServiceTimeStrategy");
2784 const bool has = it != sn_.retrialparam.end() &&
2785 r <= it->second.retrial_proc.size() &&
2786 !it->second.retrial_proc[r - 1].disabled;
2787 if (!has) {
2788 xml::Element& dn = sts.add_child("subParameter");
2789 dn.set_attr("classPath", "jmt.engine.random.Exponential");
2790 dn.set_attr("name", "Exponential");
2791 xml::Element& par = sts.add_child("subParameter");
2792 par.set_attr("classPath", "jmt.engine.random.ExponentialPar");
2793 par.set_attr("name", "distrPar");
2794 jmt_scalar(par, "java.lang.Double", "lambda", "1.000000000000");
2795 continue;
2796 }
2797 jmt_append_distribution(sts, jmt_dist_view(it->second.retrial_proc[r - 1]),
2798 "SolverJMT (retrial delay)");
2799 }
2800 }
2801
2802 /**
2803 * Port of the polling branch of `saveSwitchoverStrategy`.
2804 *
2805 * ONLY the polling branch is ported: `writeJSIM` reaches this handler from
2806 * the PollingServer section alone, and the reference's second branch --
2807 * a (K x K) per-class-pair switchover for an ordinary Server -- is
2808 * unreachable there. A switchover declared on a non-polling queue is
2809 * refused in `warn_switchover_on_non_polling`, which is where the reference
2810 * warns and drops it.
2811 */
2812 void save_switchover_strategy(xml::Element& section, std::size_t ind) {
2813 const std::size_t ist = sn_.nodes[ind - 1].station;
2814 const typename qn::NetworkStruct<T>::PollingParam pp = sn_.effective_polling(ist);
2815 xml::Element& p = jmt_param(section, "jmt.engine.NetStrategies.ServiceStrategy",
2816 "SwitchoverStrategy", true);
2817 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2818 jmt_ref_class(p, cname(r));
2819 xml::Element& sts = p.add_child("subParameter");
2820 sts.set_attr("classPath",
2821 "jmt.engine.NetStrategies.ServiceStrategies.ServiceTimeStrategy");
2822 sts.set_attr("name", "ServiceTimeStrategy");
2823 const lang::Distrib<T>& so =
2824 r <= pp.switchover.size() ? pp.switchover[r - 1] : empty_dist_;
2825 const JmtDistView<T> v = jmt_dist_view(so);
2826 if (so.disabled || v.type == lang::ProcessType::IMMEDIATE) {
2827 // No switchover declared for this buffer is a zero switchover,
2828 // not an omission: JMT reads the array positionally. An
2829 // Immediate switchover is the same zero, declared: the
2830 // reference maps both to ZeroServiceTimeStrategy, which carries
2831 // no distribution, and there is no JMT distribution to fall
2832 // back on -- an unhandled Immediate aborted the whole solve.
2833 sts.set_attr("classPath",
2834 "jmt.engine.NetStrategies.ServiceStrategies.ZeroServiceTimeStrategy");
2835 sts.set_attr("name", "ZeroServiceTimeStrategy");
2836 continue;
2837 }
2838 jmt_append_distribution(sts, v, "SolverJMT (switchover)");
2839 }
2840 }
2841
2842 std::map<std::size_t, double> empty_weights_;
2843 lang::Distrib<T> empty_dist_;
2844};
2845
2846/** Port of `@@JMTIO/writeJSIM.m`: serialize `sn` as a JMT `.jsimg` document. */
2847template <class T>
2849 JmtWriter<T> w(sn, opt);
2850 return w.write_jsim();
2851}
2852
2853/**
2854 * `JmtWriter::buffer_capacity_refusal` without a document: the writer's own
2855 * binding-buffer verdict, for the gate in `jmt::jmt_method_refusal`.
2856 */
2857template <class T>
2858std::string jmt_buffer_capacity_refusal(const qn::NetworkStruct<T>& sn, bool jmva_engine) {
2860 return w.buffer_capacity_refusal(jmva_engine);
2861}
2862
2863} // namespace io
2864} // namespace line
2865
2866#endif // LINE_IO_JMT_WRITER_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
Requested feature or arithmetic mode is not ported yet.
Definition error.h:49
UnsupportedError(const std::string &what)
Definition error.h:51
The writer itself.
Definition jmt_writer.h:433
std::string write_jsim()
Port of @@JMTIO/writeJSIM.m; returns the serialized document.
Definition jmt_writer.h:443
std::string buffer_capacity_refusal(bool jmva_engine) const
The buffer-capacity refusals of save_buffer_capacity, as a SENTENCE rather than an exception; empty w...
Definition jmt_writer.h:480
JmtWriter(const qn::NetworkStruct< T > &sn, const JmtWriteOptions &opt)
Definition jmt_writer.h:435
A network plus its refreshed NetworkStruct.
std::vector< JobClass > classes
std::vector< NodeDef > nodes
every node, in creation order
The exception types the port throws.
The distribution subtree of a JMT .jsimg file, and the scalar formatting every JMT writer shares.
void jmt_append_distribution(xml::Element &wrapper, const JmtDistView< T > &v, const char *who)
Append the distribution and distrPar pair for one process.
Definition jmt_dist.h:226
const char * jmt_drop_text(DropStrategy d)
MATLAB DropStrategy.toText, the strings JMT's dropRule field expects.
Definition jmt_writer.h:385
JmtMetricKind
The measure kinds saveMetrics requests, in the order it requests them.
Definition jmt_writer.h:282
bool jmt_metric_enabled(const qn::NetworkStruct< T > &sn, JmtMetricKind kind, std::size_t ist, std::size_t r, const std::vector< bool > &is_cache_class)
Port of the disabled rules in getAvgHandles, per (station, class).
Definition jmt_writer.h:314
std::string jmt_fmt(double x)
sprintf('%.12f', x), the numeric format of every JMT <value> body.
Definition jmt_dist.h:53
std::string jmt_write_jsim(const qn::NetworkStruct< T > &sn, const JmtWriteOptions &opt)
Port of @@JMTIO/writeJSIM.m: serialize sn as a JMT .jsimg document.
JmtSections< T > jmt_sections(const qn::NetworkStruct< T > &sn, std::size_t ind)
Definition jmt_writer.h:165
void jmt_param_value(xml::Element &section, const char *class_path, const char *name, const std::string &value)
<parameter classPath="CP" name="NAME"><value>V</value></parameter>
Definition jmt_writer.h:373
std::string jmt_buffer_capacity_refusal(const qn::NetworkStruct< T > &sn, bool jmva_engine)
JmtWriter::buffer_capacity_refusal without a document: the writer's own binding-buffer verdict,...
std::vector< bool > jmt_cache_classes(const qn::NetworkStruct< T > &sn)
The classes a Cache switches jobs into; they keep their Tput/ArvR measures.
Definition jmt_writer.h:346
xml::Element & jmt_scalar(xml::Element &parent, const char *class_path, const char *name, const std::string &value)
<subParameter classPath="java.lang.Double" name="NAME"><value>V</value>
Definition jmt_dist.h:81
bool jmt_reads_drop(DropStrategy d)
Whether JMT's queue section can read this drop strategy at all.
Definition jmt_writer.h:408
std::vector< std::vector< bool > > jmt_conn_matrix(const qn::NetworkStruct< T > &sn)
sn.connmatrix: which ordered pairs of nodes the model LINKED.
Definition jmt_writer.h:96
xml::Element & jmt_double(xml::Element &parent, const char *name, double v)
A java.lang.Double scalar subparameter.
Definition jmt_dist.h:91
std::vector< bool > jmt_exportable_classes(const qn::NetworkStruct< T > &sn)
Port of getExportableClasses.
Definition jmt_writer.h:120
const char * jmt_hetero_text(lang::HeteroSchedPolicy p)
MATLAB HeteroSchedPolicy.toJMTText: JMT's long descriptive identifiers.
Definition jmt_writer.h:414
const char * jmt_metric_text(JmtMetricKind k)
The JMT type attribute, MATLAB MetricType.toText.
Definition jmt_writer.h:285
xml::Element & jmt_param(xml::Element &section, const char *class_path, const char *name, bool array)
<parameter array="true" classPath="CP" name="NAME">
Definition jmt_writer.h:363
void jmt_ref_class(xml::Element &parent, const std::string &class_name)
<refClass>NAME</refClass>, the per-class marker of an array parameter.
Definition jmt_writer.h:380
JmtDistView< T > jmt_dist_view(const Distrib< T > &d)
Lower a Distrib to the view above.
Definition jmt_dist.h:122
std::string jmt_int(double x)
int2str(x): round to nearest, print as a decimal integer.
Definition jmt_dist.h:60
std::string jmt_num(double x)
num2str(x): MATLAB's default five-significant-digit form.
Definition jmt_dist.h:67
std::string jmt_sig2(double x)
num2str(x, 2): the two-significant-digit form used for alpha/precision.
Definition jmt_dist.h:74
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
DropStrategy
Blocking and loss rules, with the values of MATLAB DropStrategy.
Definition lang_types.h:424
@ IMMEDIATE
fires with zero delay, resolved by weight and priority
Definition lang_types.h:363
JoinStrategy
Join rules, with the values of MATLAB JoinStrategy.
Definition lang_types.h:461
RoutingStrategy
Routing strategies, with the values of MATLAB RoutingStrategy.
Definition lang_types.h:389
HeteroSchedPolicy
How a heterogeneous station picks among its server types, MATLAB HeteroSchedPolicy.
Definition lang_types.h:451
PollingType
Polling service disciplines, with the values of MATLAB PollingType.
Definition lang_types.h:370
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:324
@ LRU
least recently used
Definition lang_types.h:382
@ FIFO
first in, first out
Definition lang_types.h:380
std::unique_ptr< Element > element(const std::string &tag)
A fresh detached element, the createElement of the write side.
Definition xml.h:299
std::string serialize(const Element &root)
Serialize a document: the XML declaration MATLAB's xmlwrite emits, then the root subtree.
Definition xml.h:369
A queueing network and its refreshed NetworkStruct.
The three JMT section class names of a node: input, server, output.
Definition jmt_writer.h:160
The simulation controls the JSIM header carries, MATLAB's JMTIO properties.
Definition jmt_writer.h:68
std::string log_path
model.getLogPath, the logPath attribute
Definition jmt_writer.h:70
std::string file_name
base name; the header echoes it plus .jsimg
Definition jmt_writer.h:69
std::vector< std::size_t > missclass
std::vector< std::size_t > hitclass
A node of the network.
Element & add_child(const std::string &tag)
createElement + appendChild in one step; the child is owned here.
Definition xml.h:107
Element & set_attr(const std::string &key, const std::string &value)
setAttribute: replace the value in place when the key already exists, otherwise append.
Definition xml.h:96
Element & add_text_child(const std::string &tag, const std::string &value)
The common shape <tag>value</tag>.
Definition xml.h:122
A minimal XML DOM: read for the .lqnx interchange format, write for the JMT .jsimg and ....