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