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 // The (from, to) form is the one an ordinary Server can actually carry,
768 // and it is exactly the one JMT drops: `Server` has no switchover
769 // parameter, so the reference warns here too rather than writing a
770 // SwitchoverStrategy the loader would not read.
771 if (!sn_.stations[ist - 1].switchover_pair.empty())
772 std::cerr << "[LINE] Warning: JMT does not support switchover times for "
773 << "non-polling queues. Switchover times will be ignored for node '"
774 << nname(ind) << "'." << std::endl;
775 }
776
777 // -- Buffer section -----------------------------------------------------
778
779 /**
780 * Port of `saveBufferCapacity`.
781 *
782 * LINE's `cap` is Kendall's K, the WHOLE system capacity, and so is JMT's
783 * `size`, so the number crosses unchanged. -1 is JMT's "unbounded", and it
784 * is also what a capacity the population cannot REACH means: such a bound
785 * can never bind, and exporting it would make JMT reject arrivals that LINE
786 * admits at the instant the last job arrives.
787 *
788 * The test is `>=` and not `!=`: `refresh_capacity` DERIVES `sn.cap` for a
789 * station the user never capped, as the sum over the classes served there
790 * of the chain population, so a multi-class station gets (#classes) x N --
791 * 8 on a two-class model of 4 jobs. Under `!=` only the single-class case
792 * matched, and every multi-class one fell through to
793 * `assert_station_cap_exportable` and was refused as a "binding" buffer
794 * nobody declared. An open class carries an infinite population and so
795 * makes `total` infinite, which is what keeps a DECLARED cap in a mixed
796 * model refused: the open stream can fill the buffer under a closed job.
797 */
798 void save_buffer_capacity(xml::Element& section, std::size_t ind) {
799 const std::size_t ist = sn_.nodes[ind - 1].station;
800 std::string v = "-1";
801 if (ist != 0 && std::isfinite(sn_.cap[ist - 1])) {
802 const double total = reachable_population(ist);
803 const double ns = sn_.stations[ist - 1].nservers;
804 if (sn_.cap[ist - 1] < total && std::isfinite(ns)) {
805 assert_station_cap_exportable(ist);
806 v = jmt_int(sn_.cap[ist - 1]);
807 }
808 }
809 jmt_param_value(section, "java.lang.Integer", "size", v);
810 }
811
812 /**
813 * The most jobs that can be present at station `ist` (1-based).
814 *
815 * Read the way `refresh_capacity` derives the capacity itself: per CHAIN,
816 * because a chain's whole population can reach a station that serves any one
817 * of its classes (class switching moves jobs between them), and a chain none
818 * of whose classes is served there cannot put a single job on it.
819 *
820 * Deliberately NOT read off the clamped per-class capacities: comparing a
821 * capacity against a quantity derived from it would make every
822 * user-declared buffer look non-binding. Infinite when an open chain is
823 * served here, which is what the model total gave before and which sends
824 * the station to `assert_station_cap_exportable`, where the open classes
825 * are skipped by name.
826 *
827 * A class that never visits this station cannot fill it, so summing every
828 * class's population made a capacity that is exactly the reachable
829 * population look like a buffer -- which is what a SELF-LOOPING CLASS does.
830 * See the MATLAB twin in JMTIO/saveBufferCapacity.m.
831 */
832 double reachable_population(std::size_t ist) const {
833 double n = 0.0;
834 for (std::size_t c = 0; c < sn_.nchains; ++c) {
835 bool served = false;
836 double chain_jobs = 0.0;
837 for (std::size_t r : sn_.inchain[c]) {
838 if (!sn_.disabled[ist - 1][r - 1]) served = true;
839 chain_jobs += sn_.classes[r - 1].population;
840 }
841 if (served) n += chain_jobs;
842 }
843 return n;
844 }
845
846 /**
847 * True when station `ist` is the RECEIVING side of a true-BAS relation for
848 * class `r`, i.e. an arrival of `r` that finds `ist` full must block an
849 * upstream station rather than be lost.
850 *
851 * LINE accepts the BAS declaration in two places -- on the blocking
852 * (upstream) station, as `cqn_bas_blocking` does, or on the full
853 * destination, as a model read back from JMT does -- and
854 * `NetworkStruct::refresh_local_vars` resolves both into
855 * `sn.isbasdestination` (BUG-83). Reading `sn.droprule` at the capped
856 * station sees only the second form, which is what made SolverJMT refuse
857 * the first one.
858 */
859 bool is_bas_destination(std::size_t ist, std::size_t r) const {
860 if (ist == 0 || sn_.isbasdestination.size() < ist) return false;
861 if (sn_.isbasdestination[ist - 1].size() < r) return false;
862 return sn_.isbasdestination[ist - 1][r - 1];
863 }
864
865 /**
866 * The JMT dropStrategy/dropRule string for station `ist`, class `r`.
867 *
868 * Beyond `jmt_drop_text` this resolves the two ways LINE can declare BAS
869 * blocking onto the one way JMT can read it. JMT's queue section says what
870 * happens to an arrival that finds THIS buffer full, so it only understands
871 * the rule on the destination; a WAITQ slot that `is_bas_destination` marks
872 * is therefore written out as 'BAS blocking'. A node with no buffer of its
873 * own gets JMT's 'drop', as the reference does for a NaN station index.
874 *
875 * It also keeps the written file VALID: a strategy `jmt_reads_drop` rejects
876 * is spelled 'waiting queue', JMT's own no-limit default. That substitution
877 * is only ever reached where the rule cannot be consulted (infinite size, or
878 * a closed capacity equal to the population): a buffer that can actually
879 * fill under one of those is refused outright by
880 * `assert_station_cap_exportable`.
881 */
882 const char* drop_strategy_text(std::size_t ist, std::size_t r) const {
883 if (ist == 0) return "drop";
884 const DropStrategy d = sn_.droprule[ist - 1][r - 1];
885 if (d == DropStrategy::WAITQ && is_bas_destination(ist, r))
886 return jmt_drop_text(DropStrategy::BAS);
887 if (!jmt_reads_drop(d)) return jmt_drop_text(DropStrategy::WAITQ);
888 return jmt_drop_text(d);
889 }
890
891 /**
892 * Refuses a binding station capacity JMT cannot express, on two counts.
893 *
894 * (1) THE RULE IS ONE JMT CANNOT READ -- BBS, RSRD or retrial-with-limit;
895 * see `jmt_reads_drop`. Such a value is not approximated, it is IGNORED, so
896 * the capacity stops being enforced and JMT returns the unconstrained
897 * answer.
898 *
899 * (2) THE RULE IS WAITQ AND A CLOSED CLASS CAN REACH THE LIMIT, the same
900 * reason `assert_class_cap_exportable` below refuses the per-class one: JMT
901 * cannot hold a blocked closed job at its upstream station. Note this is the
902 * case where NO blocking rule is declared. A model that does declare BAS is
903 * exported as JMT "BAS blocking", which is the same queueing model, under
904 * either declaration form -- see `is_bas_destination`.
905 *
906 * That refusal advised expressing the limit as the STATION capacity instead,
907 * and measured on 2026-08-19 the advice was wrong, and neither of the two
908 * strategies a WAITQ station maps onto reproduces the UNDECLARED case:
909 *
910 * waiting queue does not enforce `size` at all. On a closed 3-queue
911 * tandem, N=6, Exp(1) FCFS, cap 2 at Q2, JMT returned the
912 * UNCONSTRAINED [2.03 1.99 1.98], X = 0.750, against the
913 * exact [3.6090 0.9711 1.4199], X = 0.6522.
914 * BAS blocking enforces it, but completes the service BEFORE blocking,
915 * so the blocked job moves the instant room frees -- a
916 * different queueing model, not a rounding: same fixture,
917 * [2.871 1.373 1.756], X = 0.7126.
918 *
919 * With no rule declared LINE instead disables the upstream departure while
920 * the destination is full, which for exponential service is repetitive
921 * service (RS) and is what SolverCTMC, SolverSSA and SolverLDES all agree
922 * on. So THAT model is refused rather than exported as either of the two
923 * things JMT can say. See BUG-81. A declared-BAS model is a different model
924 * and is exported, not refused: blocking after service is precisely what
925 * JMT's "BAS blocking" does.
926 *
927 * That the limit CAN be reached is the caller's to establish and is not
928 * retested here: `save_buffer_capacity` reaches this method only for a
929 * capacity strictly below the total population, which is the one thing that
930 * makes a buffer a buffer.
931 */
932 void assert_station_cap_exportable(std::size_t ist) const {
933 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
934 if (sn_.disabled[ist - 1][r - 1]) continue; // not served here
935 const DropStrategy dr = sn_.droprule[ist - 1][r - 1];
936 // Unmappable for EITHER class type, so this test precedes the open-class skip
937 if (!jmt_reads_drop(dr))
938 throw UnsupportedError(
939 "SolverJMT: station '" + nname(sn_.station_to_node[ist - 1]) +
940 "' applies drop strategy '" + jmt_drop_text(dr) + "' to class '" + cname(r) +
941 "' and carries a finite capacity " + jmt_int(sn_.cap[ist - 1]) +
942 " it can reach. JMT's queue section reads only 'drop', 'waiting queue', "
943 "'BAS blocking' and 'retrial'; it does not approximate anything else, it "
944 "ignores it, so the capacity would stop being enforced and the run would "
945 "return the unconstrained answer. Use SolverCTMC, SolverSSA or SolverLDES");
946 if (!std::isfinite(sn_.classes[r - 1].population)) continue; // open: JMT loses it too
947 if (dr != DropStrategy::WAITQ)
948 continue; // a mappable declared blocking rule is exported as itself
949 if (is_bas_destination(ist, r))
950 continue; // BAS declared on the UPSTREAM station: drop_strategy_text
951 // moves it onto this one, which is where JMT reads it
952 throw UnsupportedError(
953 "SolverJMT: station '" + nname(sn_.station_to_node[ist - 1]) +
954 "' carries a finite capacity " + jmt_int(sn_.cap[ist - 1]) +
955 " that binds for the closed class '" + cname(r) +
956 "'. LINE blocks a closed job that finds no room -- the upstream departure is "
957 "disabled and the job stays where it is -- and no JMT drop strategy reproduces "
958 "that: 'waiting queue' does not enforce the size at all, and 'BAS blocking' "
959 "completes the service before blocking, which is a different queueing model. "
960 "Use SolverCTMC, SolverSSA or SolverLDES, or declare DropStrategy.BAS if "
961 "blocking after service is the model you want, which SolverJMT does export");
962 }
963 }
964
965 /** Port of `saveDropStrategy`: the per-class rule of a full buffer. */
966 void save_drop_strategy(xml::Element& section, std::size_t ind) {
967 const std::size_t ist = sn_.nodes[ind - 1].station;
968 xml::Element& p = jmt_param(section, "java.lang.String", "dropStrategies", true);
969 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
970 if (!keep_[r - 1]) continue;
971 jmt_ref_class(p, cname(r));
972 const char* txt = drop_strategy_text(ist, r);
973 xml::Element& sp = p.add_child("subParameter");
974 sp.set_attr("classPath", "java.lang.String");
975 sp.set_attr("name", "dropStrategy");
976 sp.add_text_child("value", txt);
977 }
978 }
979
980 /**
981 * Port of `saveGetStrategy`: FCFS everywhere except a polling queue, whose
982 * discipline decides which of JMT's three polling get strategies serves it.
983 */
984 void save_get_strategy(xml::Element& section, std::size_t ind) {
985 const std::size_t ist = sn_.nodes[ind - 1].station;
986 const bool polling = sn_.nodes[ind - 1].nodetype == NodeType::Queue && ist != 0 &&
987 sn_.stations[ist - 1].sched == SchedStrategy::POLLING;
988 if (!polling) {
989 xml::Element& p = section.add_child("parameter");
990 p.set_attr("classPath", "jmt.engine.NetStrategies.QueueGetStrategies.FCFSstrategy");
991 p.set_attr("name", "FCFSstrategy");
992 return;
993 }
994 const typename qn::NetworkStruct<T>::PollingParam pp = sn_.effective_polling(ist);
995 xml::Element& p = section.add_child("parameter");
996 switch (pp.ptype) {
997 case PollingType::GATED:
998 p.set_attr("classPath",
999 "jmt.engine.NetStrategies.QueueGetStrategies.GatedPollingGetStrategy");
1000 break;
1001 case PollingType::EXHAUSTIVE:
1002 p.set_attr(
1003 "classPath",
1004 "jmt.engine.NetStrategies.QueueGetStrategies.ExhaustivePollingGetStrategy");
1005 break;
1006 case PollingType::KLIMITED: {
1007 p.set_attr("classPath",
1008 "jmt.engine.NetStrategies.QueueGetStrategies.LimitedPollingGetStrategy");
1009 xml::Element& k = p.add_child("subParameter");
1010 k.set_attr("classPath", "java.lang.Integer");
1011 k.set_attr("name", "pollingKValue");
1012 k.add_text_child("value", jmt_int(static_cast<double>(pp.pk)));
1013 break;
1014 }
1015 case PollingType::DECREMENTING:
1016 throw UnsupportedError(
1017 "SolverJMT: JMT does not support the decrementing (semiexhaustive) polling "
1018 "discipline; use the LDES solver");
1019 }
1020 p.set_attr("name", "FCFSstrategy");
1021 }
1022
1023 /**
1024 * Port of `savePutStrategy`: the discipline expressed as WHERE an arrival
1025 * is inserted in the buffer.
1026 *
1027 * JMT has no scheduling-strategy field: FCFS is a tail insertion, LCFS a
1028 * head insertion, SJF/SRPT an ordered one, and a preemptive discipline its
1029 * own put strategy. Everything unlisted -- PS above all, whose sharing is
1030 * expressed by the PSServer section instead -- is a tail insertion.
1031 */
1032 void save_put_strategy(xml::Element& section, std::size_t ind) {
1033 const std::size_t ist = sn_.nodes[ind - 1].station;
1034 xml::Element& p =
1035 jmt_param(section, "jmt.engine.NetStrategies.QueuePutStrategy", "QueuePutStrategy", true);
1036 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
1037 if (!keep_[r - 1]) continue;
1038 jmt_ref_class(p, cname(r));
1039 const char* nm = "TailStrategy";
1040 if (ist != 0) {
1041 switch (sn_.stations[ist - 1].sched) {
1042 case SchedStrategy::SIRO: nm = "RandStrategy"; break;
1043 case SchedStrategy::LJF: nm = "LJFStrategy"; break;
1044 case SchedStrategy::SJF: nm = "SJFStrategy"; break;
1045 case SchedStrategy::LEPT: nm = "LEPTStrategy"; break;
1046 case SchedStrategy::SEPT: nm = "SEPTStrategy"; break;
1047 case SchedStrategy::LCFS: nm = "HeadStrategy"; break;
1048 case SchedStrategy::LCFSPRIO: nm = "HeadStrategyPriority"; break;
1049 case SchedStrategy::LCFSPR: nm = "LCFSPRStrategy"; break;
1050 case SchedStrategy::LCFSPI: nm = "LCFSPIStrategy"; break;
1051 case SchedStrategy::LCFSPRPRIO: nm = "LCFSPRStrategyPriority"; break;
1052 case SchedStrategy::LCFSPIPRIO: nm = "LCFSPIStrategyPriority"; break;
1053 case SchedStrategy::FCFSPR: nm = "FCFSPRStrategy"; break;
1054 case SchedStrategy::FCFSPI: nm = "FCFSPIStrategy"; break;
1055 case SchedStrategy::FCFSPRPRIO: nm = "FCFSPRStrategyPriority"; break;
1056 case SchedStrategy::FCFSPIPRIO: nm = "FCFSPIStrategyPriority"; break;
1057 case SchedStrategy::HOL: nm = "TailStrategyPriority"; break;
1058 case SchedStrategy::EDD: nm = "EDDStrategy"; break;
1059 case SchedStrategy::EDF: nm = "EDFStrategy"; break;
1060 case SchedStrategy::SRPT: nm = "SRPTStrategy"; break;
1061 case SchedStrategy::SRPTPRIO: nm = "SRPTStrategyPriority"; break;
1062 default: nm = "TailStrategy"; break;
1063 }
1064 }
1065 xml::Element& sp = p.add_child("subParameter");
1066 sp.set_attr("classPath",
1067 std::string("jmt.engine.NetStrategies.QueuePutStrategies.") + nm);
1068 sp.set_attr("name", nm);
1069 }
1070 }
1071
1072 // -- Server section -----------------------------------------------------
1073
1074 /**
1075 * Port of `saveNumberOfServers`.
1076 *
1077 * LPS EXPORTS AS ONE SERVER. Its admission limit is not a server count but
1078 * a cap on the number in service, which `save_regions` expresses as an
1079 * implicit finite capacity region; exporting the limit as the server count
1080 * would give a c-server FCFS queue instead of limited processor sharing.
1081 * A load-dependent station exports `max(nservers, max lldscaling)`, since
1082 * `min(1:N, c)` scaling IS a c-server queue.
1083 */
1084 void save_number_of_servers(xml::Element& section, std::size_t ind) {
1085 const std::size_t ist = sn_.nodes[ind - 1].station;
1086 double maxjobs = 1.0;
1087 if (ist != 0) {
1088 if (sn_.stations[ist - 1].sched == SchedStrategy::LPS) {
1089 maxjobs = 1.0;
1090 } else {
1091 maxjobs = sn_.stations[ist - 1].nservers;
1092 for (const T& s : sn_.stations[ist - 1].lldscaling)
1093 maxjobs = std::max(maxjobs, d(s));
1094 }
1095 }
1096 jmt_param_value(section, "java.lang.Integer", "maxJobs", jmt_int(maxjobs));
1097 }
1098
1099 /** Port of `saveServerVisits`: one visit per class, always. */
1100 void save_server_visits(xml::Element& section) {
1101 xml::Element& p = jmt_param(section, "java.lang.Integer", "numberOfVisits", true);
1102 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
1103 if (!keep_[r - 1]) continue;
1104 jmt_ref_class(p, cname(r));
1105 xml::Element& sp = p.add_child("subParameter");
1106 sp.set_attr("classPath", "java.lang.Integer");
1107 sp.set_attr("name", "numberOfVisits");
1108 sp.add_text_child("value", "1");
1109 }
1110 }
1111
1112 /**
1113 * Server pools and job parallelism of a node, as the JMT Server section needs them.
1114 *
1115 * JMT's Server section takes classParallelism, serverNames, serversPerServerType,
1116 * serverCompatibilities and schedulingPolicy as one positional block of its
1117 * constructor (`jmt.engine.NodeSections.Server`), so the five are emitted
1118 * together or not at all, and always after the service strategies. A station
1119 * declaring parallelism alone is therefore given one synthetic pool holding all
1120 * of its servers, since the pool counts, not numberOfServers, size the server
1121 * pool once any pool is declared.
1122 */
1123 struct ServerPools {
1124 bool present = false;
1125 std::vector<std::string> names;
1126 std::vector<double> counts;
1127 std::vector<std::vector<bool>> compat; ///< [type][class]
1129 std::vector<std::size_t> parallelism; ///< per class, one-based class order
1130 };
1131
1132 ServerPools server_pools(std::size_t ind) const {
1133 ServerPools pools;
1134 const std::size_t ist = sn_.nodes[ind - 1].station;
1135 if (ist == 0) return pools;
1136 const auto& st = sn_.stations[ist - 1];
1137 const bool has_types = !st.server_types.empty();
1138 bool has_parallelism = false;
1139 for (std::size_t n : st.server_parallelism) {
1140 if (n > 1) { has_parallelism = true; break; }
1141 }
1142 if (!has_types && !has_parallelism) return pools;
1143
1144 pools.present = true;
1145 pools.parallelism.assign(sn_.nclasses, 1);
1146 for (std::size_t r = 0; r < sn_.nclasses && r < st.server_parallelism.size(); ++r) {
1147 pools.parallelism[r] = st.server_parallelism[r] < 1 ? 1 : st.server_parallelism[r];
1148 }
1149 if (has_types) {
1150 pools.policy = st.hetero_policy;
1151 for (const auto& pool : st.server_types) {
1152 pools.names.push_back(pool.name);
1153 pools.counts.push_back(pool.count);
1154 std::vector<bool> row(sn_.nclasses, true);
1155 for (std::size_t r = 0; r < sn_.nclasses; ++r) {
1156 // An EMPTY compatibility row means "every class", the constructor
1157 // default; JMT has no such shorthand, so it is expanded here.
1158 row[r] = pool.compatible.empty()
1159 ? true
1160 : (r < pool.compatible.size() ? pool.compatible[r] : false);
1161 }
1162 pools.compat.push_back(row);
1163 }
1164 } else {
1165 pools.names.push_back(sn_.nodes[ind - 1].name + " - Server Type 1");
1166 pools.counts.push_back(st.nservers);
1167 pools.compat.push_back(std::vector<bool>(sn_.nclasses, true));
1168 }
1169 return pools;
1170 }
1171
1172 /** Port of `saveClassParallelism`: `Server.serverNumRequired`, per class. */
1173 void save_class_parallelism(xml::Element& section, std::size_t ind) {
1174 const ServerPools pools = server_pools(ind);
1175 if (!pools.present) return;
1176 xml::Element& p = jmt_param(section, "java.lang.Integer", "classParallelism", true);
1177 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
1178 if (!keep_[r - 1]) continue;
1179 jmt_ref_class(p, cname(r));
1180 xml::Element& sp = p.add_child("subParameter");
1181 sp.set_attr("classPath", "java.lang.Integer");
1182 sp.set_attr("name", "serverParallelism");
1183 sp.add_text_child("value", jmt_int(static_cast<double>(pools.parallelism[r - 1])));
1184 }
1185 }
1186
1187 /** Port of `saveServerTypeNames`. */
1188 void save_server_type_names(xml::Element& section, std::size_t ind) {
1189 const ServerPools pools = server_pools(ind);
1190 if (!pools.present) return;
1191 xml::Element& p = jmt_param(section, "java.lang.String", "serverNames", true);
1192 for (const std::string& name : pools.names) {
1193 xml::Element& sp = p.add_child("subParameter");
1194 sp.set_attr("classPath", "java.lang.String");
1195 sp.set_attr("name", "serverTypesNames");
1196 sp.add_text_child("value", name);
1197 }
1198 }
1199
1200 /** Port of `saveServersPerType`. */
1201 void save_servers_per_type(xml::Element& section, std::size_t ind) {
1202 const ServerPools pools = server_pools(ind);
1203 if (!pools.present) return;
1204 xml::Element& p = jmt_param(section, "java.lang.Integer", "serversPerServerType", true);
1205 for (double count : pools.counts) {
1206 xml::Element& sp = p.add_child("subParameter");
1207 sp.set_attr("classPath", "java.lang.Integer");
1208 sp.set_attr("name", "serverTypesNumOfServers");
1209 sp.add_text_child("value", jmt_int(count));
1210 }
1211 }
1212
1213 /** Port of `saveServerCompatibilities`. */
1214 void save_server_compatibilities(xml::Element& section, std::size_t ind) {
1215 const ServerPools pools = server_pools(ind);
1216 if (!pools.present) return;
1217 xml::Element& p = jmt_param(section, "java.lang.Object", "serverCompatibilities", true);
1218 for (const std::vector<bool>& row : pools.compat) {
1219 xml::Element& tn = p.add_child("subParameter");
1220 tn.set_attr("array", "true");
1221 tn.set_attr("classPath", "java.lang.Boolean");
1222 tn.set_attr("name", "serverTypesCompatibilities");
1223 for (std::size_t r = 0; r < sn_.nclasses; ++r) {
1224 xml::Element& cn = tn.add_child("subParameter");
1225 cn.set_attr("classPath", "java.lang.Boolean");
1226 cn.set_attr("name", "compatibilities");
1227 cn.add_text_child("value", row[r] ? "true" : "false");
1228 }
1229 }
1230 }
1231
1232 /** Port of `saveHeteroSchedPolicy`, via `HeteroSchedPolicy.toJMTText`. */
1233 void save_hetero_sched_policy(xml::Element& section, std::size_t ind) {
1234 const ServerPools pools = server_pools(ind);
1235 if (!pools.present) return;
1236 jmt_param_value(section, "java.lang.String", "schedulingPolicy",
1237 jmt_hetero_text(pools.policy));
1238 }
1239
1240 /**
1241 * Warns that per-server-type service rates cannot reach the JMT engine.
1242 *
1243 * JMT keys the ServiceStrategy array of a station by refClass, so its loader
1244 * (`jmt.engine.simEngine.SimLoader`) keeps one strategy per class however many
1245 * (type, class) entries are written, and every pool of a station ends up
1246 * serving at the class rate. Pool sizes, class compatibilities and the
1247 * assignment policy do cross; per-pool service laws do not.
1248 */
1249 void warn_hetero_rates(std::size_t ind) const {
1250 const std::size_t ist = sn_.nodes[ind - 1].station;
1251 if (ist == 0) return;
1252 const auto& pools = sn_.stations[ist - 1].server_types;
1253 if (pools.size() < 2) return;
1254 bool first_set = false, distinct = false;
1255 double first = 0.0;
1256 for (const auto& pool : pools) {
1257 for (const lang::Distrib<T>& sd : pool.service) {
1258 if (sd.disabled) continue;
1259 const double mean = num_traits<T>::to_double(sd.mean);
1260 if (!(mean > 0)) continue;
1261 if (!first_set) { first = mean; first_set = true; }
1262 else if (std::abs(mean - first) > 1e-12) { distinct = true; }
1263 }
1264 }
1265 if (!distinct) return;
1266 std::cerr << "[LINE] Warning: JMT keys service strategies by job class, so the "
1267 << "per-server-type service rates of station '" << sn_.nodes[ind - 1].name
1268 << "' cannot be exported; every pool will serve at the class service rate. "
1269 << "Use the LDES or CTMC solver for per-type rates." << std::endl;
1270 }
1271
1272 /**
1273 * Port of `saveServiceStrategy`.
1274 *
1275 * A pair the class never visits becomes a DisabledServiceTimeStrategy and
1276 * an Immediate becomes a ZeroServiceTimeStrategy; neither carries a
1277 * distribution. Everything else goes through the shared emitter.
1278 */
1279 void save_service_strategy(xml::Element& section, std::size_t ind) {
1280 const std::size_t ist = sn_.nodes[ind - 1].station;
1281 xml::Element& p = jmt_param(section, "jmt.engine.NetStrategies.ServiceStrategy",
1282 "ServiceStrategy", true);
1283 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
1284 if (!keep_[r - 1]) continue;
1285 jmt_ref_class(p, cname(r));
1286 xml::Element& sts = p.add_child("subParameter");
1287 const JmtDistView<T> v =
1288 ist == 0 ? JmtDistView<T>() : jmt_dist_view(sn_.service[ist - 1][r - 1]);
1289 if (v.type == lang::ProcessType::DISABLED) {
1290 sts.set_attr("classPath",
1291 "jmt.engine.NetStrategies.ServiceStrategies."
1292 "DisabledServiceTimeStrategy");
1293 sts.set_attr("name", "DisabledServiceTimeStrategy");
1294 continue;
1295 }
1296 if (v.type == lang::ProcessType::IMMEDIATE) {
1297 sts.set_attr("classPath",
1298 "jmt.engine.NetStrategies.ServiceStrategies.ZeroServiceTimeStrategy");
1299 sts.set_attr("name", "ZeroServiceTimeStrategy");
1300 continue;
1301 }
1302 sts.set_attr("classPath",
1303 "jmt.engine.NetStrategies.ServiceStrategies.ServiceTimeStrategy");
1304 sts.set_attr("name", "ServiceTimeStrategy");
1305 jmt_append_distribution(sts, v, "SolverJMT (service)");
1306 }
1307 }
1308
1309 /**
1310 * Port of `saveArrivalStrategy`.
1311 *
1312 * A CLOSED class has no arrival process at the Source and is exported as a
1313 * ServiceTimeStrategy with a literal `null` body, which is how JMT spells
1314 * "this class does not arrive here". So is an open class the Source
1315 * disables -- one that enters by class switching only.
1316 */
1317 void save_arrival_strategy(xml::Element& section, std::size_t ind) {
1318 const std::size_t ist = sn_.nodes[ind - 1].station;
1319 xml::Element& p = jmt_param(section, "jmt.engine.NetStrategies.ServiceStrategy",
1320 "ServiceStrategy", true);
1321 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
1322 if (!keep_[r - 1]) continue;
1323 jmt_ref_class(p, cname(r));
1324 xml::Element& sts = p.add_child("subParameter");
1325 sts.set_attr("classPath",
1326 "jmt.engine.NetStrategies.ServiceStrategies.ServiceTimeStrategy");
1327 sts.set_attr("name", "ServiceTimeStrategy");
1328 const bool closed = std::isfinite(sn_.classes[r - 1].population);
1329 const JmtDistView<T> v =
1330 ist == 0 ? JmtDistView<T>() : jmt_dist_view(sn_.service[ist - 1][r - 1]);
1331 if (closed || v.type == lang::ProcessType::DISABLED) {
1332 sts.add_text_child("value", "null");
1333 continue;
1334 }
1335 if (v.type == lang::ProcessType::IMMEDIATE) {
1336 sts.set_attr("classPath",
1337 "jmt.engine.NetStrategies.ServiceStrategies.ZeroServiceTimeStrategy");
1338 sts.set_attr("name", "ZeroServiceTimeStrategy");
1339 continue;
1340 }
1341 jmt_append_distribution(sts, v, "SolverJMT (arrival)");
1342 }
1343 }
1344
1345 /** Port of `savePreemptiveStrategy`: which PS variant a PSServer runs. */
1346 void save_preemptive_strategy(xml::Element& section, std::size_t ind) {
1347 const std::size_t ist = sn_.nodes[ind - 1].station;
1348 xml::Element& p = jmt_param(section, "jmt.engine.NetStrategies.PSStrategy", "PSStrategy",
1349 true);
1350 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
1351 if (!keep_[r - 1]) continue;
1352 jmt_ref_class(p, cname(r));
1353 const char* nm = nullptr;
1354 if (ist != 0) switch (sn_.stations[ist - 1].sched) {
1355 // LPS shares the server evenly among those admitted, so its
1356 // in-service discipline IS EPS; the limit is the region.
1357 case SchedStrategy::PS:
1358 case SchedStrategy::LPS: nm = "EPSStrategy"; break;
1359 case SchedStrategy::DPS: nm = "DPSStrategy"; break;
1360 case SchedStrategy::GPS: nm = "GPSStrategy"; break;
1361 case SchedStrategy::PSPRIO: nm = "EPSStrategyPriority"; break;
1362 case SchedStrategy::DPSPRIO: nm = "DPSStrategyPriority"; break;
1363 case SchedStrategy::GPSPRIO: nm = "GPSStrategyPriority"; break;
1364 default: nm = nullptr; break;
1365 }
1366 xml::Element& sp = p.add_child("subParameter");
1367 if (nm != nullptr) {
1368 sp.set_attr("classPath", std::string("jmt.engine.NetStrategies.PSStrategies.") + nm);
1369 sp.set_attr("name", nm);
1370 }
1371 }
1372 }
1373
1374 /** Port of `savePreemptiveWeights`: the DPS/GPS share of each class. */
1375 void save_preemptive_weights(xml::Element& section, std::size_t ind) {
1376 const std::size_t ist = sn_.nodes[ind - 1].station;
1377 xml::Element& p = jmt_param(section, "java.lang.Double", "serviceWeights", true);
1378 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
1379 if (!keep_[r - 1]) continue;
1380 jmt_ref_class(p, cname(r));
1381 double w = 0.0;
1382 if (ist != 0 && sn_.stations[ist - 1].schedparam.size() >= r)
1383 w = d(sn_.stations[ist - 1].schedparam[r - 1]);
1384 xml::Element& sp = p.add_child("subParameter");
1385 sp.set_attr("classPath", "java.lang.Double");
1386 sp.set_attr("name", "serviceWeight");
1387 sp.add_text_child("value", jmt_num(w));
1388 }
1389 }
1390
1391 /**
1392 * Port of `saveDelayOffStrategy`: the setup and delay-off times of a server
1393 * that powers down when idle.
1394 *
1395 * JMT casts each per-class entry to `ServiceStrategy[]` and reads element
1396 * [0], so the strategy sits inside a single-element array rather than
1397 * directly under the class. A class with no time declared gets a
1398 * deterministic zero, not an omission: an absent entry would leave JMT's
1399 * array short and shift every later class's setup time onto the wrong one.
1400 */
1401 void save_delay_off_strategy(xml::Element& section, std::size_t ind) {
1402 const std::size_t ist = sn_.nodes[ind - 1].station;
1403 if (ist == 0) return;
1404 const auto it = sn_.setupparam.find(ist);
1405 if (it == sn_.setupparam.end()) return;
1406 const qn::SetupDelayOffParam<T>& sp = it->second;
1407 bool any = false;
1408 for (const lang::Distrib<T>& s : sp.setup)
1409 if (!s.disabled) any = true;
1410 if (!any) return;
1411
1412 const char* names[2] = {"delayOffTime", "setUpTime"};
1413 for (int which = 0; which < 2; ++which) {
1414 const std::vector<lang::Distrib<T>>& tab = which == 0 ? sp.delayoff : sp.setup;
1415 xml::Element& p = jmt_param(section, "java.lang.Object", names[which], true);
1416 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
1417 jmt_ref_class(p, cname(r));
1418 xml::Element& row = p.add_child("subParameter");
1419 row.set_attr("array", "true");
1420 row.set_attr("classPath", "jmt.engine.NetStrategies.ServiceStrategy");
1421 row.set_attr("name", names[which]);
1422 xml::Element& sts = row.add_child("subParameter");
1423 sts.set_attr("classPath",
1424 "jmt.engine.NetStrategies.ServiceStrategies.ServiceTimeStrategy");
1425 sts.set_attr("name", "ServiceTimeStrategy");
1426 if (r <= tab.size() && !tab[r - 1].disabled)
1427 append_simple_distribution(sts, tab[r - 1]);
1428 else
1429 append_zero_time(sts);
1430 }
1431 }
1432 }
1433
1434 /** `appendZeroTimeXml`: a deterministic zero. */
1435 void append_zero_time(xml::Element& parent) {
1436 xml::Element& dn = parent.add_child("subParameter");
1437 dn.set_attr("classPath", "jmt.engine.random.DeterministicDistr");
1438 dn.set_attr("name", "Deterministic");
1439 xml::Element& par = parent.add_child("subParameter");
1440 par.set_attr("classPath", "jmt.engine.random.DeterministicDistrPar");
1441 par.set_attr("name", "distrPar");
1442 jmt_scalar(par, "java.lang.Double", "t", "0.0");
1443 }
1444
1445 /**
1446 * `appendDistributionXml`: the REDUCED emitter the setup/delay-off pair
1447 * uses -- Exp, Erlang, Det, Immediate, and everything else as a
1448 * deterministic time equal to its mean.
1449 *
1450 * The fallback is the reference's and is kept: a setup time is a small
1451 * fixed overhead in every model that has one, so replacing an exotic law by
1452 * its mean there is a documented simplification rather than a silent one,
1453 * and refusing would reject models JMT can otherwise run.
1454 */
1455 void append_simple_distribution(xml::Element& parent, const lang::Distrib<T>& dist) {
1456 if (dist.type == lang::ProcessType::IMMEDIATE) {
1457 append_zero_time(parent);
1458 return;
1459 }
1460 const double mean = d(dist.mean);
1461 if (dist.type == lang::ProcessType::EXP) {
1462 xml::Element& dn = parent.add_child("subParameter");
1463 dn.set_attr("classPath", "jmt.engine.random.Exponential");
1464 dn.set_attr("name", "Exponential");
1465 xml::Element& par = parent.add_child("subParameter");
1466 par.set_attr("classPath", "jmt.engine.random.ExponentialPar");
1467 par.set_attr("name", "distrPar");
1468 jmt_double(par, "lambda", mean > 0.0 ? 1.0 / mean : 0.0);
1469 return;
1470 }
1471 if (dist.type == lang::ProcessType::ERLANG) {
1472 const std::size_t ph = dist.phases();
1473 xml::Element& dn = parent.add_child("subParameter");
1474 dn.set_attr("classPath", "jmt.engine.random.Erlang");
1475 dn.set_attr("name", "Erlang");
1476 xml::Element& par = parent.add_child("subParameter");
1477 par.set_attr("classPath", "jmt.engine.random.ErlangPar");
1478 par.set_attr("name", "distrPar");
1479 jmt_double(par, "alpha", mean > 0.0 ? static_cast<double>(ph) / mean : 0.0);
1480 jmt_scalar(par, "java.lang.Long", "r", jmt_int(static_cast<double>(ph)));
1481 return;
1482 }
1483 xml::Element& dn = parent.add_child("subParameter");
1484 dn.set_attr("classPath", "jmt.engine.random.DeterministicDistr");
1485 dn.set_attr("name", "Deterministic");
1486 xml::Element& par = parent.add_child("subParameter");
1487 par.set_attr("classPath", "jmt.engine.random.DeterministicDistrPar");
1488 par.set_attr("name", "distrPar");
1489 jmt_double(par, "t", mean);
1490 }
1491
1492 // -- Dispatcher / ClassSwitch / Fork / Join ------------------------------
1493
1494 /**
1495 * Port of `saveRoutingStrategy`.
1496 *
1497 * A CLASS SWITCH NODE IS EXPORTED AS RANDOM ROUTING. It has exactly one
1498 * outgoing link, so uniform routing over it is the same routing, and it
1499 * avoids reading `rt` at a node whose class-switch mass the refresh has
1500 * already folded into the edges around it.
1501 */
1502 void save_routing_strategy(xml::Element& section, std::size_t ind) {
1503 const std::size_t K = sn_.nclasses;
1504 const bool is_cs = sn_.nodes[ind - 1].nodetype == NodeType::ClassSwitch;
1505 xml::Element& p =
1506 jmt_param(section, "jmt.engine.NetStrategies.RoutingStrategy", "RoutingStrategy", true);
1507 for (std::size_t r = 1; r <= K; ++r) {
1508 if (!keep_[r - 1]) continue;
1509 jmt_ref_class(p, cname(r));
1510 const RoutingStrategy rs =
1511 is_cs ? RoutingStrategy::RAND
1512 : (sn_.nodes[ind - 1].routing.size() >= r ? sn_.nodes[ind - 1].routing[r - 1]
1513 : RoutingStrategy::PROB);
1514 xml::Element& sp = p.add_child("subParameter");
1515 switch (rs) {
1516 case RoutingStrategy::RAND:
1517 sp.set_attr("classPath",
1518 "jmt.engine.NetStrategies.RoutingStrategies.RandomStrategy");
1519 sp.set_attr("name", "Random");
1520 break;
1521 case RoutingStrategy::RROBIN:
1522 sp.set_attr("classPath",
1523 "jmt.engine.NetStrategies.RoutingStrategies.RoundRobinStrategy");
1524 sp.set_attr("name", "Round Robin");
1525 break;
1526 case RoutingStrategy::JSQ:
1527 sp.set_attr("classPath",
1528 "jmt.engine.NetStrategies.RoutingStrategies."
1529 "ShortestQueueLengthRoutingStrategy");
1530 sp.set_attr("name", "Join the Shortest Queue (JSQ)");
1531 break;
1532 case RoutingStrategy::SQ: {
1533 sp.set_attr("classPath",
1534 "jmt.engine.NetStrategies.RoutingStrategies.PowerOfKRoutingStrategy");
1535 sp.set_attr("name", "Power of k");
1536 const int dpar = sn_.nodes[ind - 1].routing_param.size() >= r
1537 ? sn_.nodes[ind - 1].routing_param[r - 1]
1538 : 0;
1539 jmt_scalar(sp, "java.lang.Integer", "k", jmt_int(static_cast<double>(dpar)));
1540 // Always false: LINE implements SQ(d) only. JMT's
1541 // withMemory selects Anselmi & Dufour SQ(d,N), a different
1542 // policy.
1543 jmt_scalar(sp, "java.lang.Boolean", "withMemory", "false");
1544 break;
1545 }
1546 case RoutingStrategy::WRROBIN: {
1547 sp.set_attr("classPath",
1548 "jmt.engine.NetStrategies.RoutingStrategies."
1549 "WeightedRoundRobinStrategy");
1550 sp.set_attr("name", "Weighted Round Robin");
1551 xml::Element& arr =
1552 jmt_param_sub(sp, "jmt.engine.NetStrategies.RoutingStrategies.WeightEntry",
1553 "WeightEntryArray");
1554 const std::map<std::size_t, double>& w =
1555 sn_.nodes[ind - 1].routing_weights.size() >= r
1556 ? sn_.nodes[ind - 1].routing_weights[r - 1]
1557 : empty_weights_;
1558 for (std::size_t j : outputs_of(ind)) {
1559 const auto wi = w.find(j);
1560 xml::Element& e = arr.add_child("subParameter");
1561 e.set_attr("classPath",
1562 "jmt.engine.NetStrategies.RoutingStrategies.WeightEntry");
1563 e.set_attr("name", "WeightEntry");
1564 jmt_scalar(e, "java.lang.String", "stationName", nname(j));
1565 jmt_scalar(e, "java.lang.Integer", "weight",
1566 jmt_int(wi == w.end() ? 0.0 : wi->second));
1567 }
1568 break;
1569 }
1570 case RoutingStrategy::PROB: {
1571 sp.set_attr("classPath",
1572 "jmt.engine.NetStrategies.RoutingStrategies.EmpiricalStrategy");
1573 sp.set_attr("name", "Probabilities");
1574 xml::Element& arr = jmt_param_sub(sp, "jmt.engine.random.EmpiricalEntry",
1575 "EmpiricalEntryArray");
1576 for (std::size_t j : outputs_of(ind)) {
1577 const double pr = route_node(ind, r, j, r);
1578 if (!(pr > 0.0)) continue;
1579 xml::Element& e = arr.add_child("subParameter");
1580 e.set_attr("classPath", "jmt.engine.random.EmpiricalEntry");
1581 e.set_attr("name", "EmpiricalEntry");
1582 jmt_scalar(e, "java.lang.String", "stationName", nname(j));
1583 jmt_scalar(e, "java.lang.Double", "probability", jmt_fmt(pr));
1584 }
1585 break;
1586 }
1587 default:
1588 sp.set_attr(
1589 "classPath",
1590 "jmt.engine.NetStrategies.RoutingStrategies.DisabledRoutingStrategy");
1591 sp.set_attr("name", "Random");
1592 break;
1593 }
1594 }
1595 }
1596
1597 /** `<subParameter array="true" classPath=CP name=NAME>` */
1598 static xml::Element& jmt_param_sub(xml::Element& parent, const char* cp, const char* name) {
1599 xml::Element& e = parent.add_child("subParameter");
1600 e.set_attr("array", "true");
1601 e.set_attr("classPath", cp);
1602 e.set_attr("name", name);
1603 return e;
1604 }
1605
1606 /** `sn.rtnodes((i-1)K+r, (j-1)K+s)`, guarded against an unbuilt matrix. */
1607 double route_node(std::size_t i, std::size_t r, std::size_t j, std::size_t s) const {
1608 const std::size_t K = sn_.nclasses;
1609 const std::size_t a = (i - 1) * K + (r - 1), b = (j - 1) * K + (s - 1);
1610 if (sn_.rtnodes.rows() <= a || sn_.rtnodes.cols() <= b) return 0.0;
1611 return d(sn_.rtnodes(a, b));
1612 }
1613
1614 /**
1615 * Port of `saveClassSwitchStrategy`: the (K x K) switching matrix as JMT
1616 * reads it, each cell the mass class r leaves this node with as class s.
1617 *
1618 * The cell is the SUM over the node's outgoing links of `rtnodes`, not the
1619 * declared `csmatrix`: the refresh has already multiplied the switch by the
1620 * routing, and the sum recovers the switching probability independently of
1621 * how the mass was split over the links.
1622 */
1623 void save_class_switch_strategy(xml::Element& section, std::size_t ind) {
1624 const std::size_t K = sn_.nclasses;
1625 const std::vector<std::size_t> jset = outputs_of(ind);
1626 xml::Element& p = jmt_param(section, "java.lang.Object", "matrix", true);
1627 for (std::size_t r = 1; r <= K; ++r) {
1628 if (!keep_[r - 1]) continue;
1629 jmt_ref_class(p, cname(r));
1630 xml::Element& row = p.add_child("subParameter");
1631 row.set_attr("array", "true");
1632 row.set_attr("classPath", "java.lang.Float");
1633 row.set_attr("name", "row");
1634 for (std::size_t s = 1; s <= K; ++s) {
1635 if (!keep_[s - 1]) continue;
1636 jmt_ref_class(row, cname(s));
1637 double acc = 0.0;
1638 for (std::size_t j : jset) acc += route_node(ind, r, j, s);
1639 xml::Element& cell = row.add_child("subParameter");
1640 cell.set_attr("classPath", "java.lang.Float");
1641 cell.set_attr("name", "cell");
1642 cell.add_text_child("value", jmt_fmt(acc));
1643 }
1644 }
1645 }
1646
1647 /**
1648 * Port of `saveForkStrategy`.
1649 *
1650 * ONE OutPath ENTRY PER BRANCH. The reference used to build the entry
1651 * inside a loop over the connected nodes but append it outside, so only the
1652 * LAST link survived, and the JAR and this port transcribed the same shape.
1653 * It was harmless only because `isSimplifiedFork` is true, which makes JMT
1654 * send one job down every outgoing link and ignore the branch list -- and it
1655 * stops being harmless the moment a branch carries its own probability or
1656 * its own jobs-per-link. All four codebases now emit the full list.
1657 */
1658 void save_fork_strategy(xml::Element& section, std::size_t ind) {
1659 const qn::ForkParam<T>* fk = sn_.fork_param_of(ind);
1660 const double fan_out = sn_.nodes[ind - 1].tasks_per_link;
1661 jmt_param_value(section, "java.lang.Integer", "jobsPerLink", jmt_int(fan_out));
1662 jmt_param_value(section, "java.lang.Integer", "block", "-1");
1663
1664 // isSimplifiedFork lets JMT ignore the branch list and send one job down
1665 // every link. That is only the same model when every branch is certain
1666 // and carries the same number of tasks, so a variable forking level
1667 // switches it off and makes JMT read the per-branch entries below.
1668 bool simplified = true;
1669 if (fk != 0) {
1670 for (std::size_t k = 0; k < fk->fan_out_link.rows() && simplified; ++k)
1671 for (std::size_t r = 0; r < fk->fan_out_link.cols() && simplified; ++r) {
1672 const double p = num_traits<T>::to_double(fk->fan_out_prob(k, r));
1673 if (p == 0.0) continue; // link not taken
1674 if (p != 1.0) simplified = false;
1675 if (num_traits<T>::to_double(fk->fan_out_link(k, r)) != fan_out)
1676 simplified = false;
1677 if (!fk->fan_out_dist[k][r].disabled) simplified = false;
1678 }
1679 }
1680 jmt_param_value(section, "java.lang.Boolean", "isSimplifiedFork",
1681 simplified ? "true" : "false");
1682
1683 xml::Element& p =
1684 jmt_param(section, "jmt.engine.NetStrategies.ForkStrategy", "ForkStrategy", true);
1685 const std::vector<std::size_t> outs = outputs_of(ind);
1686 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
1687 if (!keep_[r - 1]) continue;
1688 jmt_ref_class(p, cname(r));
1689 xml::Element& cs = p.add_child("subParameter");
1690 cs.set_attr("classPath", "jmt.engine.NetStrategies.ForkStrategies.ProbabilitiesFork");
1691 cs.set_attr("name", "Branch Probabilities");
1692 xml::Element& arr = jmt_param_sub(
1693 cs, "jmt.engine.NetStrategies.ForkStrategies.OutPath", "EmpiricalEntryArray");
1694 const RoutingStrategy rs = sn_.nodes[ind - 1].routing.size() >= r
1695 ? sn_.nodes[ind - 1].routing[r - 1]
1696 : RoutingStrategy::PROB;
1697 if (rs != RoutingStrategy::PROB || outs.empty()) continue;
1698 for (std::size_t oi = 0; oi < outs.size(); ++oi) {
1699 xml::Element& entry = arr.add_child("subParameter");
1700 entry.set_attr("classPath", "jmt.engine.NetStrategies.ForkStrategies.OutPath");
1701 entry.set_attr("name", "OutPathEntry");
1702 xml::Element& unit = entry.add_child("subParameter");
1703 unit.set_attr("classPath", "jmt.engine.random.EmpiricalEntry");
1704 unit.set_attr("name", "outUnitProbability");
1705 jmt_scalar(unit, "java.lang.String", "stationName", nname(outs[oi]));
1706
1707 // branch activation probability: JMT's outUnitProbability
1708 const std::size_t k0 = outs[oi] - 1, r0 = r - 1;
1709 const bool has_fan = fk != 0;
1710 const double branch_p =
1711 has_fan ? num_traits<T>::to_double(fk->fan_out_prob(k0, r0)) : 1.0;
1712 jmt_scalar(unit, "java.lang.Double", "probability", jmt_fmt(branch_p));
1713
1714 // JobsPerLinkDis is an EmpiricalEntry ARRAY: one entry per point
1715 // of the jobs-per-link distribution. A deterministic fork emits
1716 // the single degenerate entry it always did.
1717 std::vector<double> pts, prs;
1718 if (has_fan && !fk->fan_out_dist[k0][r0].disabled) {
1719 const lang::Distrib<T>& d = fk->fan_out_dist[k0][r0];
1720 double tot = 0.0;
1721 for (std::size_t e = 0; e < d.params.size(); ++e)
1722 tot += num_traits<T>::to_double(d.params[e]);
1723 for (std::size_t e = 0; e < d.params.size(); ++e) {
1724 pts.push_back(d.trace.empty()
1725 ? static_cast<double>(e + 1)
1726 : num_traits<T>::to_double(d.trace[e]));
1727 prs.push_back(num_traits<T>::to_double(d.params[e]) / tot);
1728 }
1729 } else if (has_fan) {
1730 pts.push_back(num_traits<T>::to_double(fk->fan_out_link(k0, r0)));
1731 prs.push_back(1.0);
1732 } else {
1733 pts.push_back(fan_out);
1734 prs.push_back(1.0);
1735 }
1736
1737 xml::Element& jpl =
1738 jmt_param_sub(entry, "jmt.engine.random.EmpiricalEntry", "JobsPerLinkDis");
1739 for (std::size_t e = 0; e < pts.size(); ++e) {
1740 xml::Element& jple = jpl.add_child("subParameter");
1741 jple.set_attr("classPath", "jmt.engine.random.EmpiricalEntry");
1742 jple.set_attr("name", "EmpiricalEntry");
1743 jmt_scalar(jple, "java.lang.String", "numbers", jmt_int(pts[e]));
1744 jmt_scalar(jple, "java.lang.Double", "probability", jmt_fmt(prs[e]));
1745 }
1746 }
1747 }
1748 }
1749
1750 /**
1751 * Port of `saveJoinStrategy`.
1752 *
1753 * `numRequired` is -1 for a standard join -- JMT's "every sibling" -- and
1754 * the quorum for a partial one. The reference reads two separate fields
1755 * (`fanIn` and `joinRequired`); this port's `JoinDecl` carries the quorum
1756 * once, with 0 meaning "every sibling", which is the same information.
1757 */
1758 void save_join_strategy(xml::Element& section, std::size_t ind) {
1759 const auto it = sn_.joindecl.find(ind);
1760 const bool partial =
1761 it != sn_.joindecl.end() && it->second.strategy == JoinStrategy::PARTIAL;
1762 const double quorum = it != sn_.joindecl.end() ? it->second.quorum : 0.0;
1763 xml::Element& p =
1764 jmt_param(section, "jmt.engine.NetStrategies.JoinStrategy", "JoinStrategy", true);
1765 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
1766 jmt_ref_class(p, cname(r));
1767 xml::Element& sp = p.add_child("subParameter");
1768 if (partial) {
1769 sp.set_attr("classPath", "jmt.engine.NetStrategies.JoinStrategies.PartialJoin");
1770 sp.set_attr("name", "Quorum");
1771 } else {
1772 sp.set_attr("classPath", "jmt.engine.NetStrategies.JoinStrategies.NormalJoin");
1773 sp.set_attr("name", "Standard Join");
1774 }
1775 const double req = partial ? quorum : (quorum > 0.0 ? quorum : -1.0);
1776 jmt_scalar(sp, "java.lang.Integer", "numRequired", jmt_int(req));
1777 }
1778 }
1779
1780 /** Port of `saveLogTunnel`: the ten fields JMT's LogTunnel section takes. */
1781 void save_log_tunnel(xml::Element& section, std::size_t ind) {
1782 const qn::NodeDef::LoggerParam& lg = sn_.nodes[ind - 1].logger;
1783 std::string path = lg.file_path.empty() ? sn_.log_path : lg.file_path;
1784 if (!path.empty() && path[path.size() - 1] != '/') path += '/';
1785 const char* bools[7] = {"logExecTimestamp", "logLoggerName", "logTimeStamp",
1786 "logJobID", "logJobClass", "logTimeSameClass",
1787 "logTimeAnyClass"};
1788 const bool vals[7] = {lg.start_time, lg.logger_name, lg.timestamp, lg.job_id,
1789 lg.job_class, lg.time_same_class, lg.time_any_class};
1790 jmt_param_value(section, "java.lang.String", "logfileName", lg.file_name);
1791 jmt_param_value(section, "java.lang.String", "logfilePath", path);
1792 for (int j = 0; j < 7; ++j)
1793 jmt_param_value(section, "java.lang.Boolean", bools[j], vals[j] ? "true" : "false");
1794 jmt_param_value(section, "java.lang.Integer", "numClasses",
1795 jmt_int(static_cast<double>(sn_.nclasses)));
1796 }
1797
1798 // -- model-level blocks -------------------------------------------------
1799
1800 /**
1801 * Port of `saveMetrics`: one `<measure>` per enabled (station, class, kind),
1802 * plus the FCR and cache-hit-rate measures.
1803 *
1804 * `Residence Time` is NOT requested, as in the reference: JMT's definition
1805 * of it disagrees with LINE's on class-switching models, and the residence
1806 * time is recomputed from the response time and the visits instead.
1807 */
1808 void save_metrics(xml::Element& sim) {
1812 for (int k = 0; k < 5; ++k) save_metric(sim, kinds[k]);
1813 // Tardiness needs a class deadline, which this port's struct does not
1814 // carry; the measures are therefore not requested. See save_classes.
1815 save_fcr_metrics(sim);
1816 save_cache_hit_rate_metrics(sim);
1817 }
1818
1819 void save_metric(xml::Element& sim, JmtMetricKind kind) {
1820 for (std::size_t ist = 1; ist <= sn_.nstations; ++ist)
1821 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
1822 if (!keep_[r - 1]) continue;
1823 if (!jmt_metric_enabled(sn_, kind, ist, r, cacheclass_)) continue;
1824 xml::Element& m = sim.add_child("measure");
1825 m.set_attr("alpha", jmt_sig2(1.0 - opt_.sim_conf_int));
1826 m.set_attr("name", std::string("Performance_") +
1827 jmt_int(static_cast<double>(ist)));
1828 m.set_attr("nodeType", "station");
1829 m.set_attr("precision", jmt_sig2(opt_.sim_max_rel_err));
1830 m.set_attr("referenceNode", nname(sn_.station_to_node[ist - 1]));
1831 m.set_attr("referenceUserClass", cname(r));
1832 m.set_attr("type", jmt_metric_text(kind));
1833 m.set_attr("verbose", "false");
1834 }
1835 }
1836
1837 /** Port of `saveCacheHitRateMetrics`. */
1838 void save_cache_hit_rate_metrics(xml::Element& sim) {
1839 for (const auto& kv : sn_.nodeparam) {
1840 const std::size_t ind = kv.first;
1841 if (sn_.nodes[ind - 1].nodetype != NodeType::Cache) continue;
1842 const qn::CacheParam<T>& cp = kv.second;
1843 for (std::size_t r = 0; r < cp.hitclass.size(); ++r) {
1844 if (cp.hitclass[r] == 0) continue;
1845 xml::Element& m = sim.add_child("measure");
1846 m.set_attr("alpha", jmt_sig2(1.0 - opt_.sim_conf_int));
1847 m.set_attr("name", "CacheHitRate_" + nname(ind) + "_" + cname(cp.hitclass[r]));
1848 m.set_attr("nodeType", "station");
1849 m.set_attr("precision", jmt_sig2(opt_.sim_max_rel_err));
1850 m.set_attr("referenceNode", nname(ind));
1851 m.set_attr("referenceUserClass", cname(cp.hitclass[r]));
1852 m.set_attr("type", "Cache Hit Rate");
1853 m.set_attr("verbose", "false");
1854 }
1855 }
1856 }
1857
1858 /** Port of `saveFCRMetrics`: six aggregate measures per region. */
1859 void save_fcr_metrics(xml::Element& sim) {
1860 static const char* kinds[6] = {"Number of Customers", "Response Time", "Residence Time",
1861 "Throughput", "FCR Capacity", "FCR Memory"};
1862 int counter = 0;
1863 for (std::size_t f = 1; f <= sn_.regions.size(); ++f) {
1864 const std::string fcr = "FCRegion" + jmt_int(static_cast<double>(f));
1865 for (int k = 0; k < 6; ++k) {
1866 std::string flat(kinds[k]);
1867 flat.erase(std::remove(flat.begin(), flat.end(), ' '), flat.end());
1868 xml::Element& m = sim.add_child("measure");
1869 m.set_attr("alpha", jmt_sig2(1.0 - opt_.sim_conf_int));
1870 m.set_attr("name", "FCR_" + fcr + "_" + flat + "_" +
1871 jmt_int(static_cast<double>(counter)));
1872 m.set_attr("nodeType", "region");
1873 m.set_attr("precision", jmt_sig2(opt_.sim_max_rel_err));
1874 m.set_attr("referenceNode", fcr);
1875 m.set_attr("referenceUserClass", "");
1876 m.set_attr("type", kinds[k]);
1877 m.set_attr("verbose", "false");
1878 ++counter;
1879 }
1880 }
1881 }
1882
1883 /** Port of `saveLinks`: one `<connection>` per linked ordered pair. */
1884 void save_links(xml::Element& sim) {
1885 // Column-major, as MATLAB's `find` on the connection matrix returns.
1886 for (std::size_t j = 1; j <= sn_.nodes.size(); ++j)
1887 for (std::size_t i = 1; i <= sn_.nodes.size(); ++i) {
1888 if (!conn_[i - 1][j - 1]) continue;
1889 xml::Element& c = sim.add_child("connection");
1890 c.set_attr("source", nname(i));
1891 c.set_attr("target", nname(j));
1892 }
1893 }
1894
1895 /**
1896 * Port of the `preload` block of `writeJSIM`: where the jobs start.
1897 *
1898 * A Source has an infinite reservoir and a Join holds no jobs, so neither
1899 * takes a preload. Every other station reports its per-class count, and a
1900 * closed class with no customers anywhere is omitted -- JMT reads an
1901 * omitted class as zero, and writing the zero would also declare the class
1902 * present at a station it never visits.
1903 */
1904 void save_preload(xml::Element& sim) {
1905 // Collected before anything is emitted: the block is omitted entirely
1906 // when no station takes a preload, and an element already appended to
1907 // the document cannot be withdrawn.
1908 struct Row {
1909 std::size_t node;
1910 std::vector<std::pair<std::size_t, double>> pops; // (class, count)
1911 };
1912 std::vector<Row> rows;
1913 for (std::size_t ist = 1; ist <= sn_.nstations; ++ist) {
1914 const std::size_t ind = sn_.station_to_node[ist - 1];
1915 const NodeType ty = sn_.nodes[ind - 1].nodetype;
1916 if (ty == NodeType::Source || ty == NodeType::Join) continue;
1917 const std::vector<double> nir = initial_marginal(ist);
1918 Row row;
1919 row.node = ind;
1920 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
1921 const double n = sn_.classes[r - 1].population;
1922 if (std::isfinite(n) && n == 0.0 && nir[r - 1] == 0.0) continue;
1923 row.pops.emplace_back(r, nir[r - 1]);
1924 }
1925 if (!row.pops.empty()) rows.push_back(row);
1926 }
1927 if (rows.empty()) return;
1928 xml::Element& preload = sim.add_child("preload");
1929 for (const Row& row : rows) {
1930 xml::Element& st = preload.add_child("stationPopulations");
1931 st.set_attr("stationName", nname(row.node));
1932 for (const auto& pc : row.pops) {
1933 xml::Element& cp = st.add_child("classPopulation");
1934 cp.set_attr("population", jmt_int(pc.second));
1935 cp.set_attr("refClass", cname(pc.first));
1936 }
1937 }
1938 }
1939
1940 /**
1941 * The per-class job count a station starts with.
1942 *
1943 * A Place's declared initial marking is its token count; every other
1944 * station takes `model.initDefault`, which puts each closed class's whole
1945 * population at its reference station and nothing anywhere else. Open
1946 * classes start empty.
1947 */
1948 std::vector<double> initial_marginal(std::size_t ist) const {
1949 std::vector<double> nir(sn_.nclasses, 0.0);
1950 const std::size_t ind = sn_.station_to_node[ist - 1];
1951 const auto im = sn_.initmarking.find(ind);
1952 if (im != sn_.initmarking.end()) {
1953 for (std::size_t r = 0; r < sn_.nclasses && r < im->second.size(); ++r)
1954 nir[r] = d(im->second[r]);
1955 return nir;
1956 }
1957 for (std::size_t r = 0; r < sn_.nclasses; ++r) {
1958 const double n = sn_.classes[r].population;
1959 if (std::isfinite(n) && sn_.classes[r].refstat == ist) nir[r] = n;
1960 }
1961 return nir;
1962 }
1963
1964 // -- Place (Storage / Linkage) ------------------------------------------
1965
1966 /** Port of `saveTotalCapacity`: the token bound of a place, -1 unbounded. */
1967 void save_total_capacity(xml::Element& section, std::size_t ind) {
1968 const std::size_t ist = sn_.nodes[ind - 1].station;
1969 const std::string v =
1970 (ist == 0 || !std::isfinite(sn_.cap[ist - 1])) ? "-1" : jmt_int(sn_.cap[ist - 1]);
1971 jmt_param_value(section, "java.lang.Integer", "totalCapacity", v);
1972 }
1973
1974 /** Port of `savePlaceCapacities`: the per-colour token bound. */
1975 void save_place_capacities(xml::Element& section, std::size_t ind) {
1976 const std::size_t ist = sn_.nodes[ind - 1].station;
1977 xml::Element& p = jmt_param(section, "java.lang.Integer", "capacities", true);
1978 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
1979 if (!keep_[r - 1]) continue;
1980 jmt_ref_class(p, cname(r));
1981 const double c = ist == 0 ? std::numeric_limits<double>::infinity()
1982 : sn_.classcap[ist - 1][r - 1];
1983 xml::Element& sp = p.add_child("subParameter");
1984 sp.set_attr("classPath", "java.lang.Integer");
1985 sp.set_attr("name", "capacity");
1986 sp.add_text_child("value", std::isfinite(c) ? jmt_int(c) : "-1");
1987 }
1988 }
1989
1990 /**
1991 * Port of `saveDropRule`. It differs from `saveDropStrategy` only in the
1992 * parameter names JMT's Storage section expects (`dropRules`/`dropRule`).
1993 */
1994 void save_drop_rule(xml::Element& section, std::size_t ind) {
1995 const std::size_t ist = sn_.nodes[ind - 1].station;
1996 xml::Element& p = jmt_param(section, "java.lang.String", "dropRules", true);
1997 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
1998 if (!keep_[r - 1]) continue;
1999 jmt_ref_class(p, cname(r));
2000 xml::Element& sp = p.add_child("subParameter");
2001 sp.set_attr("classPath", "java.lang.String");
2002 sp.set_attr("name", "dropRule");
2003 sp.add_text_child("value", drop_strategy_text(ist, r));
2004 }
2005 }
2006
2007 /**
2008 * Port of `savePutStrategies` (plural), the Storage section's insertion
2009 * rule. It offers only the three orders a place can hold tokens in, which
2010 * is why it is a separate handler from `savePutStrategy`.
2011 */
2012 void save_put_strategies(xml::Element& section, std::size_t ind) {
2013 const std::size_t ist = sn_.nodes[ind - 1].station;
2014 xml::Element& p = jmt_param(section, "jmt.engine.NetStrategies.QueuePutStrategy",
2015 "QueuePutStrategy", true);
2016 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2017 if (!keep_[r - 1]) continue;
2018 jmt_ref_class(p, cname(r));
2019 const char* nm = "TailStrategy";
2020 if (ist != 0) {
2021 if (sn_.stations[ist - 1].sched == SchedStrategy::SIRO) nm = "RandStrategy";
2022 else if (sn_.stations[ist - 1].sched == SchedStrategy::LCFS) nm = "HeadStrategy";
2023 }
2024 xml::Element& sp = p.add_child("subParameter");
2025 sp.set_attr("classPath",
2026 std::string("jmt.engine.NetStrategies.QueuePutStrategies.") + nm);
2027 sp.set_attr("name", nm);
2028 }
2029 }
2030
2031 // -- Transition (Enabling / Timing / Firing) ----------------------------
2032
2033 /** The transition parameters of node `ind`; refuses a node that has none. */
2034 const qn::TransitionParam<T>& transparam(std::size_t ind) const {
2035 const auto it = sn_.transparam.find(ind);
2036 if (it == sn_.transparam.end())
2037 throw InputError("SolverJMT: transition '" + nname(ind) + "' carries no mode table");
2038 return it->second;
2039 }
2040
2041 /**
2042 * Port of `saveEnablingConditions`.
2043 *
2044 * A place appears in the vector only when it carries a POSITIVE enabling or
2045 * inhibiting entry for the mode IN SOME CLASS. An enabling entry of infinity
2046 * is JMT's -1, "any number of tokens".
2047 *
2048 * THE ENTRIES ARE PER CLASS, which is what JSIM's own vector format is: one
2049 * `enablingEntry` per class, in class order. Writing the same number under
2050 * every class -- what this writer did while the struct had no class
2051 * dimension -- exported a net that demands every colour on every arc, and
2052 * that is why a multiclass net had to be refused here.
2053 */
2054 void save_enabling_conditions(xml::Element& section, std::size_t ind) {
2055 const qn::TransitionParam<T>& tp = transparam(ind);
2056 xml::Element& p =
2057 jmt_param(section, "jmt.engine.NetStrategies.TransitionUtilities.TransitionMatrix",
2058 "enablingConditions", true);
2059 for (std::size_t m = 0; m < tp.nmodes; ++m) {
2060 xml::Element& cond = p.add_child("subParameter");
2061 cond.set_attr("classPath",
2062 "jmt.engine.NetStrategies.TransitionUtilities.TransitionMatrix");
2063 cond.set_attr("name", "enablingCondition");
2064 xml::Element& vecs = jmt_param_sub(
2065 cond, "jmt.engine.NetStrategies.TransitionUtilities.TransitionVector",
2066 "enablingVectors");
2067 for (std::size_t k = 1; k <= sn_.nodes.size(); ++k) {
2068 bool relevant = false;
2069 for (std::size_t r = 1; r <= sn_.nclasses && !relevant; ++r) {
2070 const double en = arc(tp.enabling, m, k, r);
2071 const double in = arc(tp.inhibiting, m, k, r);
2072 relevant = (std::isfinite(en) && en > 0.0) ||
2073 (std::isfinite(in) && in > 0.0);
2074 }
2075 if (!relevant) continue;
2076 xml::Element& vec = vecs.add_child("subParameter");
2077 vec.set_attr("classPath",
2078 "jmt.engine.NetStrategies.TransitionUtilities.TransitionVector");
2079 vec.set_attr("name", "enablingVector");
2080 jmt_scalar(vec, "java.lang.String", "stationName", nname(k));
2081 xml::Element& entries = jmt_param_sub(vec, "java.lang.Integer", "enablingEntries");
2082 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2083 const double en = arc(tp.enabling, m, k, r);
2084 jmt_ref_class(entries, cname(r));
2085 xml::Element& e = entries.add_child("subParameter");
2086 e.set_attr("classPath", "java.lang.Integer");
2087 e.set_attr("name", "enablingEntry");
2088 e.add_text_child("value", std::isfinite(en) ? jmt_int(en) : "-1");
2089 }
2090 }
2091 }
2092 }
2093
2094 /**
2095 * Port of `saveInhibitingConditions`.
2096 *
2097 * The vectors cover the transition's INPUT places only, and an infinite
2098 * entry -- "no inhibitor arc" -- is written as 0, which is what JMT reads as
2099 * "never inhibits". Note that the reference's sentinel is inverted between
2100 * the two handlers: infinity is -1 in the enabling block and 0 here.
2101 */
2102 void save_inhibiting_conditions(xml::Element& section, std::size_t ind) {
2103 const qn::TransitionParam<T>& tp = transparam(ind);
2104 const std::vector<std::size_t> inputs = inputs_of(ind);
2105 xml::Element& p =
2106 jmt_param(section, "jmt.engine.NetStrategies.TransitionUtilities.TransitionMatrix",
2107 "inhibitingConditions", true);
2108 for (std::size_t m = 0; m < tp.nmodes; ++m) {
2109 xml::Element& cond = p.add_child("subParameter");
2110 cond.set_attr("classPath",
2111 "jmt.engine.NetStrategies.TransitionUtilities.TransitionMatrix");
2112 cond.set_attr("name", "inhibitingCondition");
2113 xml::Element& vecs = jmt_param_sub(
2114 cond, "jmt.engine.NetStrategies.TransitionUtilities.TransitionVector",
2115 "inhibitingVectors");
2116 for (std::size_t k : inputs) {
2117 xml::Element& vec = vecs.add_child("subParameter");
2118 vec.set_attr("classPath",
2119 "jmt.engine.NetStrategies.TransitionUtilities.TransitionVector");
2120 vec.set_attr("name", "inhibitingVector");
2121 jmt_scalar(vec, "java.lang.String", "stationName", nname(k));
2122 xml::Element& entries = jmt_param_sub(vec, "java.lang.Integer", "inhibitingEntries");
2123 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2124 const double in = arc(tp.inhibiting, m, k, r);
2125 jmt_ref_class(entries, cname(r));
2126 xml::Element& e = entries.add_child("subParameter");
2127 e.set_attr("classPath", "java.lang.Integer");
2128 e.set_attr("name", "inhibitingEntry");
2129 e.add_text_child("value", std::isfinite(in) ? jmt_int(in) : "0");
2130 }
2131 }
2132 }
2133 }
2134
2135 /** Port of `saveFiringOutcomes`: the tokens a firing puts in each output. */
2136 void save_firing_outcomes(xml::Element& section, std::size_t ind) {
2137 const qn::TransitionParam<T>& tp = transparam(ind);
2138 const std::vector<std::size_t> outs = outputs_of(ind);
2139 xml::Element& p =
2140 jmt_param(section, "jmt.engine.NetStrategies.TransitionUtilities.TransitionMatrix",
2141 "firingOutcomes", true);
2142 for (std::size_t m = 0; m < tp.nmodes; ++m) {
2143 xml::Element& out = p.add_child("subParameter");
2144 out.set_attr("classPath",
2145 "jmt.engine.NetStrategies.TransitionUtilities.TransitionMatrix");
2146 out.set_attr("name", "firingOutcome");
2147 xml::Element& vecs = jmt_param_sub(
2148 out, "jmt.engine.NetStrategies.TransitionUtilities.TransitionVector",
2149 "firingVectors");
2150 for (std::size_t k : outs) {
2151 xml::Element& vec = vecs.add_child("subParameter");
2152 vec.set_attr("classPath",
2153 "jmt.engine.NetStrategies.TransitionUtilities.TransitionVector");
2154 vec.set_attr("name", "firingVector");
2155 jmt_scalar(vec, "java.lang.String", "stationName", nname(k));
2156 xml::Element& entries = jmt_param_sub(vec, "java.lang.Integer", "firingEntries");
2157 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2158 const double f = arc(tp.firing, m, k, r);
2159 jmt_ref_class(entries, cname(r));
2160 xml::Element& e = entries.add_child("subParameter");
2161 e.set_attr("classPath", "java.lang.Integer");
2162 e.set_attr("name", "firingEntry");
2163 e.add_text_child("value", jmt_int(f));
2164 }
2165 }
2166 }
2167 }
2168
2169 /** `enabling`/`inhibiting`/`firing` of mode m at 1-based node k, class r. */
2170 double arc(const std::vector<Matrix<T>>& tab, std::size_t m, std::size_t k,
2171 std::size_t r) const {
2172 if (m >= tab.size() || k == 0 || k > tab[m].rows() || r == 0 || r > tab[m].cols())
2173 return 0.0;
2174 return d(tab[m](k - 1, r - 1));
2175 }
2176
2177 /** Port of `saveModeNames`. */
2178 void save_mode_names(xml::Element& section, std::size_t ind) {
2179 const qn::TransitionParam<T>& tp = transparam(ind);
2180 xml::Element& p = jmt_param(section, "java.lang.String", "modeNames", true);
2181 for (std::size_t m = 0; m < tp.nmodes; ++m) {
2182 xml::Element& sp = p.add_child("subParameter");
2183 sp.set_attr("classPath", "java.lang.String");
2184 sp.set_attr("name", "modeName");
2185 sp.add_text_child("value", m < tp.modenames.size() ? tp.modenames[m] : std::string());
2186 }
2187 }
2188
2189 /** Port of `saveNumbersOfServers`: the firing concurrency of each mode. */
2190 void save_numbers_of_servers(xml::Element& section, std::size_t ind) {
2191 const qn::TransitionParam<T>& tp = transparam(ind);
2192 xml::Element& p = jmt_param(section, "java.lang.Integer", "numbersOfServers", true);
2193 for (std::size_t m = 0; m < tp.nmodes; ++m) {
2194 const double ns = m < tp.nmodeservers.size() ? tp.nmodeservers[m] : 1.0;
2195 xml::Element& sp = p.add_child("subParameter");
2196 sp.set_attr("classPath", "java.lang.Integer");
2197 sp.set_attr("name", "numberOfServers");
2198 sp.add_text_child("value", std::isfinite(ns) ? jmt_int(ns) : "-1");
2199 }
2200 }
2201
2202 /**
2203 * Port of `saveTimingStrategies`.
2204 *
2205 * An immediate mode is a ZeroServiceTimeStrategy and carries no
2206 * distribution; a timed one carries its firing process through the shared
2207 * emitter, under JMT's `timingStrategy` name rather than the
2208 * `ServiceTimeStrategy` name a queue's service uses.
2209 */
2210 void save_timing_strategies(xml::Element& section, std::size_t ind) {
2211 const qn::TransitionParam<T>& tp = transparam(ind);
2212 xml::Element& p = jmt_param(section, "jmt.engine.NetStrategies.ServiceStrategy",
2213 "timingStrategies", true);
2214 for (std::size_t m = 0; m < tp.nmodes; ++m) {
2215 xml::Element& sp = p.add_child("subParameter");
2216 const bool immediate = m < tp.timing.size() &&
2217 tp.timing[m] == lang::TimingStrategy::IMMEDIATE;
2218 if (immediate) {
2219 sp.set_attr("classPath",
2220 "jmt.engine.NetStrategies.ServiceStrategies.ZeroServiceTimeStrategy");
2221 sp.set_attr("name", "ZeroServiceTimeStrategy");
2222 continue;
2223 }
2224 sp.set_attr("classPath",
2225 "jmt.engine.NetStrategies.ServiceStrategies.ServiceTimeStrategy");
2226 sp.set_attr("name", "timingStrategy");
2227 if (m >= tp.firingproc.size())
2228 throw InputError("SolverJMT: timed mode '" +
2229 (m < tp.modenames.size() ? tp.modenames[m] : std::string()) +
2230 "' of transition '" + nname(ind) + "' has no firing process");
2231 jmt_append_distribution(sp, jmt_dist_view(tp.firingproc[m]),
2232 "SolverJMT (transition firing)");
2233 }
2234 }
2235
2236 /** Port of `saveFiringPriorities`. */
2237 void save_firing_priorities(xml::Element& section, std::size_t ind) {
2238 const qn::TransitionParam<T>& tp = transparam(ind);
2239 xml::Element& p = jmt_param(section, "java.lang.Integer", "firingPriorities", true);
2240 for (std::size_t m = 0; m < tp.nmodes; ++m) {
2241 const double v = m < tp.firingprio.size() ? tp.firingprio[m] : 1.0;
2242 xml::Element& sp = p.add_child("subParameter");
2243 sp.set_attr("classPath", "java.lang.Integer");
2244 sp.set_attr("name", "firingPriority");
2245 sp.add_text_child("value", std::isfinite(v) ? jmt_int(v) : "-1");
2246 }
2247 }
2248
2249 /**
2250 * Port of `saveFiringWeights`.
2251 *
2252 * THE REFERENCE PRINTS THE WEIGHT AS AN INTEGER (`int2str`) although the
2253 * parameter is a `java.lang.Double`, so a weight of 0.3 exports as 0 and
2254 * the mode never wins a race it should sometimes win. That is a defect and
2255 * not a convention -- no other weight in the file is rounded -- so this
2256 * port writes the weight itself.
2257 */
2258 void save_firing_weights(xml::Element& section, std::size_t ind) {
2259 const qn::TransitionParam<T>& tp = transparam(ind);
2260 xml::Element& p = jmt_param(section, "java.lang.Double", "firingWeights", true);
2261 for (std::size_t m = 0; m < tp.nmodes; ++m) {
2262 const double v = m < tp.fireweight.size() ? d(tp.fireweight[m]) : 1.0;
2263 xml::Element& sp = p.add_child("subParameter");
2264 sp.set_attr("classPath", "java.lang.Double");
2265 sp.set_attr("name", "firingWeight");
2266 sp.add_text_child("value", std::isfinite(v) ? jmt_fmt(v) : "-1");
2267 }
2268 }
2269
2270 // -- blocking regions ---------------------------------------------------
2271
2272 /**
2273 * Port of `jmtClassCapCon`: the per-(station, class) capacities that are a
2274 * REAL constraint and must therefore reach JMT as a blocking region.
2275 *
2276 * A capacity is real only when it binds: not at a Source or a Place, not on
2277 * a pair the class never visits, and not when it already exceeds the
2278 * station's own capacity or the population of the class's chain -- such a
2279 * bound can never be reached, and exporting it as a region would add a
2280 * region JMT then reports measures for.
2281 */
2282 std::vector<std::vector<double>> class_cap_constraints() const {
2283 const double inf = std::numeric_limits<double>::infinity();
2284 std::vector<std::vector<double>> con(sn_.nstations, std::vector<double>(sn_.nclasses, inf));
2285 if (sn_.classcap.empty()) return con;
2286 std::vector<double> chainpop(sn_.nclasses, inf);
2287 for (std::size_t c = 0; c < sn_.inchain.size(); ++c) {
2288 double tot = 0.0;
2289 bool open = false;
2290 for (std::size_t r : sn_.inchain[c]) {
2291 const double n = sn_.classes[r - 1].population;
2292 if (std::isfinite(n)) tot += n; else open = true;
2293 }
2294 for (std::size_t r : sn_.inchain[c]) chainpop[r - 1] = open ? inf : tot;
2295 }
2296 for (std::size_t ist = 1; ist <= sn_.nstations; ++ist) {
2297 const NodeType ty = sn_.nodes[sn_.station_to_node[ist - 1] - 1].nodetype;
2298 if (ty == NodeType::Source || ty == NodeType::Place) continue;
2299 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2300 if (sn_.disabled[ist - 1][r - 1]) continue;
2301 const double cc = sn_.classcap[ist - 1][r - 1];
2302 if (std::isfinite(cc) && cc < 2147483647.0 &&
2303 cc < std::min(sn_.cap[ist - 1], chainpop[r - 1]))
2304 con[ist - 1][r - 1] = cc;
2305 }
2306 }
2307 return con;
2308 }
2309
2310 /**
2311 * Port of `jmtClassCapAssert`: the two per-class capacities JMT cannot
2312 * reproduce, refused by name rather than exported as something else.
2313 *
2314 * A CLOSED class is the first. LINE holds a blocked closed job at its
2315 * upstream station; JMT's blocking region parks it in the region's input
2316 * station instead, which frees the upstream server and loses the job from
2317 * the population count -- a different model, not a different rounding.
2318 * A non-loss drop strategy is the second: a region can only drop or defer.
2319 */
2320 void assert_class_cap_exportable(std::size_t ist, std::size_t r) const {
2321 const std::string sname = nname(sn_.station_to_node[ist - 1]);
2322 if (std::isfinite(sn_.classes[r - 1].population))
2323 throw UnsupportedError(
2324 "SolverJMT: station '" + sname + "' carries a finite capacity " +
2325 jmt_int(sn_.classcap[ist - 1][r - 1]) + " for the closed class '" + cname(r) +
2326 "'. LINE holds a blocked closed job at its upstream station, whereas JMT can "
2327 "only express a per-class capacity as a blocking region, which parks the job in "
2328 "the region input station instead, freeing the upstream server and losing it "
2329 "from the population count. Use SolverCTMC, SolverSSA or SolverLDES, or express "
2330 "the limit as the station capacity");
2331 const DropStrategy dr = sn_.droprule[ist - 1][r - 1];
2332 if (dr == DropStrategy::BAS || dr == DropStrategy::BBS || dr == DropStrategy::RSRD ||
2333 dr == DropStrategy::RETRIAL || dr == DropStrategy::RETRIAL_WITH_LIMIT)
2334 throw UnsupportedError(
2335 "SolverJMT: station '" + sname + "' applies drop strategy '" + jmt_drop_text(dr) +
2336 "' to class '" + cname(r) +
2337 "' and also carries a finite capacity for it. JMT exports a per-class capacity "
2338 "as a blocking region, which can only drop or defer an arrival. Remove the "
2339 "per-class capacity, or use the station capacity, which is exported with its "
2340 "drop strategy");
2341 }
2342
2343 /**
2344 * Port of `saveRegions`: three kinds of `<blockingRegion>`, in this order.
2345 *
2346 * 1. ONE PER LPS STATION. Limited processor sharing is a cap on the number
2347 * IN SERVICE, which JMT has no station-level field for; the single-node
2348 * region is how the limit is expressed, and it is why
2349 * `save_number_of_servers` exports an LPS station as a single server.
2350 * 2. The model's declared finite capacity regions.
2351 * 3. ONE PER STATION whose per-class capacity is a real constraint and that
2352 * no region above already covers, since JMT allows a node to belong to
2353 * at most one region.
2354 */
2355 void save_regions(xml::Element& sim) {
2356 const std::vector<std::vector<double>> con = class_cap_constraints();
2357 std::vector<bool> covered(sn_.nstations, false);
2358 std::size_t lps_idx = sn_.regions.size();
2359
2360 for (std::size_t ist = 1; ist <= sn_.nstations; ++ist) {
2361 if (sn_.stations[ist - 1].sched != SchedStrategy::LPS) continue;
2362 ++lps_idx;
2363 const std::size_t ind = sn_.station_to_node[ist - 1];
2364 const double limit = sn_.stations[ist - 1].schedparam.empty()
2365 ? 1.0
2366 : d(sn_.stations[ist - 1].schedparam[0]);
2367 xml::Element& br = sim.add_child("blockingRegion");
2368 br.set_attr("name", "LPSRegion" + jmt_int(static_cast<double>(lps_idx)));
2369 br.set_attr("type", "default");
2370 br.add_child("regionNode").set_attr("nodeName", nname(ind));
2371 br.add_child("globalConstraint").set_attr("maxJobs", jmt_num(limit));
2372 br.add_child("globalMemoryConstraint").set_attr("maxMemory", "-1");
2373 covered[ist - 1] = true;
2374 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2375 if (!std::isfinite(con[ist - 1][r - 1])) continue;
2376 assert_class_cap_exportable(ist, r); // rejects the non-loss cases first
2377 throw UnsupportedError(
2378 "SolverJMT: station '" + nname(ind) +
2379 "' has both LPS scheduling and a finite capacity for the open class '" +
2380 cname(r) +
2381 "'. JMT expresses both through a single blocking region, which admits only "
2382 "one drop rule per class, but LPS requires blocking while the open-class "
2383 "capacity requires dropping. Remove the per-class capacity or use a non-LPS "
2384 "scheduling strategy");
2385 }
2386 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2387 xml::Element& dr = br.add_child("dropRules");
2388 dr.set_attr("jobClass", cname(r));
2389 dr.set_attr("dropThisClass", "false");
2390 }
2391 }
2392
2393 for (std::size_t f = 1; f <= sn_.regions.size(); ++f) {
2394 const typename qn::NetworkStruct<T>::Region& rg = sn_.regions[f - 1];
2395 std::vector<std::size_t> members;
2396 for (std::size_t ist = 1; ist <= sn_.nstations; ++ist)
2397 if (ist <= rg.members.size() && rg.members[ist - 1]) {
2398 members.push_back(ist);
2399 covered[ist - 1] = true;
2400 }
2401 std::vector<double> region_class_cap(sn_.nclasses,
2402 std::numeric_limits<double>::infinity());
2403 for (std::size_t ist : members)
2404 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2405 if (!std::isfinite(con[ist - 1][r - 1])) continue;
2406 if (members.size() > 1)
2407 throw UnsupportedError(
2408 "SolverJMT: station '" + nname(sn_.station_to_node[ist - 1]) +
2409 "' carries a finite capacity for class '" + cname(r) +
2410 "' and also belongs to the multi-station region FCRegion" +
2411 jmt_int(static_cast<double>(f)) +
2412 ". JMT constrains a blocking region as a whole and allows a node to "
2413 "belong to only one region, so a per-station class capacity cannot "
2414 "be expressed alongside it");
2415 assert_class_cap_exportable(ist, r);
2416 if (!(r <= rg.rule.size() && rg.rule[r - 1] == DropStrategy::DROP))
2417 throw UnsupportedError(
2418 "SolverJMT: station '" + nname(sn_.station_to_node[ist - 1]) +
2419 "' carries a finite capacity for class '" + cname(r) +
2420 "' and also belongs to region FCRegion" +
2421 jmt_int(static_cast<double>(f)) +
2422 ", whose drop rule for that class is not DROP. A JMT blocking "
2423 "region admits a single drop rule per class, shared by all of its "
2424 "constraints, and the per-class capacity of an open class is a loss "
2425 "constraint");
2426 region_class_cap[r - 1] = con[ist - 1][r - 1];
2427 }
2428
2429 xml::Element& br = sim.add_child("blockingRegion");
2430 br.set_attr("name", "FCRegion" + jmt_int(static_cast<double>(f)));
2431 br.set_attr("type", "default");
2432 for (std::size_t ist : members)
2433 br.add_child("regionNode")
2434 .set_attr("nodeName", nname(sn_.station_to_node[ist - 1]));
2435 // The global caps are stored per MEMBER station and are the same at
2436 // each; the first member therefore carries the region's own values.
2437 const double gmax =
2438 members.empty() ? -1.0 : rg.cap[members[0] - 1][sn_.nclasses];
2439 const double gmem = members.empty() ? -1.0 : rg.maxmem[members[0] - 1];
2440 br.add_child("globalConstraint").set_attr("maxJobs", jmt_num(gmax));
2441 br.add_child("globalMemoryConstraint").set_attr("maxMemory", jmt_num(gmem));
2442 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2443 double cmax = members.empty() ? -1.0 : rg.cap[members[0] - 1][r - 1];
2444 // -1 is the struct's "unbounded"; the per-station capacity then
2445 // stands alone, and otherwise the tighter of the two binds.
2446 if (cmax == -1.0)
2447 cmax = std::isfinite(region_class_cap[r - 1]) ? region_class_cap[r - 1] : -1.0;
2448 else if (std::isfinite(region_class_cap[r - 1]))
2449 cmax = std::min(cmax, region_class_cap[r - 1]);
2450 if (cmax == -1.0 || !std::isfinite(cmax)) continue;
2451 xml::Element& cc = br.add_child("classConstraint");
2452 cc.set_attr("jobClass", cname(r));
2453 cc.set_attr("maxJobsPerClass", jmt_num(cmax));
2454 }
2455 // NO `classMemoryConstraint` IS EMITTED. `add_region` already folds
2456 // the per-class memory budget into the per-class job cap by
2457 // dividing it by the class size, so the constraint above carries it;
2458 // emitting it again would apply the same budget twice.
2459 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2460 xml::Element& dr = br.add_child("dropRules");
2461 dr.set_attr("jobClass", cname(r));
2462 dr.set_attr("dropThisClass",
2463 (r <= rg.rule.size() && rg.rule[r - 1] == DropStrategy::DROP)
2464 ? "true"
2465 : "false");
2466 }
2467 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2468 if (r > rg.weight.size()) continue;
2469 const double w = d(rg.weight[r - 1]);
2470 if (w == 1.0) continue;
2471 xml::Element& cw = br.add_child("classWeight");
2472 cw.set_attr("jobClass", cname(r));
2473 cw.set_attr("weight", jmt_num(w));
2474 }
2475 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2476 if (r > rg.size.size()) continue;
2477 const double s = d(rg.size[r - 1]);
2478 if (s == 1.0) continue;
2479 xml::Element& cs = br.add_child("classSize");
2480 cs.set_attr("jobClass", cname(r));
2481 cs.set_attr("size", jmt_num(s));
2482 }
2483 }
2484
2485 std::size_t cap_idx = 0;
2486 for (std::size_t ist = 1; ist <= sn_.nstations; ++ist) {
2487 if (covered[ist - 1]) continue;
2488 bool any = false;
2489 for (std::size_t r = 1; r <= sn_.nclasses; ++r)
2490 if (std::isfinite(con[ist - 1][r - 1])) any = true;
2491 if (!any) continue;
2492 ++cap_idx;
2493 for (std::size_t r = 1; r <= sn_.nclasses; ++r)
2494 if (std::isfinite(con[ist - 1][r - 1])) assert_class_cap_exportable(ist, r);
2495 xml::Element& br = sim.add_child("blockingRegion");
2496 br.set_attr("name", "ClassCapRegion" + jmt_int(static_cast<double>(cap_idx)));
2497 br.set_attr("type", "default");
2498 br.add_child("regionNode")
2499 .set_attr("nodeName", nname(sn_.station_to_node[ist - 1]));
2500 br.add_child("globalConstraint").set_attr("maxJobs", "-1");
2501 br.add_child("globalMemoryConstraint").set_attr("maxMemory", "-1");
2502 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2503 if (!std::isfinite(con[ist - 1][r - 1])) continue;
2504 xml::Element& cc = br.add_child("classConstraint");
2505 cc.set_attr("jobClass", cname(r));
2506 cc.set_attr("maxJobsPerClass", jmt_num(con[ist - 1][r - 1]));
2507 }
2508 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2509 if (!std::isfinite(con[ist - 1][r - 1])) continue;
2510 xml::Element& dr = br.add_child("dropRules");
2511 dr.set_attr("jobClass", cname(r));
2512 dr.set_attr("dropThisClass", "true");
2513 }
2514 }
2515 }
2516
2517 // -- Cache --------------------------------------------------------------
2518
2519 /**
2520 * Port of `saveCacheStrategy`.
2521 *
2522 * THE LIST-TO-LIST MATRIX IS DERIVED FROM THE POLICY, not from `accost`:
2523 * LRU promotes an item one list up on a hit and the last list keeps it,
2524 * every other policy keeps it where it is. That is what the reference
2525 * emits, and JMT's own replacement object does the rest.
2526 *
2527 * SFIFO is exported as JMT's FIFO cache and every policy JMT has no object
2528 * for -- HLRU, CLIMB, QLRU -- is refused by name rather than falling back
2529 * to LRU as the reference's `otherwise` does: a q-LRU cache silently
2530 * simulated as LRU returns a hit rate that is wrong by the admission
2531 * probability, with nothing in the output to show for it.
2532 */
2533 void save_cache_strategy(xml::Element& section, std::size_t ind) {
2534 const auto it = sn_.nodeparam.find(ind);
2535 if (it == sn_.nodeparam.end())
2536 throw InputError("SolverJMT: cache node '" + nname(ind) + "' carries no parameters");
2537 const qn::CacheParam<T>& cp = it->second;
2538 const std::size_t K = sn_.nclasses;
2539
2540 jmt_param_value(section, "java.lang.Integer", "maxItems",
2541 jmt_int(static_cast<double>(cp.nitems)));
2542 xml::Element& cap = jmt_param(section, "java.lang.Integer", "cacheCapacity", true);
2543 for (std::size_t l = 0; l < cp.itemcap.size(); ++l) {
2544 xml::Element& sp = cap.add_child("subParameter");
2545 sp.set_attr("classPath", "java.lang.Integer");
2546 sp.set_attr("name", "capacity");
2547 sp.add_text_child("value", jmt_int(static_cast<double>(cp.itemcap[l])));
2548 }
2549
2550 const std::size_t nlev = cp.itemcap.size();
2551 const bool lru = cp.replacestrat == lang::ReplacementStrategy::LRU;
2552 xml::Element& mat = jmt_param(section, "java.lang.Object", "matrix", true);
2553 for (std::size_t a = 1; a <= nlev; ++a) {
2554 xml::Element& row = mat.add_child("subParameter");
2555 row.set_attr("array", "true");
2556 row.set_attr("classPath", "java.lang.Float");
2557 row.set_attr("name", "row");
2558 for (std::size_t b = 1; b <= nlev; ++b) {
2559 const bool one = lru ? (a < nlev ? b == a + 1 : b == nlev) : a == b;
2560 xml::Element& cell = row.add_child("subParameter");
2561 cell.set_attr("classPath", "java.lang.Float");
2562 cell.set_attr("name", "cell");
2563 cell.add_text_child("value", one ? "1.0" : "0.0");
2564 }
2565 }
2566
2567 xml::Element& jc = jmt_param(section, "jmt.engine.QueueNet.JobClass", "jobClasses", true);
2568 for (std::size_t r = 1; r <= K; ++r) {
2569 const bool used = (r <= cp.hitclass.size() && cp.hitclass[r - 1] > 0) ||
2570 (r <= cp.missclass.size() && cp.missclass[r - 1] > 0);
2571 if (!used) continue;
2572 xml::Element& sp = jc.add_child("subParameter");
2573 sp.set_attr("classPath", "jmt.engine.QueueNet.JobClass");
2574 sp.set_attr("name", "jobClass");
2575 sp.add_text_child("value", cname(r));
2576 }
2577 const char* switch_names[2] = {"hitClasses", "missClasses"};
2578 const char* entry_names[2] = {"hitClass", "missClass"};
2579 for (int w = 0; w < 2; ++w) {
2580 const std::vector<std::size_t>& tab = w == 0 ? cp.hitclass : cp.missclass;
2581 xml::Element& p =
2582 jmt_param(section, "jmt.engine.QueueNet.JobClass", switch_names[w], true);
2583 for (std::size_t r = 1; r <= K && r <= tab.size(); ++r) {
2584 if (tab[r - 1] == 0) continue;
2585 xml::Element& sp = p.add_child("subParameter");
2586 sp.set_attr("classPath", "jmt.engine.QueueNet.JobClass");
2587 sp.set_attr("name", entry_names[w]);
2588 sp.add_text_child("value", cname(tab[r - 1]));
2589 }
2590 }
2591
2592 const char* policy = nullptr;
2593 switch (cp.replacestrat) {
2595 policy = "jmt.engine.NetStrategies.CacheStrategies.LRUCache";
2596 break;
2599 policy = "jmt.engine.NetStrategies.CacheStrategies.FIFOCache";
2600 break;
2602 policy = "jmt.engine.NetStrategies.CacheStrategies.RandomCache";
2603 break;
2604 default:
2605 throw UnsupportedError(
2606 "SolverJMT: cache '" + nname(ind) +
2607 "' uses a replacement policy JMT has no cache object for (HLRU, CLIMB and "
2608 "QLRU); use SolverLDES, which simulates them directly");
2609 }
2610 xml::Element& rp = section.add_child("parameter");
2611 rp.set_attr("classPath", policy);
2612 rp.set_attr("name", "replacePolicy");
2613
2614 /*
2615 * The popularity is PARAMETRIC in JMT: a Zipf exponent or a uniform
2616 * range, never a pmf. `CacheParam::preadkind` is what the reader kept
2617 * for exactly this, and a class whose pmf was supplied directly exports
2618 * as `null` -- the reference's own behaviour for anything that is not a
2619 * Zipf or a DiscreteSampler.
2620 */
2621 xml::Element& pop = jmt_param(
2622 section, "jmt.engine.random.discrete.DiscreteDistribution", "popularity", true);
2623 for (std::size_t r = 1; r <= K; ++r) {
2624 jmt_ref_class(pop, cname(r));
2625 const bool reads = r <= cp.pread.size() && !cp.pread[r - 1].empty();
2626 const typename qn::CacheParam<T>::Popularity kind =
2627 r <= cp.preadkind.size() ? cp.preadkind[r - 1]
2628 : typename qn::CacheParam<T>::Popularity();
2629 xml::Element& sp = pop.add_child("subParameter");
2630 if (reads && kind.type == lang::ProcessType::ZIPF) {
2631 sp.set_attr("classPath", "jmt.engine.random.discrete.Zipf");
2632 sp.set_attr("name", "popularity");
2633 jmt_scalar(sp, "java.lang.Double", "alpha", jmt_fmt(kind.s));
2634 jmt_scalar(sp, "java.lang.Integer", "numberOfElements",
2635 jmt_int(static_cast<double>(kind.n)));
2636 } else if (reads && kind.type == lang::ProcessType::DISCRETESAMPLER) {
2637 sp.set_attr("classPath", "jmt.engine.random.discrete.Uniform");
2638 sp.set_attr("name", "popularity");
2639 jmt_scalar(sp, "java.lang.Integer", "min", "1");
2640 jmt_scalar(sp, "java.lang.Integer", "max",
2641 jmt_int(static_cast<double>(kind.n == 0 ? cp.nitems : kind.n)));
2642 } else {
2643 sp.set_attr("classPath", "jmt.engine.random.discrete.DiscreteDistribution");
2644 sp.set_attr("name", "null");
2645 sp.add_text_child("value", "null");
2646 }
2647 }
2648 }
2649
2650 // -- impatience, retrials and switchover --------------------------------
2651
2652 /**
2653 * Port of `saveImpatience`: the per-class Balking or Reneging strategy.
2654 *
2655 * BALKING AND RENEGING ARE MUTUALLY EXCLUSIVE per class here, as in the
2656 * reference: JMT's Impatience array holds one strategy per class, and
2657 * balking is checked first. A class with neither gets a Reneging strategy
2658 * whose body is the literal `null`.
2659 */
2660 void save_impatience(xml::Element& section, std::size_t ind) {
2661 const std::size_t ist = sn_.nodes[ind - 1].station;
2662 xml::Element& p = jmt_param(
2663 section, "jmt.engine.NetStrategies.ImpatienceStrategies.Impatience", "Impatience", true);
2664 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2665 if (!keep_[r - 1]) continue;
2666 jmt_ref_class(p, cname(r));
2667 xml::Element& sp = p.add_child("subParameter");
2668 const qn::Station<T>* st = ist == 0 ? nullptr : &sn_.stations[ist - 1];
2669 const bool balks = st != nullptr && r <= st->balking.size() &&
2670 st->balking[r - 1].strategy ==
2672 if (balks) {
2673 sp.set_attr("classPath", "jmt.engine.NetStrategies.ImpatienceStrategies.Balking");
2674 sp.set_attr("name", "Balking");
2675 save_balking_strategy(sp, st->balking[r - 1].thresholds, st->nservers);
2676 continue;
2677 }
2678 sp.set_attr("classPath", "jmt.engine.NetStrategies.ImpatienceStrategies.Reneging");
2679 sp.set_attr("name", "Reneging");
2680 const bool renegs = st != nullptr && r <= st->impatience.size() &&
2681 st->impatience[r - 1] == lang::ImpatienceType::RENEGING &&
2682 r <= st->patience.size() && !st->patience[r - 1].disabled;
2683 if (!renegs) {
2684 sp.add_text_child("value", "null");
2685 continue;
2686 }
2687 const JmtDistView<T> v = jmt_dist_view(st->patience[r - 1]);
2688 if (v.type == lang::ProcessType::UNIFORM) {
2689 // The reference exports a patience Uniform as [0, 2*mean],
2690 // matching the MEAN only, where the service-time emitter
2691 // matches the mean and the SCV. Kept: a patience law is
2692 // declared by its mean in every model that has one.
2693 xml::Element& dn = sp.add_child("subParameter");
2694 dn.set_attr("classPath", "jmt.engine.random.Uniform");
2695 dn.set_attr("name", "Uniform");
2696 xml::Element& par = sp.add_child("subParameter");
2697 par.set_attr("classPath", "jmt.engine.random.UniformPar");
2698 par.set_attr("name", "distrPar");
2699 jmt_scalar(par, "java.lang.Double", "min", "0.0");
2700 jmt_double(par, "max", v.rate > 0.0 ? 2.0 / v.rate : 0.0);
2701 continue;
2702 }
2703 jmt_append_distribution(sp, v, "SolverJMT (patience)");
2704 }
2705 }
2706
2707 /**
2708 * Port of `saveBalkingStrategy`.
2709 *
2710 * THE THRESHOLDS ARE SHIFTED DOWN BY THE SERVER COUNT. LINE evaluates
2711 * balking against the TOTAL station population, JMT's Balking against the
2712 * number WAITING; in the regime where balking matters the servers are busy,
2713 * so the two differ by exactly S and JMT waiting w is LINE total w + S.
2714 * A gap between two intervals gets an explicit zero-probability breakpoint,
2715 * since JMT selects the LAST range with `from <= queueLength` and would
2716 * otherwise let a queue length outside every interval inherit its
2717 * neighbour's balking probability.
2718 */
2719 void save_balking_strategy(xml::Element& balking,
2720 const std::vector<typename qn::Station<T>::BalkingThreshold>& th,
2721 double nservers) {
2722 struct Bp {
2723 double from, prob;
2724 };
2725 std::vector<typename qn::Station<T>::BalkingThreshold> sorted = th;
2726 std::sort(sorted.begin(), sorted.end(),
2727 [](const typename qn::Station<T>::BalkingThreshold& a,
2728 const typename qn::Station<T>::BalkingThreshold& b) {
2729 return a.min_jobs < b.min_jobs;
2730 });
2731 const double S =
2732 (!std::isfinite(nservers) || nservers < 1.0) ? 1.0 : nservers;
2733 std::vector<Bp> bps;
2734 for (std::size_t i = 0; i < sorted.size(); ++i) {
2735 const double lo = sorted[i].min_jobs;
2736 // -1 is the wire's unbounded upper end; every other value is a
2737 // closed interval, which is why the gap test below is `hi + 1`.
2738 const double hi = sorted[i].max_jobs < 0.0
2739 ? std::numeric_limits<double>::infinity()
2740 : sorted[i].max_jobs;
2741 const double pr = d(sorted[i].probability);
2742 bps.push_back(Bp{std::max(0.0, lo - S), pr});
2743 if (!std::isfinite(hi)) continue;
2744 const double next_lo = i + 1 < sorted.size()
2745 ? sorted[i + 1].min_jobs
2746 : std::numeric_limits<double>::infinity();
2747 if (hi + 1.0 < next_lo) bps.push_back(Bp{std::max(0.0, hi + 1.0 - S), 0.0});
2748 }
2749 xml::Element& ld = balking.add_child("subParameter");
2750 ld.set_attr("classPath", "jmt.engine.NetStrategies.ServiceStrategies.LoadDependentStrategy");
2751 ld.set_attr("name", "LoadDependentStrategy");
2752 xml::Element& arr = jmt_param_sub(
2753 ld, "jmt.engine.NetStrategies.ServiceStrategies.LDParameter", "LDParameter");
2754 for (const Bp& b : bps) {
2755 xml::Element& rn = arr.add_child("subParameter");
2756 rn.set_attr("classPath", "jmt.engine.NetStrategies.ServiceStrategies.LDParameter");
2757 rn.set_attr("name", "LDParameter");
2758 jmt_scalar(rn, "java.lang.Integer", "from", jmt_int(b.from));
2759 // A DUMMY distribution: only the `function` string below is read
2760 // for balking, but the LDParameter shape requires the pair.
2761 xml::Element& dn = rn.add_child("subParameter");
2762 dn.set_attr("classPath", "jmt.engine.random.Exponential");
2763 dn.set_attr("name", "Exponential");
2764 xml::Element& par = rn.add_child("subParameter");
2765 par.set_attr("classPath", "jmt.engine.random.ExponentialPar");
2766 par.set_attr("name", "distrPar");
2767 jmt_scalar(par, "java.lang.Double", "lambda", "1.0");
2768 jmt_scalar(rn, "java.lang.String", "function", jmt_fmt(b.prob));
2769 }
2770 jmt_scalar(balking, "java.lang.Boolean", "priorityActivated", "false");
2771 }
2772
2773 /**
2774 * Port of `saveRetrialDistributions`: the orbit delay of each class.
2775 *
2776 * A class with no orbit still gets an entry -- an Exp(1) placeholder -- so
2777 * that JMT's per-class array stays aligned; omitting it would shift every
2778 * later class's retrial delay onto the wrong class.
2779 */
2780 void save_retrial_distributions(xml::Element& section, std::size_t ind) {
2781 const std::size_t ist = sn_.nodes[ind - 1].station;
2782 const auto it = ist == 0 ? sn_.retrialparam.end() : sn_.retrialparam.find(ist);
2783 xml::Element& p = jmt_param(section, "jmt.engine.NetStrategies.ServiceStrategy",
2784 "retrialDistributions", true);
2785 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2786 if (!keep_[r - 1]) continue;
2787 jmt_ref_class(p, cname(r));
2788 xml::Element& sts = p.add_child("subParameter");
2789 sts.set_attr("classPath",
2790 "jmt.engine.NetStrategies.ServiceStrategies.ServiceTimeStrategy");
2791 sts.set_attr("name", "ServiceTimeStrategy");
2792 const bool has = it != sn_.retrialparam.end() &&
2793 r <= it->second.retrial_proc.size() &&
2794 !it->second.retrial_proc[r - 1].disabled;
2795 if (!has) {
2796 xml::Element& dn = sts.add_child("subParameter");
2797 dn.set_attr("classPath", "jmt.engine.random.Exponential");
2798 dn.set_attr("name", "Exponential");
2799 xml::Element& par = sts.add_child("subParameter");
2800 par.set_attr("classPath", "jmt.engine.random.ExponentialPar");
2801 par.set_attr("name", "distrPar");
2802 jmt_scalar(par, "java.lang.Double", "lambda", "1.000000000000");
2803 continue;
2804 }
2805 jmt_append_distribution(sts, jmt_dist_view(it->second.retrial_proc[r - 1]),
2806 "SolverJMT (retrial delay)");
2807 }
2808 }
2809
2810 /**
2811 * Port of the polling branch of `saveSwitchoverStrategy`.
2812 *
2813 * ONLY the polling branch is ported: `writeJSIM` reaches this handler from
2814 * the PollingServer section alone, and the reference's second branch --
2815 * a (K x K) per-class-pair switchover for an ordinary Server -- is
2816 * unreachable there. A switchover declared on a non-polling queue is
2817 * refused in `warn_switchover_on_non_polling`, which is where the reference
2818 * warns and drops it.
2819 */
2820 void save_switchover_strategy(xml::Element& section, std::size_t ind) {
2821 const std::size_t ist = sn_.nodes[ind - 1].station;
2822 const typename qn::NetworkStruct<T>::PollingParam pp = sn_.effective_polling(ist);
2823 xml::Element& p = jmt_param(section, "jmt.engine.NetStrategies.ServiceStrategy",
2824 "SwitchoverStrategy", true);
2825 for (std::size_t r = 1; r <= sn_.nclasses; ++r) {
2826 jmt_ref_class(p, cname(r));
2827 xml::Element& sts = p.add_child("subParameter");
2828 sts.set_attr("classPath",
2829 "jmt.engine.NetStrategies.ServiceStrategies.ServiceTimeStrategy");
2830 sts.set_attr("name", "ServiceTimeStrategy");
2831 const lang::Distrib<T>& so =
2832 r <= pp.switchover.size() ? pp.switchover[r - 1] : empty_dist_;
2833 const JmtDistView<T> v = jmt_dist_view(so);
2834 if (so.disabled || v.type == lang::ProcessType::IMMEDIATE) {
2835 // No switchover declared for this buffer is a zero switchover,
2836 // not an omission: JMT reads the array positionally. An
2837 // Immediate switchover is the same zero, declared: the
2838 // reference maps both to ZeroServiceTimeStrategy, which carries
2839 // no distribution, and there is no JMT distribution to fall
2840 // back on -- an unhandled Immediate aborted the whole solve.
2841 sts.set_attr("classPath",
2842 "jmt.engine.NetStrategies.ServiceStrategies.ZeroServiceTimeStrategy");
2843 sts.set_attr("name", "ZeroServiceTimeStrategy");
2844 continue;
2845 }
2846 jmt_append_distribution(sts, v, "SolverJMT (switchover)");
2847 }
2848 }
2849
2850 std::map<std::size_t, double> empty_weights_;
2851 lang::Distrib<T> empty_dist_;
2852};
2853
2854/** Port of `@@JMTIO/writeJSIM.m`: serialize `sn` as a JMT `.jsimg` document. */
2855template <class T>
2857 JmtWriter<T> w(sn, opt);
2858 return w.write_jsim();
2859}
2860
2861/**
2862 * `JmtWriter::buffer_capacity_refusal` without a document: the writer's own
2863 * binding-buffer verdict, for the gate in `jmt::jmt_method_refusal`.
2864 */
2865template <class T>
2866std::string jmt_buffer_capacity_refusal(const qn::NetworkStruct<T>& sn, bool jmva_engine) {
2868 return w.buffer_capacity_refusal(jmva_engine);
2869}
2870
2871} // namespace io
2872} // namespace line
2873
2874#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:426
@ IMMEDIATE
fires with zero delay, resolved by weight and priority
Definition lang_types.h:365
JoinStrategy
Join rules, with the values of MATLAB JoinStrategy.
Definition lang_types.h:463
RoutingStrategy
Routing strategies, with the values of MATLAB RoutingStrategy.
Definition lang_types.h:391
HeteroSchedPolicy
How a heterogeneous station picks among its server types, MATLAB HeteroSchedPolicy.
Definition lang_types.h:453
PollingType
Polling service disciplines, with the values of MATLAB PollingType.
Definition lang_types.h:372
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:326
@ LRU
least recently used
Definition lang_types.h:384
@ FIFO
first in, first out
Definition lang_types.h:382
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
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
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 ....