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