LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ldes_cli.cpp
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5
6/**
7 * @file
8 * `ldes`: the native LDES engine behind the interface of `jline.cli.LdesCLI`.
9 *
10 * WHAT THIS BINARY IS FOR. `common/ldes` has been a GraalVM image of the JAVA
11 * engine; this program is its replacement, built from
12 * `cpp/include/line/solvers/ldes/`. Every client of that binary -- MATLAB's
13 * `@SolverLDES/solveCli.m`, native Python's `wrappers/solver_ldes`, and the C++
14 * client `wrappers/ldes/solver_ldes.h` -- speaks to it through exactly two
15 * artefacts: the ARGUMENT VECTOR and the `ldes-result` DOCUMENT. Neither may
16 * drift. A client is not recompiled when the engine changes hands, so an
17 * argument this program rejects and the Java one accepted, or a key it spells
18 * differently, is a silent breakage in three codebases at once.
19 *
20 * THE INTERFACE IS THEREFORE COPIED, not designed:
21 *
22 * ldes solve <model.json> -o <result.json> [flags]
23 *
24 * with the flag set of `LdesCLI.handleSolveCommand` and the document shape of
25 * `LDESResultIO.write`. Flags this engine cannot honour are REFUSED BY NAME
26 * rather than ignored, which is the one deliberate difference from the AOT
27 * image it replaces: that image ignored flags it predated, and an ignored
28 * `--slotlength` silently ran a continuous-time simulation under a slotted
29 * model's name. A refusal is visible; a silent default is not.
30 *
31 * A LAYEREDNETWORK document (`modelType: LayeredNetwork`) is dispatched to the
32 * native LN engine, `ldes_ln_engine_solve`, and answered with the layered
33 * document of `LDESResultIO.saveLN` (see `ln_result_json`). Flags that engine
34 * has no counterpart for (transient, histograms, busy periods, warm starts,
35 * slotted time, replications) are refused by name.
36 *
37 * WHAT IS NOT HERE. `--rest`/server mode belongs to `line_cli` and is refused
38 * with the reason rather than accepted and dropped.
39 */
40
41#include <chrono>
42#include <cmath>
43#include <cstdio>
44#include <ctime>
45#include <cstdlib>
46#include <fstream>
47#include <iostream>
48#include <sstream>
49#include <string>
50#include <vector>
51
57#include "line/util/error.h"
58
59namespace {
60
63
64void print_help() {
65 std::cout
66 << "LDES: discrete-event simulation engine for LINE models.\n\n"
67 << "Usage: ldes solve <model.json> [-o <result.json>] [options]\n\n"
68 << "Options:\n"
69 << " -s, --samples N service completions to simulate (default 200000)\n"
70 << " -e, --maxevents N event budget; overrides --samples\n"
71 << " --maxtime SECONDS wall-clock budget\n"
72 << " --seed N run seed (default 23000; -1 draws one)\n"
73 << " --method NAME engine method (default 'default')\n"
74 << " --cnvgon stop on the convergence check\n"
75 << " --cnvgtol X relative tolerance of that check\n"
76 << " --cnvgbatch N batches before the first check\n"
77 << " --cnvgchk N events between checks\n"
78 << " --tranfilter NAME mser5 | fixed | none\n"
79 << " --mserbatch N MSER batch size\n"
80 << " --warmupfrac X warmup fraction, for --tranfilter fixed\n"
81 << " --tranobs N transient observation points over --timespan\n"
82 << " --cimethod NAME obm | bm | spectral | none\n"
83 << " --obmoverlap X overlap of the batch means\n"
84 << " --ciminbatch N batches below which no CI is reported\n"
85 << " --ciminobs N observations below which no CI is reported\n"
86 << " --spectrallowfreqfrac X low-frequency fraction of the spectral CI\n"
87 << " --slotted run on the slot lattice\n"
88 << " --slotlength X slot length (implies --slotted)\n"
89 << " --replications N independent replications\n"
90 << " --numthreads N threads across replications\n"
91 << " --timespan T0,T1 transient run over [T0,T1]\n"
92 << " --busyperiod K busy-period orders 1..K\n"
93 << " --busyperiod-subnet i,j,... an extra busy-period target\n"
94 << " --initsol V initial state, station-major, comma separated\n"
95 << " --initjobs J job-level initial state, one row per job, rows ';'\n"
96 << " separated: station,class,remaining,elapsed,qage,sage,order[,id]\n"
97 << " --trackjobs export the final job-level state and tagged departures\n"
98 << " --export-histogram fill the state histogram block\n"
99 << " --trajectory fill the state trajectory block\n"
100 << " --respt-samples record every per-visit response time\n"
101 << " -h, --help this text\n";
102}
103
104[[noreturn]] void fail(const std::string& msg) {
105 std::cerr << "Error: " << msg << "\n";
106 std::exit(1);
107}
108
109/** A flag the reference accepts and this engine cannot honour: say so. */
110[[noreturn]] void refuse(const std::string& flag, const std::string& why) {
111 std::cerr << "Error: " << flag << " is accepted by the Java engine but not by this one: "
112 << why << ".\nRun the model through common/ldes.jar for it, rather than "
113 "taking a silently different simulation.\n";
114 std::exit(1);
115}
116
117std::string need_value(int argc, char** argv, int& i, const std::string& flag) {
118 if (i + 1 >= argc) fail(flag + " requires a value.");
119 return std::string(argv[++i]);
120}
121
122std::vector<double> parse_doubles(const std::string& csv) {
123 std::vector<double> out;
124 std::stringstream ss(csv);
125 std::string tok;
126 while (std::getline(ss, tok, ',')) {
127 if (!tok.empty()) out.push_back(std::atof(tok.c_str()));
128 }
129 return out;
130}
131
132std::vector<std::size_t> parse_indices(const std::string& csv) {
133 std::vector<std::size_t> out;
134 std::stringstream ss(csv);
135 std::string tok;
136 while (std::getline(ss, tok, ',')) {
137 if (!tok.empty()) out.push_back(static_cast<std::size_t>(std::atol(tok.c_str())));
138 }
139 return out;
140}
141
142/** A JSON number the way the reference writes it: full round-trip precision. */
143std::string num(double v) {
144 if (std::isnan(v)) return "null";
145 if (std::isinf(v)) return v > 0 ? "1e999" : "-1e999";
146 char buf[40];
147 std::snprintf(buf, sizeof(buf), "%.17g", v);
148 return std::string(buf);
149}
150
151/** A (rows x cols) matrix as the nested array `LDESResultIO` emits. */
152std::string mat_json(const line::Matrix<double>& m, std::size_t rows, std::size_t cols) {
153 std::string s = "[";
154 for (std::size_t i = 0; i < rows; ++i) {
155 if (i) s += ", ";
156 s += "[";
157 for (std::size_t j = 0; j < cols; ++j) {
158 if (j) s += ", ";
159 const bool inside = (i < m.rows() && j < m.cols());
160 s += num(inside ? m(i, j) : 0.0);
161 }
162 s += "]";
163 }
164 s += "]";
165 return s;
166}
167
168/** The per-visit response times as the nested `[station][class][sample]` array. */
169std::string respt_samples_json(const LdesResult& r) {
170 if (r.respTimeSamples.empty()) return std::string();
171 std::string s = "[";
172 for (std::size_t i = 0; i < r.respTimeSamples.size(); ++i) {
173 if (i) s += ", ";
174 s += "[";
175 for (std::size_t k = 0; k < r.respTimeSamples[i].size(); ++k) {
176 if (k) s += ", ";
177 s += "[";
178 for (std::size_t j = 0; j < r.respTimeSamples[i][k].size(); ++j) {
179 if (j) s += ", ";
180 s += num(r.respTimeSamples[i][k][j]);
181 }
182 s += "]";
183 }
184 s += "]";
185 }
186 s += "]";
187 return s;
188}
189
190std::string str_json(const std::string& s) {
191 std::string out = "\"";
192 for (char c : s) {
193 if (c == '"' || c == '\\') {
194 out += '\\';
195 out += c;
196 } else if (c == '\n') {
197 out += "\\n";
198 } else {
199 out += c;
200 }
201 }
202 out += "\"";
203 return out;
204}
205
206/**
207 * The `ldes-result` document, key for key with `LDESResultIO.write`.
208 *
209 * The order of the keys is the reference's, and the blocks a run did not fill
210 * are still emitted with their zero matrices: every client indexes this
211 * document by key and by station row, and a missing block reads as a parse
212 * failure rather than as an absent measurement.
213 */
214std::string result_json(const LdesResult& r, const LdesOptions& o, double runtime,
215 std::size_t events) {
216 const std::size_t S = r.nstations, C = r.nclasses;
217 std::string s;
218 s += "{\n";
219 s += " \"format\": \"ldes-result\",\n";
220 s += " \"version\": \"1.0\",\n";
221 s += " \"solver\": \"SolverLDES\",\n";
222 s += " \"method\": " + str_json(o.method) + ",\n";
223 s += " \"runtime\": " + num(runtime) + ",\n";
224 s += " \"converged\": false,\n";
225 s += " \"stoppingReason\": \"max_events\",\n";
226 s += " \"convergenceBatches\": 0,\n";
227 s += " \"totalSimulatedEvents\": " + std::to_string(events) + ",\n";
228
229 s += " \"dimensions\": {\"nstations\": " + std::to_string(S) +
230 ", \"nclasses\": " + std::to_string(C) + ", \"nchains\": " + std::to_string(r.nchains) +
231 ", \"stationNames\": [";
232 for (std::size_t i = 0; i < r.station_names.size(); ++i) {
233 if (i) s += ", ";
234 s += str_json(r.station_names[i]);
235 }
236 s += "], \"classNames\": [";
237 for (std::size_t i = 0; i < r.class_names.size(); ++i) {
238 if (i) s += ", ";
239 s += str_json(r.class_names[i]);
240 }
241 s += "]},\n";
242
243 s += " \"metrics\": {";
244 s += "\"QN\": " + mat_json(r.QN, S, C);
245 s += ", \"UN\": " + mat_json(r.UN, S, C);
246 s += ", \"RN\": " + mat_json(r.RN, S, C);
247 s += ", \"TN\": " + mat_json(r.TN, S, C);
248 s += ", \"AN\": " + mat_json(r.AN, S, C);
249 s += ", \"WN\": " + mat_json(r.WN, S, C);
250 s += ", \"CN\": " + mat_json(r.CN, 1, C);
251 s += ", \"XN\": " + mat_json(r.XN, 1, C);
252 // The sibling-drop rate at a Join, where LossRate = ArvR - Tput does NOT
253 // hold: ArvR counts the SIBLINGS offered and Tput the PARENT jobs released.
254 // `LDESResultIO.write` puts it inside `metrics`, so it goes here and not in
255 // an optional block of its own.
256 if (!r.DropRateJoin.empty()) s += ", \"DropRateJoin\": " + mat_json(r.DropRateJoin, S, C);
257 s += "},\n";
258
259 // THE OPTIONAL BLOCKS, each written ONLY when the run measured it, exactly
260 // as `LDESResultIO.write` gates them. A block emitted empty is not the same
261 // as an absent one: `ldes_prob_from_histogram` and `getAvgBusyPeriod` both
262 // treat presence as the claim that the measurement was taken.
263 // GATED ON THE FLAG, not on the measurement. A transient run computes the
264 // histogram whether or not anyone asked for it, and `LDESResultIO` writes
265 // the block only when the flag was given; emitting it anyway would make
266 // this engine's document differ from the reference's on every transient run.
267 if (o.export_histogram && r.histogram_space.rows() > 0 && r.histogram_time.rows() > 0) {
268 s += " \"stateHistogram\": {";
269 s += "\"space\": " + mat_json(r.histogram_space, r.histogram_space.rows(),
270 r.histogram_space.cols());
271 s += ", \"time\": " + mat_json(r.histogram_time, r.histogram_time.rows(), 1);
272 if (r.traj_space.rows() > 0 && r.traj_time.rows() > 0) {
273 s += ", \"trajSpace\": " +
274 mat_json(r.traj_space, r.traj_space.rows(), r.traj_space.cols());
275 s += ", \"trajTime\": " + mat_json(r.traj_time, r.traj_time.rows(), 1);
276 }
277 s += "},\n";
278 }
279
280 if (!r.busy_periods.empty()) {
281 s += " \"busyPeriods\": {\"orders\": " +
282 std::to_string(r.busy_periods[0].mean.size()) + ", \"targets\": [";
283 for (std::size_t ti = 0; ti < r.busy_periods.size(); ++ti) {
284 const LdesResult::BusyPeriodTarget& t = r.busy_periods[ti];
285 if (ti) s += ", ";
286 s += "{\"name\": " + str_json(t.name) + ", \"stations\": [";
287 for (std::size_t k = 0; k < t.stations.size(); ++k) {
288 if (k) s += ", ";
289 s += std::to_string(t.stations[k]);
290 }
291 s += "], \"class\": " + std::to_string(t.job_class) + ", \"mean\": [";
292 for (std::size_t k = 0; k < t.mean.size(); ++k) {
293 if (k) s += ", ";
294 s += num(t.mean[k]);
295 }
296 s += "], \"count\": [";
297 for (std::size_t k = 0; k < t.count.size(); ++k) {
298 if (k) s += ", ";
299 s += num(t.count[k]);
300 }
301 s += "]}";
302 }
303 s += "]},\n";
304 }
305
306 // The job-level end state, in the --initjobs layout, so a run can be continued from it.
307 if (o.track_jobs || o.init_jobs.rows() > 0) {
308 s += " \"jobState\": {\"finalJobs\": " +
309 mat_json(r.finalJobs, r.finalJobs.rows(), r.finalJobs.cols());
310 s += ", \"jobDepartures\": " +
311 mat_json(r.jobDepartures, r.jobDepartures.rows(), r.jobDepartures.cols()) + "},\n";
312 }
313
314 if (!r.cache_metrics.empty()) {
315 s += " \"cacheMetrics\": {";
316 bool first = true;
317 for (std::map<std::string, line::ldes::LdesCacheMetrics>::const_iterator it =
318 r.cache_metrics.begin();
319 it != r.cache_metrics.end(); ++it) {
320 if (!first) s += ", ";
321 first = false;
322 const line::ldes::LdesCacheMetrics& cm = it->second;
323 s += str_json(it->first) + ": {";
324 s += "\"hit\": " + mat_json(cm.hit, cm.hit.rows(), cm.hit.cols());
325 s += ", \"delayed\": " + mat_json(cm.delayed, cm.delayed.rows(), cm.delayed.cols());
326 s += ", \"miss\": " + mat_json(cm.miss, cm.miss.rows(), cm.miss.cols());
327 s += ", \"latency\": " + mat_json(cm.latency, cm.latency.rows(), cm.latency.cols());
328 s += ", \"hitList\": " + mat_json(cm.hitList, cm.hitList.rows(), cm.hitList.cols());
329 s += ", \"itemProb\": " + mat_json(cm.itemProb, cm.itemProb.rows(), cm.itemProb.cols());
330 s += ", \"listCost\": " + mat_json(cm.listCost, cm.listCost.rows(), cm.listCost.cols());
331 s += "}";
332 }
333 s += "},\n";
334 }
335
336 if (r.QNfcr.rows() > 0) {
337 const std::size_t G = r.QNfcr.rows();
338 s += " \"fcr\": {\"nregions\": " + std::to_string(G);
339 s += ", \"QNfcr\": " + mat_json(r.QNfcr, G, C);
340 s += ", \"UNfcr\": " + mat_json(r.UNfcr, G, C);
341 s += ", \"RNfcr\": " + mat_json(r.RNfcr, G, C);
342 s += ", \"TNfcr\": " + mat_json(r.TNfcr, G, C);
343 s += ", \"ANfcr\": " + mat_json(r.ANfcr, G, C);
344 s += ", \"WNfcr\": " + mat_json(r.WNfcr, G, C);
345 s += ", \"WeightNfcr\": " + mat_json(r.WeightNfcr, G, C);
346 s += ", \"MemOccNfcr\": " + mat_json(r.MemOccNfcr, G, C);
347 s += ", \"DropRateNfcr\": " + mat_json(r.DropRateNfcr, G, C);
348 s += "},\n";
349 }
350
351 // Fork-join section metrics (getAvgFJ), TOP-LEVEL as the Java engine writes them: one row per Fork/Join pair,
352 // named by "forks"/"joins", in fork-then-join node order. Steady state only, and only on a model with a pair.
353 if (r.t.empty() && !r.fj_forks.empty() && r.FJTput.rows() == r.fj_forks.size()) {
354 const std::size_t P = r.fj_forks.size();
355 s += " \"forkJoin\": {\"forks\": [";
356 for (std::size_t p = 0; p < P; ++p) s += (p ? ", " : "") + str_json(r.fj_forks[p]);
357 s += "], \"joins\": [";
358 for (std::size_t p = 0; p < P; ++p) s += (p ? ", " : "") + str_json(r.fj_joins[p]);
359 s += "]";
360 s += ", \"FJQLen\": " + mat_json(r.FJQLen, P, C);
361 s += ", \"FJRespT\": " + mat_json(r.FJRespT, P, C);
362 s += ", \"FJTput\": " + mat_json(r.FJTput, P, C);
363 s += "},\n";
364 }
365
366 // The reference gates the whole transient block on `--trajectory`, not on
367 // the run being transient.
368 // The samples appear at TOP LEVEL under --respt-samples and INSIDE the
369 // transient block under --trajectory. Both spellings are the reference's,
370 // and both are read: the C++ client takes whichever is present, MATLAB's
371 // `sample()` and `sampleSys()` read the nested one and returned nothing
372 // while only the top-level spelling existed.
373 const std::string respt_json = respt_samples_json(r);
374 if (o.export_trajectory && !r.t.empty()) {
375 s += " \"transient\": {\"t\": [";
376 for (std::size_t k = 0; k < r.t.size(); ++k) {
377 if (k) s += ", ";
378 s += num(r.t[k]);
379 }
380 s += "]";
381 const char* keys[3] = {"QNt", "UNt", "TNt"};
382 const std::vector<std::vector<line::Matrix<double>>>* src[3] = {&r.QNt, &r.UNt, &r.TNt};
383 for (int q = 0; q < 3; ++q) {
384 s += std::string(", \"") + keys[q] + "\": [";
385 for (std::size_t i = 0; i < src[q]->size(); ++i) {
386 if (i) s += ", ";
387 s += "[";
388 for (std::size_t k = 0; k < (*src[q])[i].size(); ++k) {
389 if (k) s += ", ";
390 const line::Matrix<double>& mm = (*src[q])[i][k];
391 s += mat_json(mm, mm.rows(), mm.cols());
392 }
393 s += "]";
394 }
395 s += "]";
396 }
397 if (!respt_json.empty()) s += ", \"respTimeSamples\": " + respt_json;
398 s += "},\n";
399 }
400
401 if (o.export_respt && !respt_json.empty())
402 s += " \"respTimeSamples\": " + respt_json + ",\n";
403 s += " \"sampleCounts\": {";
404 s += "\"QNSamples\": " + mat_json(r.QNSamples, S, C);
405 s += ", \"UNSamples\": " + mat_json(r.UNSamples, S, C);
406 s += ", \"RNSamples\": " + mat_json(r.RNSamples, S, C);
407 s += ", \"TNSamples\": " + mat_json(r.TNSamples, S, C);
408 s += "},\n";
409
410 s += " \"confidenceIntervals\": {";
411 s += "\"QNCI\": " + mat_json(r.QNCI, S, C);
412 s += ", \"UNCI\": " + mat_json(r.UNCI, S, C);
413 s += ", \"RNCI\": " + mat_json(r.RNCI, S, C);
414 s += ", \"TNCI\": " + mat_json(r.TNCI, S, C);
415 s += ", \"ANCI\": " + mat_json(r.ANCI, S, C);
416 s += ", \"WNCI\": " + mat_json(r.WNCI, S, C);
417 s += "},\n";
418
419 s += " \"relativePrecision\": {";
420 s += "\"QNRelPrec\": " + mat_json(r.QNRelPrec, S, C);
421 s += ", \"UNRelPrec\": " + mat_json(r.UNRelPrec, S, C);
422 s += ", \"RNRelPrec\": " + mat_json(r.RNRelPrec, S, C);
423 s += ", \"TNRelPrec\": " + mat_json(r.TNRelPrec, S, C);
424 s += "},\n";
425
426 s += " \"impatience\": {";
427 s += "\"renegedCustomers\": " + mat_json(r.renegedCustomers, S, C);
428 s += ", \"avgRenegingWaitTime\": " + mat_json(r.avgRenegingWaitTime, S, C);
429 s += ", \"renegingRate\": " + mat_json(r.renegingRate, S, C);
430 s += ", \"balkedCustomers\": " + mat_json(r.balkedCustomers, S, C);
431 s += ", \"balkingProbability\": " + mat_json(r.balkingProbability, S, C);
432 s += ", \"retriedCustomers\": " + mat_json(r.retriedCustomers, S, C);
433 s += ", \"retrialDropped\": " + mat_json(r.retrialDropped, S, C);
434 s += ", \"avgOrbitSize\": " + mat_json(r.avgOrbitSize, S, C);
435 s += "}\n";
436 s += "}\n";
437 return s;
438}
439
440/** A 1-based (nidx+1) x 1 element column as the 1 x nidx row `LDESResultIO.saveLN` emits. */
441std::string ln_row_json(const line::Matrix<double>& m, std::size_t nidx) {
442 std::string s = "[[";
443 for (std::size_t k = 1; k <= nidx; ++k) {
444 if (k > 1) s += ", ";
445 s += num(k < m.rows() ? m(k, 0) : 0.0);
446 }
447 return s + "]]";
448}
449
450/**
451 * The LAYERED `ldes-result` document, key for key with `LDESResultIO.saveLN`, in the JAR's
452 * 0-based element index space (C++ element k is entry k-1 of every row and of `cacheMetrics`).
453 *
454 * UCallLN, TCallLN and UEntryClassLN ARE NOT EMITTED: the native LN engine does not measure the
455 * per-call and per-entry-stream thread occupancy, and no subprocess client reads them (they back
456 * the JAR-only getLNAvgCallUtil/CallTput/EntryClassUtil). The CI block is all null, as the
457 * JAR's layered engine leaves it.
458 */
459std::string ln_result_json(const line::lqn::LqnStruct<double>& lsn,
460 const line::ldes::engine::LnResult& r, const LdesOptions& o,
461 double runtime, bool respt) {
462 const std::size_t n = lsn.nidx;
463 // THE DOCUMENT CARRIES THE JAR ENGINE'S RAW CONVENTIONS, which differ from the LnResult table:
464 // ULN is MEAN BUSY SERVERS, undivided, because every client divides it by hostReachMult itself
465 // (Python getLNAvgTable); emitting the per-server figure would divide twice. QLN at a processor
466 // and RLN at a task are 0, as Solver_ssj_ln writes them; the native engine measures both, and
467 // LnResult keeps them for an in-process caller.
468 line::Matrix<double> QLN = r.QLN, ULN = r.ULN, RLN = r.RLN;
469 for (std::size_t k = 1; k <= n; ++k) {
470 ULN(k, 0) *= lsn.host_reach_mult(k);
471 if (k > lsn.hshift && k <= lsn.hshift + lsn.nhosts) QLN(k, 0) = 0.0;
472 if (k > lsn.tshift && k <= lsn.tshift + lsn.ntasks) RLN(k, 0) = 0.0;
473 }
474 std::string s = "{\"format\": \"ldes-result\", \"version\": \"1.0\", "
475 "\"modelType\": \"LayeredNetwork\", \"solver\": \"LDES\", ";
476 s += "\"method\": " + str_json(o.method) + ", \"runtime\": " + num(runtime) + ", ";
477 s += "\"dimensions\": {\"nidx\": " + std::to_string(n) +
478 ", \"nhosts\": " + std::to_string(lsn.nhosts) +
479 ", \"ntasks\": " + std::to_string(lsn.ntasks) +
480 ", \"nentries\": " + std::to_string(lsn.nentries) +
481 ", \"nacts\": " + std::to_string(lsn.nacts) +
482 ", \"ncalls\": " + std::to_string(lsn.ncalls) +
483 ", \"tshift\": " + std::to_string(lsn.tshift) +
484 ", \"eshift\": " + std::to_string(lsn.eshift) +
485 ", \"ashift\": " + std::to_string(lsn.ashift) + ", \"names\": [";
486 for (std::size_t k = 1; k <= n; ++k) {
487 if (k > 1) s += ", ";
488 s += str_json(k < lsn.names.size() ? lsn.names[k] : "Element" + std::to_string(k - 1));
489 }
490 s += "]}, \"metrics\": {";
491 s += "\"QLN\": " + ln_row_json(QLN, n) + ", \"ULN\": " + ln_row_json(ULN, n);
492 s += ", \"RLN\": " + ln_row_json(RLN, n) + ", \"WLN\": " + ln_row_json(r.WLN, n);
493 s += ", \"TLN\": " + ln_row_json(r.TLN, n) + ", \"ALN\": " + ln_row_json(r.ALN, n);
494 s += ", \"ZLN\": " + ln_row_json(r.ZLN, n) + "}, ";
495 s += "\"confidenceIntervals\": {\"QLNCI\": null, \"ULNCI\": null, \"RLNCI\": null, "
496 "\"TLNCI\": null}";
497 if (!r.cache_task.empty()) {
498 s += ", \"cacheMetrics\": [";
499 for (std::size_t i = 0; i < r.cache_task.size(); ++i) {
500 if (i) s += ", ";
501 s += "{\"cacheTaskIdx\": " + std::to_string(r.cache_task[i] - 1) +
502 ", \"itemEntryIdx\": " + std::to_string(r.cache_entry[i] - 1) +
503 ", \"hitProb\": " + num(r.cache_hit[i]) + ", \"missProb\": " +
504 num(r.cache_miss[i]) + ", \"delayedProb\": " + num(r.cache_delayed[i]) +
505 ", \"readRate\": " + num(r.cache_read_rate[i]) + "}";
506 }
507 s += "]";
508 }
509 if (respt) {
510 s += ", \"entryRespTimeSamples\": [";
511 for (std::size_t e = 0; e < r.entry_resp_samples.size(); ++e) {
512 if (e) s += ", ";
513 s += "[";
514 for (std::size_t j = 0; j < r.entry_resp_samples[e].size(); ++j) {
515 if (j) s += ", ";
516 s += num(r.entry_resp_samples[e][j]);
517 }
518 s += "]";
519 }
520 s += "]";
521 }
522 return s + "}\n";
523}
524
525} // namespace
526
527int main(int argc, char** argv) {
528 if (argc < 2) {
529 print_help();
530 return 1;
531 }
532 const std::string cmd = argv[1];
533 if (cmd == "-h" || cmd == "--help") {
534 print_help();
535 return 0;
536 }
537 if (cmd != "solve") {
538 std::cerr << "Error: unknown command '" << cmd << "'. The only command is 'solve'.\n";
539 return 1;
540 }
541 if (argc < 3) fail("solve requires a model file.");
542
543 const std::string model_path = argv[2];
544 std::string output_path;
545 LdesOptions o;
546 bool have_samples = false, have_events = false;
547 std::vector<std::string> given; // every flag passed, for the layered path's refusals
548
549 for (int i = 3; i < argc; ++i) {
550 const std::string a = argv[i];
551 given.push_back(a);
552 if (a == "-o") {
553 output_path = need_value(argc, argv, i, a);
554 } else if (a == "-s" || a == "--samples") {
555 o.samples = static_cast<std::size_t>(std::atol(need_value(argc, argv, i, a).c_str()));
556 have_samples = true;
557 } else if (a == "-e" || a == "--maxevents") {
558 o.events = static_cast<std::size_t>(std::atol(need_value(argc, argv, i, a).c_str()));
559 have_events = true;
560 } else if (a == "--maxtime") {
561 o.timeout = std::atof(need_value(argc, argv, i, a).c_str());
562 } else if (a == "--seed") {
563 o.seed = std::atol(need_value(argc, argv, i, a).c_str());
564 } else if (a == "--method") {
565 o.method = need_value(argc, argv, i, a);
566 } else if (a == "--cnvgon") {
567 o.cnvgon = true;
568 } else if (a == "--cnvgtol") {
569 o.cnvgtol = std::atof(need_value(argc, argv, i, a).c_str());
570 } else if (a == "--cnvgbatch") {
571 o.cnvgbatch = std::atoi(need_value(argc, argv, i, a).c_str());
572 } else if (a == "--cnvgchk") {
573 o.cnvgchk = std::atoi(need_value(argc, argv, i, a).c_str());
574 } else if (a == "--tranfilter") {
575 o.tranfilter = need_value(argc, argv, i, a);
576 } else if (a == "--mserbatch") {
577 o.mserbatch = std::atoi(need_value(argc, argv, i, a).c_str());
578 } else if (a == "--tranobs") {
579 o.tranobs = std::atoi(need_value(argc, argv, i, a).c_str());
580 if (o.tranobs <= 0) fail("--tranobs must be positive.");
581 } else if (a == "--warmupfrac") {
582 o.warmupfrac = std::atof(need_value(argc, argv, i, a).c_str());
583 } else if (a == "--cimethod") {
584 o.cimethod = need_value(argc, argv, i, a);
585 } else if (a == "--obmoverlap") {
586 o.obmoverlap = std::atof(need_value(argc, argv, i, a).c_str());
587 } else if (a == "--ciminbatch") {
588 o.ciminbatch = std::atoi(need_value(argc, argv, i, a).c_str());
589 } else if (a == "--ciminobs") {
590 o.ciminobs = std::atoi(need_value(argc, argv, i, a).c_str());
591 } else if (a == "--spectrallowfreqfrac") {
592 o.spectral_low_freq_frac = std::atof(need_value(argc, argv, i, a).c_str());
593 } else if (a == "--slotted") {
594 o.slotted = true;
595 } else if (a == "--slotlength") {
596 o.slot_length = std::atof(need_value(argc, argv, i, a).c_str());
597 o.slotted = true; // the reference lets --slotlength imply --slotted
598 } else if (a == "--replications") {
599 o.replications = std::atoi(need_value(argc, argv, i, a).c_str());
600 } else if (a == "--numthreads") {
601 o.numthreads = std::atoi(need_value(argc, argv, i, a).c_str());
602 } else if (a == "--timespan") {
603 const std::vector<double> t = parse_doubles(need_value(argc, argv, i, a));
604 if (t.size() != 2) fail("--timespan takes T0,T1.");
605 o.has_timespan = true;
606 o.t0 = t[0];
607 o.t1 = t[1];
608 } else if (a == "--busyperiod") {
609 o.busy_period_orders = std::atoi(need_value(argc, argv, i, a).c_str());
610 } else if (a == "--busyperiod-subnet") {
611 o.busy_period_subnets.push_back(parse_indices(need_value(argc, argv, i, a)));
612 } else if (a == "--initsol") {
613 o.init_sol = parse_doubles(need_value(argc, argv, i, a));
614 } else if (a == "--initjobs") {
615 const std::string v = need_value(argc, argv, i, a);
616 std::vector<std::vector<double>> rows;
617 std::size_t at = 0;
618 while (at <= v.size()) {
619 const std::size_t semi = std::min(v.find(';', at), v.size());
620 if (semi > at) rows.push_back(parse_doubles(v.substr(at, semi - at)));
621 at = semi + 1;
622 }
623 const std::size_t ncols = rows.empty() ? 0 : rows[0].size();
624 for (std::size_t r = 0; r < rows.size(); ++r)
625 if (rows[r].size() != ncols) fail("--initjobs rows must have the same length.");
626 o.init_jobs = line::Matrix<double>(rows.size(), ncols, 0.0);
627 for (std::size_t r = 0; r < rows.size(); ++r)
628 for (std::size_t c = 0; c < ncols; ++c) o.init_jobs(r, c) = rows[r][c];
629 } else if (a == "--trackjobs") {
630 o.track_jobs = true;
631 } else if (a == "--export-histogram") {
632 o.export_histogram = true;
633 } else if (a == "--trajectory") {
634 o.export_trajectory = true;
635 } else if (a == "--respt-samples") {
636 o.export_respt = true;
637 } else if (a == "--rest") {
638 refuse(a, "server mode belongs to line-cli, not to this binary");
639 } else if (a == "-h" || a == "--help") {
640 print_help();
641 return 0;
642 } else {
643 fail("unknown option '" + a + "'.");
644 }
645 }
646 if (have_events && !have_samples) o.samples = o.events;
647
648 try {
649 // A LayeredNetwork takes the native LN engine (the twin of Solver_ssj_ln) and the layered
650 // document, as LdesCLI routes it to its own. That engine reads --samples/--maxevents and
651 // --seed; the flags that change WHAT is simulated or reported are refused by name, the
652 // estimation and budget knobs are ignored exactly as the JAR's layered engine ignores them.
653 if (line::io::is_layered_json(model_path)) {
654 static const char* const network_only[] = {
655 "--timespan", "--tranobs", "--trajectory", "--export-histogram", "--busyperiod",
656 "--busyperiod-subnet", "--initsol", "--initjobs", "--trackjobs", "--slotted",
657 "--slotlength", "--replications"};
658 for (const std::string& g : given)
659 for (const char* f : network_only)
660 if (g == f)
661 fail(g + " is a Network-path option; the layered engine reports the steady "
662 "state of one replication only.");
663 if (o.method != "default")
664 fail("method '" + o.method + "' is not a layered LDES method; use 'default'.");
667 const std::chrono::steady_clock::time_point t0 = std::chrono::steady_clock::now();
669 const double runtime =
670 std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();
671 const std::string doc = ln_result_json(lsn, r, o, runtime, o.export_respt);
672 if (output_path.empty()) {
673 std::cout << doc;
674 } else {
675 std::ofstream out(output_path.c_str());
676 if (!out) fail("cannot write '" + output_path + "'.");
677 out << doc;
678 }
679 std::cerr << "LDES analysis [method: default; type: approximate, randomized; lang: cpp; "
680 "native LN engine] completed in " << runtime << "s. Iterations: "
681 << r.events << ".\n";
682 return 0;
683 }
686
687 // WALL time, not std::clock(): the replications run on a thread pool, and CPU time summed over the
688 // threads would report the serial cost of a run that took a fraction of it.
689 const std::chrono::steady_clock::time_point t_start = std::chrono::steady_clock::now();
690 const LdesResult r = line::ldes::ldes_engine_solve(sn, o);
691 const double runtime =
692 std::chrono::duration<double>(std::chrono::steady_clock::now() - t_start).count();
693
694 const std::string doc = result_json(r, o, runtime, o.samples);
695 if (output_path.empty()) {
696 std::cout << doc;
697 } else {
698 std::ofstream out(output_path.c_str());
699 if (!out) fail("cannot write '" + output_path + "'.");
700 out << doc;
701 }
702 std::cerr << "LDES analysis [method: " << o.method
703 << "; type: approximate, randomized; lang: cpp] completed in " << runtime
704 << "s. Iterations: " << o.samples << ".\n";
705 return 0;
706 } catch (const line::UnsupportedError& e) {
707 std::cerr << "Error: [" << e.id() << "] " << e.what() << "\n";
708 return 2;
709 } catch (const line::Error& e) {
710 // Same exit status as before; only the vocabulary id is added to the line.
711 std::cerr << "Error: [" << e.id() << "] " << e.what() << "\n";
712 return 1;
713 } catch (const std::exception& e) {
714 std::cerr << "Error: " << e.what() << "\n";
715 return 1;
716 }
717}
Base error for the multiprecision C++ port.
Definition error.h:73
const std::string & id() const noexcept
The full identifier, e.g.
Definition error.h:78
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
A refusal: the feature, method or analysis is not covered: LINE:unsupported:<kind>.
Definition error.h:117
A network plus its refreshed NetworkStruct.
A queueing network under construction.
const NetworkStruct< T > & get_struct()
The refreshed struct, MATLAB's model.getStruct().
The exception types the port throws.
int main(int argc, char **argv)
Definition ldes_cli.cpp:527
The NATIVE LDES discrete-event engine.
The NATIVE LDES engine for LAYERED (LQN) models, the C++ twin of jline/solvers/ldes/handlers/Solver_s...
The option and result records of SolverLDES, the discrete-event simulator.
model.json with type: "LayeredNetwork" -> LqnStruct, via LqnBuilder.
bool is_layered_json(const std::string &path)
True when a file is a LayeredNetwork model.json rather than an .lqnx.
qn::Network< T > read_network_json(const std::string &path)
Parse a model.json file into a qn::Network<T>.
lqn::LqnStruct< T > read_layered_model(const std::string &path)
Read a layered model from any of its interchanges: the LINE model.json, the LQNS ....
LdesResult ldes_engine_solve(const qn::NetworkStruct< T > &sn, const LdesOptions &o)
Simulate sn, over independent REPLICATIONS when options.replications > 1.
engine::LnResult ldes_ln_engine_solve(const lqn::LqnStruct< T > &lsn, const LdesOptions &o)
Simulate a layered model in process.
Reader for the LINE model.json interchange (a Network model) into a qn::Network<T> built through the ...
Per-cache hit/miss/latency, as the cacheMetrics block carries them.
Matrix< double > listCost
(1 x nlists) mean storage cost held per list
Matrix< double > latency
(1 x nclasses) expected retrieval latency
Matrix< double > hitList
(nclasses x nlists) hit probability by list
Matrix< double > itemProb
(nitems x nlists+1) item position law
Matrix< double > delayed
(1 x nclasses) delayed-hit probability
Matrix< double > miss
(1 x nclasses) miss probability
Matrix< double > hit
(1 x nclasses) hit probability
The knobs of one LDES run.
One ldes-result document, parsed.
The layered result: per element, the mean measures.
std::vector< double > cache_read_rate
Matrix< double > ZLN
Think time per element, Solver_ssj_ln's ZLN: Little's law on a task's thread pool read backwards,...
std::vector< double > cache_miss
Matrix< double > ALN
Arrival rate per element, Solver_ssj_ln's ALN: an entry's request rate (its throughput,...
Matrix< double > WLN
Residence time per element, the ResidT column: the time an activity holds ITS HOST PROCESSOR per visi...
std::vector< std::size_t > cache_entry
std::vector< double > cache_hit
std::vector< std::vector< double > > entry_resp_samples
Every per-request ENTRY response time observed, one vector per entry in LOCAL index space (0....
std::vector< std::size_t > cache_task
Cache measures, one row per ItemEntry that was read, pooled over the replicas of its cache task as So...
long long events
Activity completions over the whole run, warmup included: what the budget counts.
std::vector< double > cache_delayed
std::size_t nentries
Definition lqn_struct.h:209
std::vector< std::string > names
(nidx+1) declared name
Definition lqn_struct.h:212
double host_reach_mult(std::size_t idx) const
Servers of the host of element idx that its work can reach: the divisor of every LQN Util column (Sol...
Definition lqn_struct.h:383