LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ldes_sampler.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_SOLVERS_LDES_LDES_SAMPLER_H
6#define LINE_SOLVERS_LDES_LDES_SAMPLER_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The variate generators of the native LDES engine.
12 *
13 * ONE SAMPLER PER (station, class) PAIR, built once and then advanced, which is
14 * what `Solver_ssj.initializeGenerators` does and is not an optimization: a
15 * MAP, a RAP and an ME carry a PHASE across successive samples, and rebuilding
16 * the generator per variate would restart that phase every time and silently
17 * turn a correlated process into a renewal one with the same marginal. The
18 * autocorrelation is the reason those processes are in the model at all, so
19 * losing it produces a run that looks converged and answers a different
20 * question.
21 *
22 * WHERE THE PARAMETERS COME FROM, transcribed from
23 * `createNonMarkovianArrivalGen` and `firingGenFromMeanScv`:
24 *
25 * - ERLANG, HYPEREXP, PH, APH, COXIAN, COX2, MAP, MMPP2, ME, RAP, DMAP read
26 * the (D0,D1) REPRESENTATION, so their higher moments are exactly the
27 * model's;
28 * - UNIFORM, GAMMA, WEIBULL, LOGNORMAL, PARETO read the MOMENT PAIR (mean,
29 * SCV) and invert it, because `sn.proc` carries an Erlang FIT for them
30 * rather than their own parameters (see _kb/09-ldes-and-cache.md);
31 * - DET, IMMEDIATE, REPLAYER, and the counting families read their own
32 * parameters directly.
33 *
34 * A moment pair that no member of the family can realise is REFUSED by name.
35 * A non-negative uniform needs SCV <= 1/3; papering over that with a clamp
36 * would run a model with a different variance and report it as the user's.
37 */
38
39#include <algorithm>
40#include <cmath>
41#include <cstddef>
42#include <limits>
43#include <memory>
44#include <random>
45#include <string>
46#include <vector>
47
51#include "line/util/rng_ssj.h"
53#include "line/util/error.h"
54#include "line/util/matrix.h"
55
56namespace line {
57namespace ldes {
58namespace engine {
59
60/**
61 * The engine's randomness, in the SHAPE the Java engine uses it.
62 *
63 * `Solver_ssj` carries TWO generators wherever it samples, and which one serves
64 * depends on the family rather than on the call site:
65 *
66 * MRG32k3a stream = new MRG32k3a();
67 * stream.setSeed(new long[]{ seed+offset, ..., seed+offset+5 });
68 * this.svcRng[i][k] = new java.util.Random(seed + offset);
69 *
70 * The SSJ `randvar` generators (Exponential, Erlang, Uniform, Weibull, Pareto,
71 * Lognormal, Gamma, Poisson, Binomial, Bernoulli) are built on the MRG STREAM;
72 * the Markovian families do not go through SSJ at all -- `MapSampleGen` calls
73 * `Map_sample.map_sample(D0, D1, 1, rng)` with the java.util.Random. So a C++
74 * engine that means to walk the same sample path needs both, seeded from the
75 * same `seed + offset`, and must send each family to the same one.
76 *
77 * `mc` is the third generator and the one still out of step: `rap_sample` and
78 * `me_sample` take `pfqn::McRng` by reference, and the RAP/ME path has no SSJ
79 * or java.util.Random counterpart to align to yet.
80 */
81struct Rng {
82 rng::Mrg32k3a stream; ///< SSJ MRG32k3a: every renewal family
83 rng::JavaRandom aux; ///< java.util.Random: the Markovian samplers
84 pfqn::McRng mc; ///< the residual RAP/ME path, not yet aligned
85
86 /**
87 * A run seed and the TWO offsets the reference derives, one per generator.
88 *
89 * They are not always equal, which is why this takes both. At a SERVICE
90 * site `Solver_ssj` seeds the stream and the java.util.Random from the same
91 * `((numSources + svcIdx) * numClasses + k) * 10 + 1000`; at an ARRIVAL site
92 * the stream gets `(srcIdx * numClasses + k) * 10` and the Random the same
93 * expression PLUS 2000. Seeding both from one offset would put the
94 * Markovian arrival samplers on a stream the reference never uses.
95 */
96 Rng(long long seed, long long stream_offset, long long aux_offset)
97 : aux(seed + aux_offset), mc(static_cast<std::uint64_t>(seed + aux_offset)) {
98 stream.set_seed_offset(seed, stream_offset);
99 }
100
101 /** The common case, where the reference uses one offset for both. */
102 Rng(long long seed, long long offset) : Rng(seed, offset, offset) {}
103};
104
105/** Uniform on (0,1) off the MRG stream; `next_double` never returns 0. */
106inline double uniform01(Rng& g) { return g.stream.next_double(); }
107
108/**
109 * One variate generator, holding whatever state its family needs.
110 *
111 * Deliberately a tagged struct rather than a class hierarchy: the sampler is
112 * called once per event on the engine's hot path, and a virtual dispatch there
113 * costs more than the switch. The tag IS `ProcessType`, so a family added to
114 * the language shows up here as a missing case rather than as silence.
115 */
116class Sampler {
117public:
119
120 /** Build the generator of `d`; the (station, class) names are for diagnostics. */
121 template <class T>
122 Sampler(const lang::Distrib<T>& d, const std::string& where) : where_(where) {
123 type_ = d.type;
126 switch (type_) {
128 break;
130 require_mean();
131 rate_ = 1.0 / mean_;
132 break;
134 require_mean();
135 break;
137 // The reference serves an Immediate in 1e-8 time units, not in
138 // zero: a zero service time is an infinite rate the estimators
139 // cannot carry. See Distrib::immediate().
141 break;
143 for (const T& v : d.trace) trace_.push_back(num_traits<T>::to_double(v));
144 if (trace_.empty())
145 throw InputError("SolverLDES (native engine): the Replayer at " + where_ +
146 " carries no samples");
147 break;
149 require_moments();
150 const double half = mean_ * std::sqrt(3.0 * scv_);
151 a_ = mean_ - half;
152 b_ = mean_ + half;
153 if (!(a_ >= 0.0) || !(b_ > a_))
154 throw InputError("SolverLDES (native engine): the Uniform at " + where_ +
155 " implies a support reaching below zero; a non-negative "
156 "uniform requires an SCV of at most 1/3");
157 break;
158 }
160 require_moments();
161 a_ = 1.0 / scv_; // shape
162 b_ = mean_ * scv_; // SCALE, not the reference's rate
163 break;
165 require_moments();
166 const double c = std::sqrt(scv_);
167 a_ = std::pow(c, -1.086); // shape
168 b_ = mean_ / std::tgamma(1.0 + 1.0 / a_); // scale
169 if (!(a_ > 0.0) || !(b_ > 0.0))
170 throw InputError("SolverLDES (native engine): the Weibull at " + where_ +
171 " has unusable moments");
172 break;
173 }
175 require_moments();
176 const double c2p1 = scv_ + 1.0;
177 a_ = std::log(mean_ / std::sqrt(c2p1)); // mu
178 b_ = std::sqrt(std::log(c2p1)); // sigma
179 if (!(b_ > 0.0))
180 throw InputError("SolverLDES (native engine): the Lognormal at " + where_ +
181 " has a zero log-variance");
182 break;
183 }
185 require_moments();
186 // SCV = 1/(a(a-2)) for a > 2, so a = 1 + sqrt(1 + 1/SCV), and the
187 // scale follows from mean = a*m/(a-1).
188 a_ = std::sqrt(1.0 + 1.0 / scv_) + 1.0; // shape
189 b_ = mean_ * (a_ - 1.0) / a_; // scale
190 break;
192 require_mean();
193 // Supported on {1,2,...}: the trial index of the first success,
194 // mean 1/p and SCV 1-p. SSJ's own Geometric counts FAILURES and
195 // would admit a zero-length interval.
196 a_ = 1.0 / mean_;
197 if (!(a_ > 0.0) || a_ > 1.0)
198 throw InputError("SolverLDES (native engine): the Geometric at " + where_ +
199 " has a success probability outside (0,1]");
200 break;
202 // The reference carries a DiscreteUniform as the moment pair
203 // {mean, SCV}, not as its bounds, so the bounds are recovered from
204 // the pair HERE TOO rather than read off `d.params`: the two
205 // engines must place the lattice identically, and the Java one has
206 // nothing but the moments. var = (w^2-1)/12 gives the integral
207 // width, and the mean then places it, which leaves a lower bound
208 // off the integers where the user put it.
209 require_moments();
210 const double var = scv_ * mean_ * mean_;
211 b_ = std::floor(std::sqrt(12.0 * var + 1.0) + 0.5); // width
212 a_ = mean_ - (b_ - 1.0) / 2.0; // lower bound
213 if (!(b_ >= 1.0))
214 throw InputError("SolverLDES (native engine): the DiscreteUniform at " +
215 where_ + " has moments no (min,max) pair realises");
216 if (!(a_ >= 0.0))
217 throw InputError("SolverLDES (native engine): the DiscreteUniform at " +
218 where_ + " reaches below zero");
219 break;
220 }
222 require_mean();
223 break;
225 if (!(mean_ >= 0.0) || !(mean_ <= 1.0))
226 throw InputError("SolverLDES (native engine): the Bernoulli at " + where_ +
227 " has a mean outside [0,1]");
228 break;
230 require_moments();
231 const double p = 1.0 - scv_ * mean_;
232 if (!(p > 0.0) || p > 1.0)
233 throw InputError("SolverLDES (native engine): the Binomial at " + where_ +
234 " has moments no (n,p) pair realises");
235 a_ = p;
236 b_ = std::floor(mean_ / p + 0.5);
237 break;
238 }
245 load_schedule(d);
246 break;
261 load_map(d);
262 break;
263 default:
264 throw UnsupportedError("SolverLDES (native engine): the process at " + where_ +
265 " is of a family this engine does not sample");
266 }
267 }
268
269 bool disabled() const { return type_ == lang::ProcessType::DISABLED; }
270 lang::ProcessType type() const { return type_; }
271 double mean() const { return mean_; }
272
273 /**
274 * One variate from a TIME-INHOMOGENEOUS process, given the instant it
275 * starts at.
276 *
277 * The families whose parameters depend on absolute time cannot be sampled
278 * from a duration alone, so this overload takes `from` and every other
279 * family ignores it. The engine calls it wherever a duration is drawn; a
280 * homogeneous process is unaffected.
281 */
282 double next_at(Rng& g, double from) {
283 if (!has_schedule_) return next(g);
284 return schedule_sample(g, from);
285 }
286
287 bool time_varying() const { return has_schedule_; }
288
289 /**
290 * One variate. Advances whatever phase the family carries.
291 *
292 * EVERY RENEWAL FAMILY BUT THE ERLANG COSTS EXACTLY ONE UNIFORM off the MRG
293 * stream, which is not an optimization but the reference's behaviour: SSJ's
294 * instance generators invert, and a family that drew twice would shift
295 * every later event in the run. The quantiles live in
296 * `ldes_ssj_variates.h` and are pinned there against SSJ's own output. The
297 * Erlang is the exception, and deliberately so: it costs k uniforms in both
298 * engines because the Gamma inversion SSJ used is not reproducible across
299 * JVM releases.
300 */
301 double next(Rng& g) { return resolve_zero_atom(draw(g)); }
302
303 /**
304 * The reference's `resolveZeroAtom`: the discrete laws supported on
305 * {0,1,...} can return a zero-length interval, and a zero-length service is
306 * an infinite rate no estimator carries. `Immediate` is exactly what a
307 * zero-length interval denotes elsewhere in the engine, so the atom is
308 * served at that scale. Under SLOTTED mode 1/Immediate is not a lattice
309 * point, so `slot_snap` refuses the sample rather than inventing one, which
310 * is the refusal the reference raises in its own words.
311 */
312 double resolve_zero_atom(double v) const {
313 if (v != 0.0) return v;
314 switch (type_) {
320 default:
321 return v;
322 }
323 }
324
325private:
326 double draw(Rng& g) {
327 switch (type_) {
329 // SSJ's ExponentialDist.inverseF is -log1p(-u)/lambda, which is
330 // NOT -log(u)/lambda: they differ in the last bits for small u,
331 // exactly where an interarrival time matters most.
332 return ssj::exponential_inverse(rate_, uniform01(g));
335 return mean_;
337 const double v = trace_[trace_idx_];
338 trace_idx_ = (trace_idx_ + 1) % trace_.size();
339 // The reference floors a non-positive trace entry rather than
340 // scheduling an event in the past.
341 return (v <= 0.0) ? 1e-9 : v;
342 }
344 return ssj::uniform_inverse(a_, b_, uniform01(g));
346 // b_ is the SCALE here; SSJ's GammaGen takes the RATE.
347 return ssj::gamma_inverse(a_, 1.0 / b_, uniform01(g));
349 // b_ is the SCALE; SSJ's WeibullGen takes lambda = 1/scale.
350 return ssj::weibull_inverse(a_, 1.0 / b_, 0.0, uniform01(g));
352 return ssj::lognormal_inverse(a_, b_, uniform01(g));
354 return ssj::pareto_inverse(a_, b_, uniform01(g));
356 if (a_ >= 1.0) {
357 uniform01(g); // keep the stream consumption independent of p
358 return 1.0;
359 }
360 return std::ceil(std::log(1.0 - uniform01(g)) / std::log(1.0 - a_));
361 }
363 return ssj::uniform_int_inverse(a_, b_, uniform01(g));
365 return ssj::poisson_inverse(mean_, uniform01(g));
367 return ssj::bernoulli_inverse(mean_, uniform01(g));
369 return ssj::binomial_inverse(static_cast<int>(b_), a_, uniform01(g));
371 // An Erlang costs k uniforms, one per phase, which is the one
372 // renewal family that does not invert in a single draw. The
373 // reference used to invert the Gamma here, but that inversion
374 // is iterative and its last bits move between JVM releases, so
375 // a seeded run was not reproducible across JVMs; both engines
376 // now convolve exponentials instead.
377 if (erlang_k_ <= 0) return next_markovian(g);
378 double sum = 0.0;
379 for (int i = 0; i < erlang_k_; ++i)
380 sum += ssj::exponential_inverse(erlang_rate_, uniform01(g));
381 return sum;
382 }
383 default:
384 return next_markovian(g);
385 }
386 }
387
388public:
389 /**
390 * The phase the process is in, for a caller that must carry it across a
391 * preemption or across successive executions of an LQN entry. -1 when the
392 * family has no phase.
393 */
394 int phase() const { return has_phase_ ? static_cast<int>(phase_) : -1; }
395 /**
396 * The 1-based mark of the interval last returned by `next`/`next_at`, or 0 when
397 * this process carries none. At a Source the engine reads it to pick the class of
398 * the arriving job, exactly as `Solver_ssj` consumes `arrivalPendingMark`.
399 */
400 int last_mark() const { return last_mark_; }
401 /**
402 * How many jobs the epoch last returned by `next`/`next_at` releases, or 1 when
403 * this process carries no batch axis. At a Source the engine reads it to release
404 * that many jobs at one instant, exactly as `Solver_ssj` consumes
405 * `arrivalBatchSize`; at a bulk server it is how many completions the firing
406 * clears.
407 */
408 int last_batch() const { return last_batch_ > 0 ? last_batch_ : 1; }
409 bool marked() const { return !mark_.empty() || !seg_mark_.empty(); }
410 /** True when the process carries its OWN batch sizes: a BMAP or a BMMAPt. */
411 bool batched() const { return !batch_.empty() || !seg_batch_.empty(); }
412 void set_phase(int p) {
413 if (has_phase_ && p >= 0 && static_cast<std::size_t>(p) < map_.order()) {
414 phase_ = static_cast<std::size_t>(p);
415 phase_known_ = true;
416 }
417 }
418
419private:
420 void require_mean() const {
421 if (!(mean_ > 0.0) || !std::isfinite(mean_))
422 throw InputError("SolverLDES (native engine): the process at " + where_ +
423 " has a mean that is neither finite nor positive");
424 }
425 void require_moments() const {
426 require_mean();
427 if (!(scv_ > 0.0) || !std::isfinite(scv_))
428 throw InputError("SolverLDES (native engine): the process at " + where_ +
429 " has an SCV that is neither finite nor positive");
430 }
431
432 template <class T>
433 void load_map(const lang::Distrib<T>& d) {
434 if (d.D0.rows() == 0 || d.D0.rows() != d.D1.rows())
435 throw InputError("SolverLDES (native engine): the process at " + where_ +
436 " declares no (D0,D1) representation to sample");
437 const std::size_t K = d.D0.rows();
438 map_.D0 = Matrix<double>(K, K, 0.0);
439 map_.D1 = Matrix<double>(K, K, 0.0);
440 for (std::size_t i = 0; i < K; ++i)
441 for (std::size_t j = 0; j < K; ++j) {
442 map_.D0(i, j) = num_traits<T>::to_double(d.D0(i, j));
443 map_.D1(i, j) = num_traits<T>::to_double(d.D1(i, j));
444 }
445 // THE MOMENTS OF A (D0,D1) PROCESS COME FROM THE PAIR, not from the
446 // Distrib's own fields: `Distrib::map_dist` leaves mean 0 and SCV 1 as
447 // placeholders, so reading them would give this station a zero mean
448 // service time and a utilization computed against nothing.
449 if (!(mean_ > 0.0)) {
450 mean_ = num_traits<T>::to_double(mam::map_mean(map_));
451 if (!(mean_ > 0.0))
452 throw InputError("SolverLDES (native engine): the process at " + where_ +
453 " has a non-positive mean under its (D0,D1) representation");
454 }
455 // THE PER-MARK BLOCKS. `d.D1` is the aggregate sum_k D1k, which is what the
456 // interval walk uses; `Dmark` tells the marks apart once a D1 transition has
457 // fired. An MPH arrives here already lowered (D0 = S, D1k = s_k*alpha), so
458 // the same blocks serve it and no separate renewal path is needed.
459 mark_.clear();
460 batch_.clear();
462 for (const Matrix<T>& Dk : d.Dmark) {
463 Matrix<double> m(K, K, 0.0);
464 for (std::size_t i = 0; i < K; ++i)
465 for (std::size_t j = 0; j < K; ++j)
466 m(i, j) = num_traits<T>::to_double(Dk(i, j));
467 mark_.push_back(m);
468 }
469 }
470 // A BMAP OVERLOADS THE SAME FIELD WITH BATCH SIZES: `Dmark[b]` is the block
471 // that releases b+1 jobs. They used to be read into `mark_` and never
472 // consulted, because `mark_` is filled only for a marked stationary process
473 // and nothing else looked -- so this engine answered a BMAP model as its
474 // AGGREGATE MAP, every batch collapsed to one job, in silence. The blocks now
475 // drive `next_bmap_java`, which is the transcription of the reference's
476 // `Map_sample.BmapSampler`.
477 if (type_ == lang::ProcessType::BMAP) {
478 for (const Matrix<T>& Db : d.Dmark) {
479 Matrix<double> m(K, K, 0.0);
480 for (std::size_t i = 0; i < K; ++i)
481 for (std::size_t j = 0; j < K; ++j)
482 m(i, j) = num_traits<T>::to_double(Db(i, j));
483 batch_.push_back(m);
484 }
485 }
486 // ONLY the genuinely correlated families carry a phase between samples.
487 // A PH, an Erlang, a HyperExp and a Coxian are RENEWAL: restarting them
488 // from the entry law each time is what makes successive services
489 // independent, and carrying the phase instead would introduce a
490 // correlation the model does not have.
491 has_phase_ = (type_ == lang::ProcessType::MAP || type_ == lang::ProcessType::MMPP2 ||
492 type_ == lang::ProcessType::RAP ||
495 // ONCE PER STATION, not once per event: the table costs a matrix
496 // exponential and a thousand row-vector products, and every service at
497 // this station then inverts by binary search.
498 if (type_ == lang::ProcessType::ME)
499 me_.reset(new mam::MeSampler<double>(map_));
500 if (type_ == lang::ProcessType::ERLANG) recover_erlang();
501 }
502
503 /**
504 * Recovers (k, lambda) from an Erlang's (D0,D1) so `next` can convolve k
505 * exponentials off the MRG stream instead of walking the phases off the
506 * auxiliary java.util.Random. The reference engine samples the same
507 * convolution, so the two engines then agree sample for sample on an
508 * Erlang model; walking the phases does not, because it draws 2k uniforms
509 * from a different stream. A cell that does not carry the bidiagonal
510 * equal-rate structure of an Erlang is left to the MAP walk rather than
511 * approximated.
512 */
513 void recover_erlang() {
514 erlang_k_ = 0;
515 const std::size_t n = map_.order();
516 if (n == 0) return;
517 const double lambda = -map_.D0(0, 0);
518 if (!(lambda > 0.0)) return;
519 const double tol = 1e-12 * lambda;
520 for (std::size_t i = 0; i < n; ++i) {
521 if (std::fabs(-map_.D0(i, i) - lambda) > tol) return;
522 for (std::size_t j = 0; j < n; ++j) {
523 if (j == i) continue;
524 const double want = (j == i + 1) ? lambda : 0.0;
525 if (std::fabs(map_.D0(i, j) - want) > tol) return;
526 }
527 // Only the last phase completes, and it completes into phase 0.
528 for (std::size_t j = 0; j < n; ++j) {
529 const double want = (i + 1 == n && j == 0) ? lambda : 0.0;
530 if (std::fabs(map_.D1(i, j) - want) > tol) return;
531 }
532 }
533 erlang_k_ = static_cast<int>(n);
534 erlang_rate_ = lambda;
535 }
536
537 /**
538 * One interval from a MAP, transcribed from `Map_sample.MapSampler.next`.
539 *
540 * THE GENERATOR IS java.util.Random, NOT the MRG stream. `MapSampleGen`
541 * extends SSJ's `RandomVariateGen` and is handed the stream like every other
542 * generator, but its `nextDouble` ignores it and calls
543 * `Map_sample.map_sample(D0, D1, 1, rng)` with the java.util.Random built
544 * beside it. Drawing this family off the MRG stream would both use the wrong
545 * numbers and desynchronize every renewal draw that follows.
546 *
547 * The walk itself: hold the phase across calls (it is only advanced here,
548 * so an idle server freezes its service process), draw the initial phase
549 * from map_pie on the first call, then alternate an exponential holding
550 * time at rate -D0(i,i) with a choice among the 2n competing transitions,
551 * ending when the chosen one is a D1 (an event) rather than a D0 (a hidden
552 * phase change). One uniform for the holding time and one for the choice,
553 * per step, in that order.
554 */
555 double next_map_java(Rng& g) {
556 const std::size_t n = map_.order();
557 if (n == 1) {
558 // The reference's exponential shortcut: -log(u)/lambda, with plain
559 // log and not log1p, because Map_sample spells it that way.
560 const double lambda = map_.D1(0, 0);
561 return -std::log(g.aux.next_double()) / lambda;
562 }
563 if (!phase_known_) {
564 const std::vector<double> pie = mam::map_pie(map_);
565 double sum = 0.0;
566 const double r = g.aux.next_double();
567 phase_ = n - 1;
568 for (std::size_t i = 0; i < n; ++i) {
569 sum += pie[i];
570 if (r < sum) {
571 phase_ = i;
572 break;
573 }
574 }
575 phase_known_ = true;
576 }
577 double sample = 0.0;
578 std::vector<double> row(2 * n, 0.0);
579 bool go = true;
580 while (go) {
581 const double rate = -map_.D0(phase_, phase_);
582 sample += -std::log(g.aux.next_double()) / rate;
583 for (std::size_t k = 0; k < n; ++k) {
584 row[k] = map_.D0(phase_, k);
585 row[n + k] = map_.D1(phase_, k);
586 }
587 row[phase_] = 0.0; // the diagonal is the exit rate, not a target
588 std::size_t next_state = 2 * n - 1;
589 double sum = 0.0;
590 const double r = g.aux.next_double();
591 for (std::size_t j = 0; j < 2 * n; ++j) {
592 sum += row[j] / rate;
593 if (r < sum) {
594 if (j >= n) {
595 next_state = j - n;
596 go = false;
597 } else {
598 next_state = j;
599 go = true;
600 }
601 break;
602 }
603 }
604 if (next_state >= 2 * n - 1 && go) {
605 // The loop above fell through: the reference leaves nextState at
606 // its initialized 2n-1 and continues, which is a D1 transition.
607 next_state = n - 1;
608 go = false;
609 }
610 phase_ = (next_state < n) ? next_state : next_state - n;
611 }
612 return sample;
613 }
614
615 /**
616 * The 1-based mark of an arrival firing on transition (i, j), drawn with
617 * probability D1k(i,j)/D1_agg(i,j). Transcribes `Mmap_sample.MmapSampler.sampleMark`,
618 * including its fallbacks: an unmarked process reports mark 1, and so does a
619 * transition on which every block is zero.
620 */
621 int sample_mark(std::size_t i, std::size_t j, Rng& g) {
622 const std::size_t C = mark_.size();
623 if (C == 0) return 1;
624 double total = 0.0;
625 for (std::size_t c = 0; c < C; ++c) total += std::max(0.0, mark_[c](i, j));
626 if (!(total > 0.0)) return 1;
627 const double r = g.aux.next_double() * total;
628 double sum = 0.0;
629 for (std::size_t c = 0; c < C; ++c) {
630 sum += std::max(0.0, mark_[c](i, j));
631 if (r < sum) return static_cast<int>(c) + 1;
632 }
633 return static_cast<int>(C);
634 }
635
636 /**
637 * One marked interval, transcribed from `Mmap_sample.MmapSampler.next`.
638 *
639 * The interval walk is the unmarked one over the aggregate pair; the mark is a
640 * SECOND draw taken on the transition that ended it, so an unmarked and a marked
641 * process of the same pair do not share a sample path past the first arrival.
642 * That is the reference's own ordering and must not be reordered to save a draw.
643 *
644 * Unlike `next_map_java` this does NOT carry the reference's fall-through repair:
645 * `MmapSampler` leaves nextState at its initialized 2n-1 and reads it as a D1
646 * transition into phase n-1, which is what is transcribed here.
647 */
648 double next_mmap_java(Rng& g) {
649 const std::size_t n = map_.order();
650 if (n == 1) {
651 const double lambda = map_.D1(0, 0);
652 const double time = -std::log(g.aux.next_double()) / lambda;
653 last_mark_ = sample_mark(0, 0, g);
654 return time;
655 }
656 if (!phase_known_) {
657 const std::vector<double> pie = mam::map_pie(map_);
658 double sum = 0.0;
659 const double r = g.aux.next_double();
660 phase_ = n - 1;
661 for (std::size_t i = 0; i < n; ++i) {
662 sum += pie[i];
663 if (r < sum) {
664 phase_ = i;
665 break;
666 }
667 }
668 phase_known_ = true;
669 }
670 double sample = 0.0;
671 std::vector<double> row(2 * n, 0.0);
672 while (true) {
673 const double rate = -map_.D0(phase_, phase_);
674 sample += -std::log(g.aux.next_double()) / rate;
675 for (std::size_t k = 0; k < n; ++k) {
676 row[k] = map_.D0(phase_, k);
677 row[n + k] = map_.D1(phase_, k);
678 }
679 row[phase_] = 0.0; // the diagonal is the exit rate, not a target
680 std::size_t next_state = 2 * n - 1;
681 double sum = 0.0;
682 const double r = g.aux.next_double();
683 for (std::size_t j = 0; j < 2 * n; ++j) {
684 sum += row[j] / rate;
685 if (r < sum) {
686 next_state = j;
687 break;
688 }
689 }
690 if (next_state >= n) {
691 const std::size_t dest = next_state - n;
692 last_mark_ = sample_mark(phase_, dest, g);
693 phase_ = dest;
694 return sample;
695 }
696 phase_ = next_state;
697 }
698 }
699
700 /**
701 * One BATCH interval, transcribed from `Map_sample.BmapSampler.next`.
702 *
703 * Unlike the marked walk, the batch label is NOT a second draw: the block that
704 * fires decides both the destination phase and how many jobs the epoch releases,
705 * so the row scanned is D0 followed by the batch blocks BATCH-MAJOR then
706 * destination, which is the reference's own `row[nphases + b*nphases + k]`
707 * indexing. Reordering it would keep the same law and lose the sample path.
708 *
709 * The one-phase case is the reference's fast path and takes TWO draws: the
710 * exponential interval, then the batch size against the per-batch rates.
711 */
712 double next_bmap_java(Rng& g) {
713 const std::size_t n = map_.order();
714 const std::size_t B = batch_.size();
715 last_batch_ = 1;
716 if (n == 1) {
717 const double lambda = map_.D1(0, 0);
718 const double time = -std::log(g.aux.next_double()) / lambda;
719 double total = 0.0;
720 for (std::size_t b = 0; b < B; ++b) total += batch_[b](0, 0);
721 if (total > 0.0) {
722 const double r = g.aux.next_double() * total;
723 double cum = 0.0;
724 for (std::size_t b = 0; b < B; ++b) {
725 cum += batch_[b](0, 0);
726 if (r < cum) {
727 last_batch_ = static_cast<int>(b) + 1;
728 break;
729 }
730 }
731 }
732 return time;
733 }
734 if (!phase_known_) {
735 const std::vector<double> pie = mam::map_pie(map_);
736 double sum = 0.0;
737 const double r = g.aux.next_double();
738 phase_ = n - 1;
739 for (std::size_t i = 0; i < n; ++i) {
740 sum += pie[i];
741 if (r < sum) {
742 phase_ = i;
743 break;
744 }
745 }
746 phase_known_ = true;
747 }
748 const std::size_t cols = n + B * n;
749 std::vector<double> row(cols, 0.0);
750 double sample = 0.0;
751 while (true) {
752 const double rate = -map_.D0(phase_, phase_);
753 sample += -std::log(g.aux.next_double()) / rate;
754 for (std::size_t k = 0; k < n; ++k) row[k] = map_.D0(phase_, k);
755 row[phase_] = 0.0; // the diagonal is the exit rate, not a target
756 for (std::size_t b = 0; b < B; ++b)
757 for (std::size_t k = 0; k < n; ++k) row[n + b * n + k] = batch_[b](phase_, k);
758 std::size_t next_state = phase_;
759 bool fired = false;
760 double sum = 0.0;
761 const double r = g.aux.next_double();
762 for (std::size_t j = 0; j < cols; ++j) {
763 sum += row[j] / rate;
764 if (r < sum) {
765 if (j < n) {
766 next_state = j;
767 } else {
768 const std::size_t idx = j - n;
769 last_batch_ = static_cast<int>(idx / n) + 1;
770 next_state = idx % n;
771 fired = true;
772 }
773 break;
774 }
775 }
776 phase_ = next_state;
777 if (fired) return sample;
778 }
779 }
780
781 double next_markovian(Rng& g) {
782 std::vector<double> out;
783 if (type_ == lang::ProcessType::MAP || type_ == lang::ProcessType::MMPP2 ||
784 type_ == lang::ProcessType::PH || type_ == lang::ProcessType::APH ||
787 return next_map_java(g);
788 }
790 return next_mmap_java(g);
791 }
792 if (type_ == lang::ProcessType::BMAP && !batch_.empty()) {
793 return next_bmap_java(g);
794 }
795 if (type_ == lang::ProcessType::ME) {
796 // An ME is a RENEWAL process: the entry law is the same at every
797 // sample, so the inversion table built in `load_map` serves them
798 // all. Routing it through `rap_sample` instead cost a matrix
799 // exponential per bisection step and ran 140x slower than the Java
800 // engine, which is a wall-clock timeout rather than a slow answer.
801 return me_->next(g.mc);
802 }
803 if (type_ == lang::ProcessType::RAP) {
804 // A RAP's state is the real-valued ENTRY LAW, not a discrete phase,
805 // so it is chained through `a_out` rather than recovered from a
806 // trace.
807 std::vector<double> a_next;
808 out = mam::rap_sample(map_, 1, g.mc, entry_, &a_next);
809 entry_ = a_next;
810 } else {
811 mam::SampleTrace tr;
812 std::vector<double> start;
813 if (has_phase_ && phase_known_) {
814 start.assign(map_.order(), 0.0);
815 start[phase_] = 1.0;
816 }
817 out = mam::map_sample(map_, 1, g.mc, start, &tr);
818 if (has_phase_ && !tr.last.empty()) {
819 phase_ = tr.last[0];
820 phase_known_ = true;
821 }
822 }
823 if (out.empty()) return 0.0;
824 // A discrete-time MAP samples a SLOT COUNT, which is already the
825 // interval on the lattice; the continuous families sample the interval
826 // directly. Neither needs rescaling here.
827 return out[0];
828 }
829
830 template <class T>
831 void load_schedule(const lang::Distrib<T>& d) {
832 if (!d.has_schedule())
833 throw InputError("SolverLDES (native engine): the time-inhomogeneous process at " +
834 where_ + " carries no segment schedule");
835 for (const T& b : d.sched_bp) bp_.push_back(num_traits<T>::to_double(b));
836 if (bp_.size() != d.sched_D0.size() + 1)
837 throw InputError("SolverLDES (native engine): the schedule at " + where_ +
838 " has a boundary vector that does not bound its segments");
839 for (std::size_t k = 0; k < d.sched_D0.size(); ++k) {
840 const std::size_t H = d.sched_D0[k].rows();
841 mam::Map<double> seg;
842 seg.D0 = Matrix<double>(H, H, 0.0);
843 seg.D1 = Matrix<double>(H, H, 0.0);
844 for (std::size_t a = 0; a < H; ++a)
845 for (std::size_t b2 = 0; b2 < H; ++b2) {
846 seg.D0(a, b2) = num_traits<T>::to_double(d.sched_D0[k](a, b2));
847 seg.D1(a, b2) = num_traits<T>::to_double(d.sched_D1[k](a, b2));
848 }
849 segs_.push_back(seg);
850 }
851 // The per-mark blocks, segment-major here (`seg_mark_[k][c]`) because the walk
852 // scans one segment at a time; the wire and the Distrib are mark-major.
853 seg_mark_.clear();
854 if (d.has_marked_schedule()) {
855 const std::size_t C = d.sched_Dmark.size();
856 for (std::size_t k = 0; k < d.sched_D0.size(); ++k) {
857 const std::size_t H = d.sched_D0[k].rows();
858 std::vector<Matrix<double>> per_mark;
859 for (std::size_t c = 0; c < C; ++c) {
860 Matrix<double> m(H, H, 0.0);
861 for (std::size_t a = 0; a < H; ++a)
862 for (std::size_t b2 = 0; b2 < H; ++b2)
863 m(a, b2) = num_traits<T>::to_double(d.sched_Dmark[c][k](a, b2));
864 per_mark.push_back(m);
865 }
866 seg_mark_.push_back(per_mark);
867 }
868 }
869 // The batch blocks, transposed the same way: the Distrib is mark-major then
870 // batch then segment, the walk is segment-major, so this is
871 // `seg_batch_[k][c][b]`. `seg_mark_` stays the batch-AGGREGATED per-mark
872 // schedule and `segs_[k].D1` the aggregate over marks as well, so a consumer
873 // that ignores batches walks exactly the MMAPt this hides down to.
874 seg_batch_.clear();
875 if (d.has_batch_schedule()) {
876 const std::size_t C = d.sched_Dbatch.size();
877 const std::size_t B = d.sched_Dbatch[0].size();
878 for (std::size_t k = 0; k < d.sched_D0.size(); ++k) {
879 const std::size_t H = d.sched_D0[k].rows();
880 std::vector<std::vector<Matrix<double>>> per_mark;
881 for (std::size_t c = 0; c < C; ++c) {
882 std::vector<Matrix<double>> per_batch;
883 for (std::size_t b = 0; b < B; ++b) {
884 Matrix<double> m(H, H, 0.0);
885 for (std::size_t a = 0; a < H; ++a)
886 for (std::size_t b2 = 0; b2 < H; ++b2)
887 m(a, b2) = num_traits<T>::to_double(d.sched_Dbatch[c][b][k](a, b2));
888 per_batch.push_back(m);
889 }
890 per_mark.push_back(per_batch);
891 }
892 seg_batch_.push_back(per_mark);
893 }
894 }
895 cyclic_ = d.sched_cyclic;
896 has_schedule_ = true;
897 phase_ = 0;
898 if (!(mean_ > 0.0)) mean_ = num_traits<T>::to_double(d.mean);
899 if (!(mean_ > 0.0) && !segs_.empty()) mean_ = num_traits<double>::to_double(
900 mam::map_mean(segs_[0]));
901 }
902
903 double period() const { return bp_.back() - bp_.front(); }
904
905 /**
906 * Position of t within one period, measured from bp_.front(), or -1 past a
907 * non-cyclic horizon.
908 *
909 * The walk below carries this REDUCED offset rather than the wall clock: it
910 * stays bounded by the period however far the simulation has run, which is
911 * what keeps a boundary crossing representable.
912 */
913 double offset_at(double t) const {
914 double offset = t - bp_.front();
915 const double per = period();
916 if (cyclic_) {
917 offset = std::fmod(offset, per);
918 if (offset < 0.0) offset += per;
919 } else if (offset < 0.0 || offset >= per) {
920 return -1.0;
921 }
922 return offset;
923 }
924
925 /** Index of the segment holding an offset already reduced to [0, period). */
926 int segment_of(double offset) const {
927 const double pos = bp_.front() + offset;
928 for (std::size_t k = 0; k + 1 < bp_.size(); ++k)
929 if (pos < bp_[k + 1]) return static_cast<int>(k);
930 return static_cast<int>(segs_.size()) - 1;
931 }
932
933 /** Index of the segment in force at t, or -1 past a non-cyclic horizon. */
934 int segment_at(double t) const {
935 const double offset = offset_at(t);
936 return (offset < 0.0) ? -1 : segment_of(offset);
937 }
938
939 /**
940 * One interval of a MAPt / PHt / NHPP, started at `from`.
941 *
942 * The walk advances through the phase process and STOPS AT EVERY SEGMENT
943 * BOUNDARY, resampling the holding time under the new generator. That is
944 * what makes the schedule piecewise-constant rather than merely
945 * time-averaged: an exponential holding time drawn under one segment's rate
946 * and allowed to run past the boundary would carry the old rate into the
947 * new segment, which is exactly the approximation the family exists to
948 * avoid. Transcribes `sampleMAPtInterarrival`.
949 */
950 double schedule_sample(Rng& g, double from) {
951 const std::size_t H = segs_[0].order();
952 double offset = offset_at(from);
953 if (offset < 0.0) return 0.0; // past a non-cyclic horizon: no more events
954 std::size_t idx = static_cast<std::size_t>(segment_of(offset));
955 double elapsed = 0.0;
956 for (int guard = 0; guard < 1000000; ++guard) {
957 const double to_boundary = (bp_[idx + 1] - bp_.front()) - offset;
958 const mam::Map<double>& seg = segs_[idx];
959 const double total = -seg.D0(phase_, phase_);
960 // An absorbing phase never fires, so it always reaches the boundary.
961 const double holding = (total > 0.0) ? -std::log(uniform01(g)) / total
962 : std::numeric_limits<double>::infinity();
963 if (holding >= to_boundary) {
964 ++idx;
965 if (idx >= segs_.size()) {
966 if (!cyclic_) return 0.0;
967 idx = 0;
968 }
969 elapsed += to_boundary;
970 // LAND EXACTLY ON THE BOUNDARY by taking the next segment's own
971 // offset. Adding to_boundary to a running position instead would
972 // stall the walk: far from the schedule origin the sliver left by
973 // the modulo is below one ulp of that position, so the addition
974 // does not move it and the loop spins on the same boundary until
975 // the guard trips -- a hang, not a wrong number, and one that only
976 // shows up once the simulation clock has run far enough.
977 offset = bp_[idx] - bp_.front();
978 continue;
979 }
980 elapsed += holding;
981 offset += holding;
982 // The transitions competing out of the current phase, ARRIVALS
983 // FIRST: a D1 entry ends the interval, a D0 entry only moves phase.
984 const double u = uniform01(g) * total;
985 double cum = 0.0;
986 int chosen = -1;
987 // A MARKED schedule splits the arrival arm across the mark blocks at NO EXTRA
988 // DRAW: the scan runs destination-major and mark-minor, so the running total
989 // after all marks of a destination equals the aggregate total after that
990 // destination. The winning destination is therefore the one the unmarked walk
991 // would pick for the same uniform, which is what makes a K = 1 MMAPt reproduce
992 // the MAPt with the same matrices sample path for sample path.
993 if (!seg_mark_.empty()) {
994 const std::vector<Matrix<double>>& marks = seg_mark_[idx];
995 // The BATCH axis goes INSIDE the mark axis, so dropping it (every
996 // batch size 1) leaves the scan order untouched and a B = 1 BMMAPt
997 // reproduces the MMAPt sample path exactly as a K = 1 MMAPt
998 // reproduces the MAPt. Neither label costs an extra draw.
999 const bool batched_here = !seg_batch_.empty();
1000 const std::size_t B = batched_here ? seg_batch_[idx][0].size() : std::size_t(1);
1001 for (std::size_t j = 0; j < H; ++j) {
1002 for (std::size_t c = 0; c < marks.size(); ++c) {
1003 if (!batched_here) {
1004 cum += marks[c](phase_, j);
1005 if (u < cum) {
1006 last_mark_ = static_cast<int>(c) + 1;
1007 last_batch_ = 1;
1008 phase_ = j;
1009 return elapsed;
1010 }
1011 continue;
1012 }
1013 for (std::size_t b = 0; b < B; ++b) {
1014 cum += seg_batch_[idx][c][b](phase_, j);
1015 if (u < cum) {
1016 last_mark_ = static_cast<int>(c) + 1;
1017 last_batch_ = static_cast<int>(b) + 1;
1018 phase_ = j;
1019 return elapsed;
1020 }
1021 }
1022 }
1023 }
1024 for (std::size_t j = 0; j < H; ++j) {
1025 if (j == phase_) continue;
1026 cum += seg.D0(phase_, j);
1027 if (u < cum) {
1028 chosen = static_cast<int>(H + j);
1029 break;
1030 }
1031 }
1032 if (chosen < 0) {
1033 // Rounding shortfall: attribute the draw to the last positive arrival
1034 // entry, as the unmarked walk below does.
1035 for (std::size_t j = H; j-- > 0;)
1036 for (std::size_t c = marks.size(); c-- > 0;) {
1037 if (!batched_here) {
1038 if (marks[c](phase_, j) > 0.0) {
1039 last_mark_ = static_cast<int>(c) + 1;
1040 last_batch_ = 1;
1041 phase_ = j;
1042 return elapsed;
1043 }
1044 continue;
1045 }
1046 for (std::size_t b = B; b-- > 0;)
1047 if (seg_batch_[idx][c][b](phase_, j) > 0.0) {
1048 last_mark_ = static_cast<int>(c) + 1;
1049 last_batch_ = static_cast<int>(b) + 1;
1050 phase_ = j;
1051 return elapsed;
1052 }
1053 }
1054 last_mark_ = 1;
1055 last_batch_ = 1;
1056 return elapsed;
1057 }
1058 phase_ = static_cast<std::size_t>(chosen) - H;
1059 continue;
1060 }
1061 for (std::size_t j = 0; j < 2 * H; ++j) {
1062 const double w = (j < H) ? seg.D1(phase_, j)
1063 : ((j - H == phase_) ? 0.0 : seg.D0(phase_, j - H));
1064 cum += w;
1065 if (u < cum) {
1066 chosen = static_cast<int>(j);
1067 break;
1068 }
1069 }
1070 if (chosen < 0)
1071 for (std::size_t j = 2 * H; j-- > 0;) {
1072 const double w = (j < H) ? seg.D1(phase_, j)
1073 : ((j - H == phase_) ? 0.0 : seg.D0(phase_, j - H));
1074 if (w > 0.0) {
1075 chosen = static_cast<int>(j);
1076 break;
1077 }
1078 }
1079 if (chosen < static_cast<int>(H)) {
1080 phase_ = static_cast<std::size_t>(chosen);
1081 return elapsed;
1082 }
1083 phase_ = static_cast<std::size_t>(chosen) - H;
1084 }
1085 return 0.0;
1086 }
1087
1089 std::string where_;
1090 double mean_ = 0.0, scv_ = 1.0, rate_ = 0.0;
1091 double a_ = 0.0, b_ = 0.0; ///< the family's two shape/scale slots
1092 int erlang_k_ = 0; ///< phases of an Erlang, 0 when not recovered
1093 double erlang_rate_ = 0.0; ///< per-phase rate of an Erlang
1094 std::vector<double> trace_;
1095 std::size_t trace_idx_ = 0;
1096 mam::Map<double> map_;
1097 std::vector<double> entry_; ///< RAP entry law, chained across samples
1098 std::shared_ptr<mam::MeSampler<double>> me_; ///< ME inversion table, built once
1099 /// Per-SEGMENT, per-mark arrival blocks of a marked schedule; empty otherwise.
1100 std::vector<std::vector<Matrix<double>>> seg_mark_;
1101 /// Per-mark arrival blocks D1k; empty for an unmarked process.
1102 std::vector<Matrix<double>> mark_;
1103 /// Per-BATCH-SIZE blocks of a stationary BMAP, `batch_[b]` releasing b+1 jobs;
1104 /// empty for every other family. The schedule twin is `seg_batch_`.
1105 std::vector<Matrix<double>> batch_;
1106 /// Per-SEGMENT, per-mark, per-batch blocks of a BMMAPt; empty otherwise.
1107 std::vector<std::vector<std::vector<Matrix<double>>>> seg_batch_;
1108 /// The 1-based mark of the interval last returned; 0 before the first draw.
1109 int last_mark_ = 0;
1110 /// How many jobs the epoch last returned releases; 0 before the first draw.
1111 int last_batch_ = 0;
1112 bool has_phase_ = false, phase_known_ = false;
1113 // The time-inhomogeneous schedule: boundaries, per-segment (D0,D1), and
1114 // whether the schedule repeats past its last boundary.
1115 bool has_schedule_ = false, cyclic_ = false;
1116 std::vector<double> bp_;
1117 std::vector<mam::Map<double>> segs_;
1118 std::size_t phase_ = 0;
1119};
1120
1121} // namespace engine
1122} // namespace ldes
1123} // namespace line
1124
1125#endif // LINE_SOLVERS_LDES_LDES_SAMPLER_H
InputError(const std::string &what)
Definition error.h:39
UnsupportedError(const std::string &what)
Definition error.h:51
double next_at(Rng &g, double from)
One variate from a TIME-INHOMOGENEOUS process, given the instant it starts at.
double resolve_zero_atom(double v) const
The reference's resolveZeroAtom: the discrete laws supported on {0,1,...} can return a zero-length in...
lang::ProcessType type() const
bool batched() const
True when the process carries its OWN batch sizes: a BMAP or a BMMAPt.
int last_mark() const
The 1-based mark of the interval last returned by next/next_at, or 0 when this process carries none.
int phase() const
The phase the process is in, for a caller that must carry it across a preemption or across successive...
int last_batch() const
How many jobs the epoch last returned by next/next_at releases, or 1 when this process carries no bat...
Sampler(const lang::Distrib< T > &d, const std::string &where)
Build the generator of d; the (station, class) names are for diagnostics.
double next(Rng &g)
One variate.
java.util.Random, the 48-bit LCG of the Java Language Specification.
Definition rng_ssj.h:168
SSJ's umontreal.ssj.rng.MRG32k3a, state and all.
Definition rng_ssj.h:69
double next_double()
SSJ's nextValue(): the combined generator, returning a double in (0, 1).
Definition rng_ssj.h:90
void set_seed_offset(long long seed, long long offset)
Convenience for the engine's {seed+off, ..., seed+off+5} idiom.
Definition rng_ssj.h:80
The exception types the port throws.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
The variate layer of the Java LDES engine, reproduced: SSJ's randvar generators as inverse-CDF functi...
Sample the inter-arrival times of a MAP, a RAP or a matrix exponential.
Dense matrix and non-owning view.
Sample a matrix exponential by numerical inversion of its exact CDF.
ProcessType
Distribution kinds, with the values of MATLAB ProcessType.
Definition lang_types.h:485
@ BMMAPT
BMMAPT crosses the BATCH axis with the two above: a block is indexed by segment, mark and batch size,...
Definition lang_types.h:574
@ MPH
The MARKED families, MATLAB's ProcessType.m:36-38.
Definition lang_types.h:559
@ NHPP
The time-INHOMOGENEOUS families of Ko and Pender (ORL 45, 2017): an NHPP is a rate schedule lambda(t)...
Definition lang_types.h:538
bool process_is_marked_stationary(ProcessType p)
True when the type carries PER-MARK arrival blocks, i.e.
Definition lang_types.h:679
double uniform01(Rng &g)
Uniform on (0,1) off the MRG stream; next_double never returns 0.
double exponential_inverse(double lambda, double u)
ExponentialDist.inverseF(lambda, u), SSJ's log1p spelling.
double uniform_int_inverse(double lo, double width, double u)
UniformIntDist.inverseF(i, j, u), written over the lower bound and the WIDTH w = j - i + 1 so that a ...
double binomial_inverse(int n, double p, double u)
BinomialDist.inverseF(n, p, u), by the same forward inversion.
double uniform_inverse(double a, double b, double u)
UniformDist.inverseF(a, b, u).
double weibull_inverse(double alpha, double lambda, double delta, double u)
WeibullDist.inverseF(alpha, lambda, delta, u).
double pareto_inverse(double alpha, double beta, double u)
ParetoDist.inverseF(alpha, beta, u) = beta (1-u)^(-1/alpha).
double gamma_inverse(double alpha, double lambda, double u)
GammaDist.inverseF(alpha, lambda, u): the quantile of a Gamma of shape alpha and RATE lambda,...
double bernoulli_inverse(double p, double u)
BernoulliDist.inverseF(p, u): 1 when u exceeds 1 - p.
double lognormal_inverse(double mu, double sigma, double u)
LognormalDist.inverseF(mu, sigma, u) = exp(mu + sigma Phi^-1(u)).
double poisson_inverse(double lambda, double u)
PoissonDist.inverseF(lambda, u): the smallest k whose cdf reaches u.
T map_mean(const Map< T > &m)
Mean inter-arrival time, 1/lambda.
Definition map_moment.h:101
std::vector< T > map_pie(const Map< T > &m)
Phase distribution seen by an arriving job, pie = pi D1 / (pi D1 e).
Definition map_moment.h:89
std::vector< T > rap_sample(const Map< T > &m, std::size_t n, pfqn::McRng &rng, const std::vector< T > &a0=std::vector< T >(), std::vector< T > *a_out=0)
Sample a RAP or a matrix exponential by inverse transform.
Definition map_sample.h:249
std::vector< T > map_sample(const Map< T > &m, std::size_t n, pfqn::McRng &rng, const std::vector< T > &pie0=std::vector< T >(), SampleTrace *trace=0)
Sample the inter-arrival times of a MAP, a RAP or a matrix exponential.
Definition map_sample.h:98
std::mt19937_64 McRng
The generator type every Monte Carlo entry point in this tree accepts.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
The two random number generators the Java LDES engine draws from, reproduced exactly: SSJ's MRG32k3a ...
Matrix< T > D0
The (D0,D1) pair when the type carries one directly.
Definition lang_types.h:853
std::vector< Matrix< T > > Dmark
MMAP per-class D1 blocks / BMAP per-batch-size blocks; empty otherwise.
Definition lang_types.h:863
std::vector< T > trace
Replayer / Trace samples; empty for every other type.
Definition lang_types.h:828
static constexpr double Immediate
Rate of an Immediate distribution; its mean is 1/Immediate = 1e-8.
Definition lang_types.h:766
The engine's randomness, in the SHAPE the Java engine uses it.
Rng(long long seed, long long stream_offset, long long aux_offset)
A run seed and the TWO offsets the reference derives, one per generator.
rng::JavaRandom aux
java.util.Random: the Markovian samplers
Rng(long long seed, long long offset)
The common case, where the reference uses one offset for both.
rng::Mrg32k3a stream
SSJ MRG32k3a: every renewal family.
pfqn::McRng mc
the residual RAP/ME path, not yet aligned
Matrix< T > D1
Definition map_moment.h:55
Matrix< T > D0
Definition map_moment.h:54
static double to_double(const double &v)
Definition number.h:151