LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
jmt_dist.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_DIST_H
6#define LINE_IO_JMT_DIST_H
7
8/**
9 * @file
10 * @ingroup line_io
11 * The distribution subtree of a JMT `.jsimg` file, and the scalar formatting
12 * every JMT writer shares.
13 *
14 * ONE EMITTER, SEVEN CALLERS. The reference spells this block out again in
15 * `saveServiceStrategy`, `saveArrivalStrategy`, `saveDelayOffStrategy`,
16 * `saveSwitchoverStrategy`, `saveTimingStrategies`, `saveRetrialDistributions`
17 * and `saveImpatience` -- seven near-copies, which is why those files are the
18 * seven largest in `@@JMTIO`. They differ only in the WRAPPER element that
19 * carries the pair, never in the pair itself, so this port factors the pair out
20 * and leaves each caller its own wrapper. A divergence between two copies of
21 * the same emitter is a wrong service time in one place and not the other, and
22 * it cannot be seen by reading either copy.
23 *
24 * WHAT JMT IS GIVEN IS THE FITTED PROCESS, NOT THE DECLARED ONE, for the
25 * families whose parameters the reference re-derives from the first two
26 * moments: Gamma, Pareto, Weibull, Lognormal and Uniform are all reconstructed
27 * from `sn.rates` and `sn.scv` rather than from the constructor arguments. That
28 * is the reference's behaviour and is kept, because the moments are what the
29 * struct carries after a refresh; a model whose Weibull was fitted by moments
30 * therefore round-trips, and one whose shape was set directly is exported as
31 * the Justus-approximation Weibull with the same mean and SCV.
32 */
33
34#include <cmath>
35#include <cstdio>
36#include <string>
37#include <vector>
38
41#include "line/num/number.h"
42#include "line/util/error.h"
43#include "line/util/matrix.h"
44#include "line/util/xml.h"
45
46namespace line {
47namespace io {
48
49using lang::Distrib;
51
52/** `sprintf('%.12f', x)`, the numeric format of every JMT `<value>` body. */
53inline std::string jmt_fmt(double x) {
54 char buf[64];
55 std::snprintf(buf, sizeof(buf), "%.12f", x);
56 return std::string(buf);
57}
58
59/** `int2str(x)`: round to nearest, print as a decimal integer. */
60inline std::string jmt_int(double x) {
61 char buf[32];
62 std::snprintf(buf, sizeof(buf), "%.0f", x);
63 return std::string(buf);
64}
65
66/** `num2str(x)`: MATLAB's default five-significant-digit form. */
67inline std::string jmt_num(double x) {
68 char buf[32];
69 std::snprintf(buf, sizeof(buf), "%.5g", x);
70 return std::string(buf);
71}
72
73/** `num2str(x, 2)`: the two-significant-digit form used for alpha/precision. */
74inline std::string jmt_sig2(double x) {
75 char buf[32];
76 std::snprintf(buf, sizeof(buf), "%.2g", x);
77 return std::string(buf);
78}
79
80/** `<subParameter classPath="java.lang.Double" name="NAME"><value>V</value>` */
81inline xml::Element& jmt_scalar(xml::Element& parent, const char* class_path, const char* name,
82 const std::string& value) {
83 xml::Element& p = parent.add_child("subParameter");
84 p.set_attr("classPath", class_path);
85 p.set_attr("name", name);
86 p.add_text_child("value", value);
87 return p;
88}
89
90/** A `java.lang.Double` scalar subparameter. */
91inline xml::Element& jmt_double(xml::Element& parent, const char* name, double v) {
92 return jmt_scalar(parent, "java.lang.Double", name, jmt_fmt(v));
93}
94
95/**
96 * The (D0, D1) pair a JMT MAP or phase-type parameter block is built from,
97 * plus the moments the analytic families are reconstructed from.
98 *
99 * Assembled by `jmt_dist_view` so that a caller holding only a `Distrib` and a
100 * caller holding a refreshed `(rate, scv)` row of `sn` reach the same emitter.
101 */
102template <class T>
104 ProcessType type = ProcessType::DISABLED;
105 std::size_t phases = 1;
106 double rate = 0.0; ///< `sn.rates(i,r)`
107 double scv = 1.0; ///< `sn.scv(i,r)`
108 std::vector<double> pie;
110 std::string trace_file; ///< Replayer / Trace file name, empty otherwise
111};
112
113/**
114 * Lower a `Distrib` to the view above.
115 *
116 * The rate is `1/mean` and NOT `d.params[0]`: `sn.rates` is what the reference
117 * reads, and for a fitted family the two differ. A DISABLED or IMMEDIATE
118 * distribution is reported by type alone and carries no pair, exactly as
119 * `sn.proc` leaves it empty there.
120 */
121template <class T>
124 v.type = d.type;
125 if (d.disabled || d.type == ProcessType::DISABLED) {
126 v.type = ProcessType::DISABLED;
127 return v;
128 }
129 if (d.type == ProcessType::IMMEDIATE) return v;
130 const double mean = num_traits<T>::to_double(d.mean);
131 v.rate = mean > 0.0 ? 1.0 / mean : 0.0;
133 v.phases = d.phases();
134 if (d.type == ProcessType::REPLAYER) {
135 // The reference reads `sn.nodeparam{ind}{r}.fileName`; this port carries
136 // the same path on the distribution (`Distrib::trace_file`). A Replayer
137 // built from samples in memory has none, and is refused by the emitter
138 // rather than exported as a Replayer reading nothing.
140 return v;
141 }
142 // Only the Markovian families reach JMT as a matrix pair. For the rest the
143 // pair is the refresh's Erlang approximation, which the analytic branches
144 // below never look at, so building it would be wasted work and, under exact
145 // arithmetic, would refuse on a Gamma that the analytic branch handles.
146 switch (d.type) {
147 case ProcessType::PH:
148 case ProcessType::APH:
149 case ProcessType::COXIAN:
150 case ProcessType::COX2:
151 case ProcessType::ERLANG:
152 case ProcessType::HYPEREXP:
153 case ProcessType::MAP:
154 case ProcessType::MMPP2: {
155 const mam::Map<T> m = lang::dist_to_map(d);
156 v.D0 = Matrix<double>(m.D0.rows(), m.D0.cols(), 0.0);
157 v.D1 = Matrix<double>(m.D1.rows(), m.D1.cols(), 0.0);
158 for (std::size_t a = 0; a < m.D0.rows(); ++a)
159 for (std::size_t b = 0; b < m.D0.cols(); ++b) {
160 v.D0(a, b) = num_traits<T>::to_double(m.D0(a, b));
161 v.D1(a, b) = num_traits<T>::to_double(m.D1(a, b));
162 }
163 const std::vector<T> pie = lang::dist_pie(d);
164 v.pie.resize(pie.size());
165 for (std::size_t k = 0; k < pie.size(); ++k) v.pie[k] = num_traits<T>::to_double(pie[k]);
166 v.phases = m.D0.rows();
167 break;
168 }
169 default:
170 break;
171 }
172 return v;
173}
174
175/**
176 * True when the reference routes the process through JMT's `PhaseTypeDistr`
177 * rather than through a named analytic law.
178 *
179 * COX2 IS ADDED TO THE REFERENCE'S LIST. `saveServiceStrategy` sends PH, APH,
180 * COXIAN and a HyperExp of more than two phases down this branch, and lets
181 * COX2 fall into the analytic switch -- which has no COX2 case, so MATLAB
182 * errors on an undefined `javaClass`. A two-phase Coxian has an exact
183 * phase-type representation, and this port's `ProcessType` keeps COX2 distinct
184 * from COXIAN, so it is exported through the same exact branch rather than
185 * through an error.
186 */
187inline bool jmt_is_phase_type(ProcessType t, std::size_t phases) {
188 return t == ProcessType::PH || t == ProcessType::APH || t == ProcessType::COXIAN ||
189 t == ProcessType::COX2 || (phases > 2 && t == ProcessType::HYPEREXP);
190}
191
192/** `<subParameter array="true" classPath="java.lang.Object" name="NAME">` */
193inline xml::Element& jmt_object_array(xml::Element& parent, const char* name) {
194 xml::Element& e = parent.add_child("subParameter");
195 e.set_attr("array", "true");
196 e.set_attr("classPath", "java.lang.Object");
197 e.set_attr("name", name);
198 return e;
199}
200
201/** One `vector` of `entry` doubles, the JMT encoding of a matrix row. */
202inline void jmt_append_row(xml::Element& parent, const Matrix<double>& M, std::size_t row,
203 std::size_t n, bool negate_diagonal) {
204 xml::Element& vec = jmt_object_array(parent, "vector");
205 for (std::size_t j = 0; j < n; ++j) {
206 const double raw = M(row, j);
207 const double v = negate_diagonal ? (row == j ? -std::fabs(raw) : std::fabs(raw)) : raw;
208 jmt_scalar(vec, "java.lang.Double", "entry", jmt_fmt(v));
209 }
210}
211
212/**
213 * Append the `distribution` and `distrPar` pair for one process.
214 *
215 * @param wrapper the element the pair is appended to -- the caller's
216 * ServiceTimeStrategy, or a Timing / Impatience wrapper
217 * @param v the lowered process
218 * @param who the caller, for the refusal message of an unexportable family
219 *
220 * A family JMT has no law for is REFUSED BY NAME. Falling through to the
221 * nearest available law would export a model that simulates cleanly and answers
222 * a different question, and the featset gate is what is supposed to catch this:
223 * a refusal here means the featset and this emitter have drifted apart.
224 */
225template <class T>
226void jmt_append_distribution(xml::Element& wrapper, const JmtDistView<T>& v, const char* who) {
227 if (jmt_is_phase_type(v.type, v.phases)) {
228 xml::Element& dist = wrapper.add_child("subParameter");
229 dist.set_attr("classPath", "jmt.engine.random.PhaseTypeDistr");
230 dist.set_attr("name", "Phase-Type");
231 xml::Element& par = wrapper.add_child("subParameter");
232 par.set_attr("classPath", "jmt.engine.random.PhaseTypePar");
233 par.set_attr("name", "distrPar");
234
235 xml::Element& alpha = jmt_object_array(par, "alpha");
236 xml::Element& alphavec = jmt_object_array(alpha, "vector");
237 for (std::size_t k = 0; k < v.phases; ++k) {
238 const double a = k < v.pie.size() ? std::fabs(v.pie[k]) : 0.0;
239 jmt_scalar(alphavec, "java.lang.Double", "entry", jmt_fmt(a));
240 }
241 xml::Element& Tblk = jmt_object_array(par, "T");
242 for (std::size_t k = 0; k < v.phases; ++k) jmt_append_row(Tblk, v.D0, k, v.phases, true);
243 return;
244 }
245
246 if (v.type == ProcessType::MAP || v.type == ProcessType::MMPP2) {
247 // MMPP2 reaches BOTH this branch and the analytic MMPP2Distr branch in
248 // the reference: the phase-type test above excludes it, and the MAP
249 // test here catches it first, so the `MMPP2Par` case of the analytic
250 // switch is unreachable. Kept unreachable here too rather than
251 // "corrected": the MAP form is exact and the two would otherwise
252 // disagree on which one a Burst process is exported as.
253 xml::Element& dist = wrapper.add_child("subParameter");
254 dist.set_attr("classPath", "jmt.engine.random.MAPDistr");
255 dist.set_attr("name", "Burst (MAP)");
256 xml::Element& par = wrapper.add_child("subParameter");
257 par.set_attr("classPath", "jmt.engine.random.MAPPar");
258 par.set_attr("name", "distrPar");
259 xml::Element& d0 = jmt_object_array(par, "D0");
260 for (std::size_t k = 0; k < v.phases; ++k) jmt_append_row(d0, v.D0, k, v.phases, false);
261 xml::Element& d1 = jmt_object_array(par, "D1");
262 for (std::size_t k = 0; k < v.phases; ++k) jmt_append_row(d1, v.D1, k, v.phases, false);
263 return;
264 }
265
266 const char* java_class = nullptr;
267 const char* java_par_class = nullptr;
268 const char* display = process_to_text(v.type);
269 switch (v.type) {
270 case ProcessType::DET:
271 java_class = "jmt.engine.random.DeterministicDistr";
272 java_par_class = "jmt.engine.random.DeterministicDistrPar";
273 break;
274 case ProcessType::ERLANG:
275 java_class = "jmt.engine.random.Erlang";
276 java_par_class = "jmt.engine.random.ErlangPar";
277 break;
278 case ProcessType::EXP:
279 java_class = "jmt.engine.random.Exponential";
280 java_par_class = "jmt.engine.random.ExponentialPar";
281 display = "Exponential";
282 break;
283 case ProcessType::GAMMA:
284 java_class = "jmt.engine.random.GammaDistr";
285 java_par_class = "jmt.engine.random.GammaDistrPar";
286 break;
287 case ProcessType::HYPEREXP:
288 java_class = "jmt.engine.random.HyperExp";
289 java_par_class = "jmt.engine.random.HyperExpPar";
290 display = "Hyperexponential";
291 break;
292 case ProcessType::PARETO:
293 java_class = "jmt.engine.random.Pareto";
294 java_par_class = "jmt.engine.random.ParetoPar";
295 break;
296 case ProcessType::WEIBULL:
297 java_class = "jmt.engine.random.Weibull";
298 java_par_class = "jmt.engine.random.WeibullPar";
299 break;
300 case ProcessType::LOGNORMAL:
301 java_class = "jmt.engine.random.Lognormal";
302 java_par_class = "jmt.engine.random.LognormalPar";
303 break;
304 case ProcessType::UNIFORM:
305 java_class = "jmt.engine.random.Uniform";
306 java_par_class = "jmt.engine.random.UniformPar";
307 break;
308 case ProcessType::REPLAYER:
309 java_class = "jmt.engine.random.Replayer";
310 java_par_class = "jmt.engine.random.ReplayerPar";
311 display = "Replayer";
312 break;
313 default:
314 throw UnsupportedError(std::string(who) + ": JMT has no distribution for '" +
315 process_to_text(v.type) + "'");
316 }
317
318 xml::Element& dist = wrapper.add_child("subParameter");
319 dist.set_attr("classPath", java_class);
320 dist.set_attr("name", display);
321 xml::Element& par = wrapper.add_child("subParameter");
322 par.set_attr("classPath", java_par_class);
323 par.set_attr("name", "distrPar");
324
325 switch (v.type) {
326 case ProcessType::DET:
327 jmt_double(par, "t", v.rate > 0.0 ? 1.0 / v.rate : 0.0);
328 break;
329 case ProcessType::EXP:
330 jmt_double(par, "lambda", v.rate);
331 break;
332 case ProcessType::HYPEREXP:
333 jmt_double(par, "p", v.pie.empty() ? 0.0 : v.pie[0]);
334 jmt_double(par, "lambda1", v.D0.rows() > 0 ? -v.D0(0, 0) : 0.0);
335 jmt_double(par, "lambda2", v.D0.rows() > 1 ? -v.D0(1, 1) : 0.0);
336 break;
337 case ProcessType::ERLANG:
338 jmt_double(par, "alpha", v.rate * static_cast<double>(v.phases));
339 jmt_scalar(par, "java.lang.Long", "r", jmt_int(static_cast<double>(v.phases)));
340 break;
341 case ProcessType::GAMMA:
342 jmt_double(par, "alpha", 1.0 / v.scv);
343 jmt_double(par, "beta", v.scv / v.rate);
344 break;
345 case ProcessType::PARETO: {
346 const double shape = std::sqrt(1.0 + 1.0 / v.scv) + 1.0;
347 const double scale = (1.0 / v.rate) * (shape - 1.0) / shape;
348 jmt_double(par, "alpha", shape);
349 jmt_double(par, "k", scale);
350 break;
351 }
352 case ProcessType::WEIBULL: {
353 // Justus (1976) approximation of the shape from the SCV, as the
354 // reference uses; `alpha` is JMT's scale and `r` its shape.
355 const double c = std::sqrt(v.scv);
356 const double rval = std::pow(c, -1.086);
357 const double alpha = (1.0 / v.rate) / std::tgamma(1.0 + 1.0 / rval);
358 jmt_double(par, "alpha", alpha);
359 jmt_double(par, "r", rval);
360 break;
361 }
362 case ProcessType::LOGNORMAL: {
363 const double c = std::sqrt(v.scv);
364 const double mu = std::log((1.0 / v.rate) / std::sqrt(c * c + 1.0));
365 const double sigma = std::sqrt(std::log(c * c + 1.0));
366 jmt_double(par, "mu", mu);
367 jmt_double(par, "sigma", sigma);
368 break;
369 }
370 case ProcessType::UNIFORM: {
371 const double maxVal =
372 (std::sqrt(12.0 * v.scv / (v.rate * v.rate)) + 2.0 / v.rate) / 2.0;
373 const double minVal = 2.0 / v.rate - maxVal;
374 jmt_double(par, "min", minVal);
375 jmt_double(par, "max", maxVal);
376 break;
377 }
378 case ProcessType::REPLAYER:
379 if (v.trace_file.empty())
380 throw UnsupportedError(std::string(who) +
381 ": a Replayer must name the trace file JMT is to read; "
382 "this port carries the samples on the distribution and the "
383 "caller has not staged them beside the model");
384 jmt_scalar(par, "java.lang.String", "fileName", v.trace_file);
385 break;
386 default:
387 break;
388 }
389}
390
391} // namespace io
392} // namespace line
393
394#endif // LINE_IO_JMT_DIST_H
std::size_t rows() const
Definition matrix.h:89
UnsupportedError(const std::string &what)
Definition error.h:51
What refreshProcessRepresentations and refreshLST compute FROM a distribution: the (D0,...
The exception types the port throws.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
Dense matrix and non-owning view.
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
std::string jmt_fmt(double x)
sprintf('%.12f', x), the numeric format of every JMT <value> body.
Definition jmt_dist.h:53
xml::Element & jmt_object_array(xml::Element &parent, const char *name)
<subParameter array="true" classPath="java.lang.Object" name="NAME">
Definition jmt_dist.h:193
bool jmt_is_phase_type(ProcessType t, std::size_t phases)
True when the reference routes the process through JMT's PhaseTypeDistr rather than through a named a...
Definition jmt_dist.h:187
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
void jmt_append_row(xml::Element &parent, const Matrix< double > &M, std::size_t row, std::size_t n, bool negate_diagonal)
One vector of entry doubles, the JMT encoding of a matrix row.
Definition jmt_dist.h:202
xml::Element & jmt_double(xml::Element &parent, const char *name, double v)
A java.lang.Double scalar subparameter.
Definition jmt_dist.h:91
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
mam::Map< T > dist_to_map(const Distrib< T > &d)
std::vector< T > dist_pie(const Distrib< T > &d)
sn.pie: the phase distribution seen by an arriving job.
ProcessType
Distribution kinds, with the values of MATLAB ProcessType.
Definition lang_types.h:483
Number-type abstraction for the templated API port.
The (D0, D1) pair a JMT MAP or phase-type parameter block is built from, plus the moments the analyti...
Definition jmt_dist.h:103
std::string trace_file
Replayer / Trace file name, empty otherwise.
Definition jmt_dist.h:110
double rate
sn.rates(i,r)
Definition jmt_dist.h:106
std::vector< double > pie
Definition jmt_dist.h:108
double scv
sn.scv(i,r)
Definition jmt_dist.h:107
Matrix< double > D0
Definition jmt_dist.h:109
std::size_t phases
Definition jmt_dist.h:105
Matrix< double > D1
Definition jmt_dist.h:109
std::string trace_file
The trace FILE a Replayer was read from, when there was one.
Definition lang_types.h:748
std::size_t phases() const
The order of the representation, MATLAB's sn.phases.
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
Matrix< T > D1
Definition map_moment.h:55
Matrix< T > D0
Definition map_moment.h:54
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 ....