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 require_mean();
203 break;
205 if (!(mean_ >= 0.0) || !(mean_ <= 1.0))
206 throw InputError("SolverLDES (native engine): the Bernoulli at " + where_ +
207 " has a mean outside [0,1]");
208 break;
210 require_moments();
211 const double p = 1.0 - scv_ * mean_;
212 if (!(p > 0.0) || p > 1.0)
213 throw InputError("SolverLDES (native engine): the Binomial at " + where_ +
214 " has moments no (n,p) pair realises");
215 a_ = p;
216 b_ = std::floor(mean_ / p + 0.5);
217 break;
218 }
222 load_schedule(d);
223 break;
237 load_map(d);
238 break;
239 default:
240 throw UnsupportedError("SolverLDES (native engine): the process at " + where_ +
241 " is of a family this engine does not sample");
242 }
243 }
244
245 bool disabled() const { return type_ == lang::ProcessType::DISABLED; }
246 lang::ProcessType type() const { return type_; }
247 double mean() const { return mean_; }
248
249 /**
250 * One variate from a TIME-INHOMOGENEOUS process, given the instant it
251 * starts at.
252 *
253 * The families whose parameters depend on absolute time cannot be sampled
254 * from a duration alone, so this overload takes `from` and every other
255 * family ignores it. The engine calls it wherever a duration is drawn; a
256 * homogeneous process is unaffected.
257 */
258 double next_at(Rng& g, double from) {
259 if (!has_schedule_) return next(g);
260 return schedule_sample(g, from);
261 }
262
263 bool time_varying() const { return has_schedule_; }
264
265 /**
266 * One variate. Advances whatever phase the family carries.
267 *
268 * EVERY RENEWAL FAMILY BUT THE ERLANG COSTS EXACTLY ONE UNIFORM off the MRG
269 * stream, which is not an optimization but the reference's behaviour: SSJ's
270 * instance generators invert, and a family that drew twice would shift
271 * every later event in the run. The quantiles live in
272 * `ldes_ssj_variates.h` and are pinned there against SSJ's own output. The
273 * Erlang is the exception, and deliberately so: it costs k uniforms in both
274 * engines because the Gamma inversion SSJ used is not reproducible across
275 * JVM releases.
276 */
277 double next(Rng& g) {
278 switch (type_) {
280 // SSJ's ExponentialDist.inverseF is -log1p(-u)/lambda, which is
281 // NOT -log(u)/lambda: they differ in the last bits for small u,
282 // exactly where an interarrival time matters most.
283 return ssj::exponential_inverse(rate_, uniform01(g));
286 return mean_;
288 const double v = trace_[trace_idx_];
289 trace_idx_ = (trace_idx_ + 1) % trace_.size();
290 // The reference floors a non-positive trace entry rather than
291 // scheduling an event in the past.
292 return (v <= 0.0) ? 1e-9 : v;
293 }
295 return ssj::uniform_inverse(a_, b_, uniform01(g));
297 // b_ is the SCALE here; SSJ's GammaGen takes the RATE.
298 return ssj::gamma_inverse(a_, 1.0 / b_, uniform01(g));
300 // b_ is the SCALE; SSJ's WeibullGen takes lambda = 1/scale.
301 return ssj::weibull_inverse(a_, 1.0 / b_, 0.0, uniform01(g));
303 return ssj::lognormal_inverse(a_, b_, uniform01(g));
305 return ssj::pareto_inverse(a_, b_, uniform01(g));
307 if (a_ >= 1.0) {
308 uniform01(g); // keep the stream consumption independent of p
309 return 1.0;
310 }
311 return std::ceil(std::log(1.0 - uniform01(g)) / std::log(1.0 - a_));
312 }
314 return ssj::poisson_inverse(mean_, uniform01(g));
316 return ssj::bernoulli_inverse(mean_, uniform01(g));
318 return ssj::binomial_inverse(static_cast<int>(b_), a_, uniform01(g));
320 // An Erlang costs k uniforms, one per phase, which is the one
321 // renewal family that does not invert in a single draw. The
322 // reference used to invert the Gamma here, but that inversion
323 // is iterative and its last bits move between JVM releases, so
324 // a seeded run was not reproducible across JVMs; both engines
325 // now convolve exponentials instead.
326 if (erlang_k_ <= 0) return next_markovian(g);
327 double sum = 0.0;
328 for (int i = 0; i < erlang_k_; ++i)
329 sum += ssj::exponential_inverse(erlang_rate_, uniform01(g));
330 return sum;
331 }
332 default:
333 return next_markovian(g);
334 }
335 }
336
337 /**
338 * The phase the process is in, for a caller that must carry it across a
339 * preemption or across successive executions of an LQN entry. -1 when the
340 * family has no phase.
341 */
342 int phase() const { return has_phase_ ? static_cast<int>(phase_) : -1; }
343 void set_phase(int p) {
344 if (has_phase_ && p >= 0 && static_cast<std::size_t>(p) < map_.order()) {
345 phase_ = static_cast<std::size_t>(p);
346 phase_known_ = true;
347 }
348 }
349
350private:
351 void require_mean() const {
352 if (!(mean_ > 0.0) || !std::isfinite(mean_))
353 throw InputError("SolverLDES (native engine): the process at " + where_ +
354 " has a mean that is neither finite nor positive");
355 }
356 void require_moments() const {
357 require_mean();
358 if (!(scv_ > 0.0) || !std::isfinite(scv_))
359 throw InputError("SolverLDES (native engine): the process at " + where_ +
360 " has an SCV that is neither finite nor positive");
361 }
362
363 template <class T>
364 void load_map(const lang::Distrib<T>& d) {
365 if (d.D0.rows() == 0 || d.D0.rows() != d.D1.rows())
366 throw InputError("SolverLDES (native engine): the process at " + where_ +
367 " declares no (D0,D1) representation to sample");
368 const std::size_t K = d.D0.rows();
369 map_.D0 = Matrix<double>(K, K, 0.0);
370 map_.D1 = Matrix<double>(K, K, 0.0);
371 for (std::size_t i = 0; i < K; ++i)
372 for (std::size_t j = 0; j < K; ++j) {
373 map_.D0(i, j) = num_traits<T>::to_double(d.D0(i, j));
374 map_.D1(i, j) = num_traits<T>::to_double(d.D1(i, j));
375 }
376 // THE MOMENTS OF A (D0,D1) PROCESS COME FROM THE PAIR, not from the
377 // Distrib's own fields: `Distrib::map_dist` leaves mean 0 and SCV 1 as
378 // placeholders, so reading them would give this station a zero mean
379 // service time and a utilization computed against nothing.
380 if (!(mean_ > 0.0)) {
381 mean_ = num_traits<T>::to_double(mam::map_mean(map_));
382 if (!(mean_ > 0.0))
383 throw InputError("SolverLDES (native engine): the process at " + where_ +
384 " has a non-positive mean under its (D0,D1) representation");
385 }
386 // ONLY the genuinely correlated families carry a phase between samples.
387 // A PH, an Erlang, a HyperExp and a Coxian are RENEWAL: restarting them
388 // from the entry law each time is what makes successive services
389 // independent, and carrying the phase instead would introduce a
390 // correlation the model does not have.
391 has_phase_ = (type_ == lang::ProcessType::MAP || type_ == lang::ProcessType::MMPP2 ||
392 type_ == lang::ProcessType::RAP || type_ == lang::ProcessType::MMAP ||
394 // ONCE PER STATION, not once per event: the table costs a matrix
395 // exponential and a thousand row-vector products, and every service at
396 // this station then inverts by binary search.
397 if (type_ == lang::ProcessType::ME)
398 me_.reset(new mam::MeSampler<double>(map_));
399 if (type_ == lang::ProcessType::ERLANG) recover_erlang();
400 }
401
402 /**
403 * Recovers (k, lambda) from an Erlang's (D0,D1) so `next` can convolve k
404 * exponentials off the MRG stream instead of walking the phases off the
405 * auxiliary java.util.Random. The reference engine samples the same
406 * convolution, so the two engines then agree sample for sample on an
407 * Erlang model; walking the phases does not, because it draws 2k uniforms
408 * from a different stream. A cell that does not carry the bidiagonal
409 * equal-rate structure of an Erlang is left to the MAP walk rather than
410 * approximated.
411 */
412 void recover_erlang() {
413 erlang_k_ = 0;
414 const std::size_t n = map_.order();
415 if (n == 0) return;
416 const double lambda = -map_.D0(0, 0);
417 if (!(lambda > 0.0)) return;
418 const double tol = 1e-12 * lambda;
419 for (std::size_t i = 0; i < n; ++i) {
420 if (std::fabs(-map_.D0(i, i) - lambda) > tol) return;
421 for (std::size_t j = 0; j < n; ++j) {
422 if (j == i) continue;
423 const double want = (j == i + 1) ? lambda : 0.0;
424 if (std::fabs(map_.D0(i, j) - want) > tol) return;
425 }
426 // Only the last phase completes, and it completes into phase 0.
427 for (std::size_t j = 0; j < n; ++j) {
428 const double want = (i + 1 == n && j == 0) ? lambda : 0.0;
429 if (std::fabs(map_.D1(i, j) - want) > tol) return;
430 }
431 }
432 erlang_k_ = static_cast<int>(n);
433 erlang_rate_ = lambda;
434 }
435
436 /**
437 * One interval from a MAP, transcribed from `Map_sample.MapSampler.next`.
438 *
439 * THE GENERATOR IS java.util.Random, NOT the MRG stream. `MapSampleGen`
440 * extends SSJ's `RandomVariateGen` and is handed the stream like every other
441 * generator, but its `nextDouble` ignores it and calls
442 * `Map_sample.map_sample(D0, D1, 1, rng)` with the java.util.Random built
443 * beside it. Drawing this family off the MRG stream would both use the wrong
444 * numbers and desynchronize every renewal draw that follows.
445 *
446 * The walk itself: hold the phase across calls (it is only advanced here,
447 * so an idle server freezes its service process), draw the initial phase
448 * from map_pie on the first call, then alternate an exponential holding
449 * time at rate -D0(i,i) with a choice among the 2n competing transitions,
450 * ending when the chosen one is a D1 (an event) rather than a D0 (a hidden
451 * phase change). One uniform for the holding time and one for the choice,
452 * per step, in that order.
453 */
454 double next_map_java(Rng& g) {
455 const std::size_t n = map_.order();
456 if (n == 1) {
457 // The reference's exponential shortcut: -log(u)/lambda, with plain
458 // log and not log1p, because Map_sample spells it that way.
459 const double lambda = map_.D1(0, 0);
460 return -std::log(g.aux.next_double()) / lambda;
461 }
462 if (!phase_known_) {
463 const std::vector<double> pie = mam::map_pie(map_);
464 double sum = 0.0;
465 const double r = g.aux.next_double();
466 phase_ = n - 1;
467 for (std::size_t i = 0; i < n; ++i) {
468 sum += pie[i];
469 if (r < sum) {
470 phase_ = i;
471 break;
472 }
473 }
474 phase_known_ = true;
475 }
476 double sample = 0.0;
477 std::vector<double> row(2 * n, 0.0);
478 bool go = true;
479 while (go) {
480 const double rate = -map_.D0(phase_, phase_);
481 sample += -std::log(g.aux.next_double()) / rate;
482 for (std::size_t k = 0; k < n; ++k) {
483 row[k] = map_.D0(phase_, k);
484 row[n + k] = map_.D1(phase_, k);
485 }
486 row[phase_] = 0.0; // the diagonal is the exit rate, not a target
487 std::size_t next_state = 2 * n - 1;
488 double sum = 0.0;
489 const double r = g.aux.next_double();
490 for (std::size_t j = 0; j < 2 * n; ++j) {
491 sum += row[j] / rate;
492 if (r < sum) {
493 if (j >= n) {
494 next_state = j - n;
495 go = false;
496 } else {
497 next_state = j;
498 go = true;
499 }
500 break;
501 }
502 }
503 if (next_state >= 2 * n - 1 && go) {
504 // The loop above fell through: the reference leaves nextState at
505 // its initialized 2n-1 and continues, which is a D1 transition.
506 next_state = n - 1;
507 go = false;
508 }
509 phase_ = (next_state < n) ? next_state : next_state - n;
510 }
511 return sample;
512 }
513
514 double next_markovian(Rng& g) {
515 std::vector<double> out;
516 if (type_ == lang::ProcessType::MAP || type_ == lang::ProcessType::MMPP2 ||
517 type_ == lang::ProcessType::PH || type_ == lang::ProcessType::APH ||
520 return next_map_java(g);
521 }
522 if (type_ == lang::ProcessType::ME) {
523 // An ME is a RENEWAL process: the entry law is the same at every
524 // sample, so the inversion table built in `load_map` serves them
525 // all. Routing it through `rap_sample` instead cost a matrix
526 // exponential per bisection step and ran 140x slower than the Java
527 // engine, which is a wall-clock timeout rather than a slow answer.
528 return me_->next(g.mc);
529 }
530 if (type_ == lang::ProcessType::RAP) {
531 // A RAP's state is the real-valued ENTRY LAW, not a discrete phase,
532 // so it is chained through `a_out` rather than recovered from a
533 // trace.
534 std::vector<double> a_next;
535 out = mam::rap_sample(map_, 1, g.mc, entry_, &a_next);
536 entry_ = a_next;
537 } else {
538 mam::SampleTrace tr;
539 std::vector<double> start;
540 if (has_phase_ && phase_known_) {
541 start.assign(map_.order(), 0.0);
542 start[phase_] = 1.0;
543 }
544 out = mam::map_sample(map_, 1, g.mc, start, &tr);
545 if (has_phase_ && !tr.last.empty()) {
546 phase_ = tr.last[0];
547 phase_known_ = true;
548 }
549 }
550 if (out.empty()) return 0.0;
551 // A discrete-time MAP samples a SLOT COUNT, which is already the
552 // interval on the lattice; the continuous families sample the interval
553 // directly. Neither needs rescaling here.
554 return out[0];
555 }
556
557 template <class T>
558 void load_schedule(const lang::Distrib<T>& d) {
559 if (!d.has_schedule())
560 throw InputError("SolverLDES (native engine): the time-inhomogeneous process at " +
561 where_ + " carries no segment schedule");
562 for (const T& b : d.sched_bp) bp_.push_back(num_traits<T>::to_double(b));
563 if (bp_.size() != d.sched_D0.size() + 1)
564 throw InputError("SolverLDES (native engine): the schedule at " + where_ +
565 " has a boundary vector that does not bound its segments");
566 for (std::size_t k = 0; k < d.sched_D0.size(); ++k) {
567 const std::size_t H = d.sched_D0[k].rows();
568 mam::Map<double> seg;
569 seg.D0 = Matrix<double>(H, H, 0.0);
570 seg.D1 = Matrix<double>(H, H, 0.0);
571 for (std::size_t a = 0; a < H; ++a)
572 for (std::size_t b2 = 0; b2 < H; ++b2) {
573 seg.D0(a, b2) = num_traits<T>::to_double(d.sched_D0[k](a, b2));
574 seg.D1(a, b2) = num_traits<T>::to_double(d.sched_D1[k](a, b2));
575 }
576 segs_.push_back(seg);
577 }
578 cyclic_ = d.sched_cyclic;
579 has_schedule_ = true;
580 phase_ = 0;
581 if (!(mean_ > 0.0)) mean_ = num_traits<T>::to_double(d.mean);
582 if (!(mean_ > 0.0) && !segs_.empty()) mean_ = num_traits<double>::to_double(
583 mam::map_mean(segs_[0]));
584 }
585
586 double period() const { return bp_.back() - bp_.front(); }
587
588 /** Index of the segment in force at t, or -1 past a non-cyclic horizon. */
589 int segment_at(double t) const {
590 double offset = t - bp_.front();
591 const double per = period();
592 if (cyclic_) {
593 offset = std::fmod(offset, per);
594 if (offset < 0.0) offset += per;
595 } else if (offset < 0.0 || offset >= per) {
596 return -1;
597 }
598 const double pos = bp_.front() + offset;
599 for (std::size_t k = 0; k + 1 < bp_.size(); ++k)
600 if (pos < bp_[k + 1]) return static_cast<int>(k);
601 return static_cast<int>(segs_.size()) - 1;
602 }
603
604 /**
605 * One interval of a MAPt / PHt / NHPP, started at `from`.
606 *
607 * The walk advances through the phase process and STOPS AT EVERY SEGMENT
608 * BOUNDARY, resampling the holding time under the new generator. That is
609 * what makes the schedule piecewise-constant rather than merely
610 * time-averaged: an exponential holding time drawn under one segment's rate
611 * and allowed to run past the boundary would carry the old rate into the
612 * new segment, which is exactly the approximation the family exists to
613 * avoid. Transcribes `sampleMAPtInterarrival`.
614 */
615 double schedule_sample(Rng& g, double from) {
616 const std::size_t H = segs_[0].order();
617 double elapsed = 0.0, pos = from;
618 for (int guard = 0; guard < 1000000; ++guard) {
619 const int idx = segment_at(pos);
620 if (idx < 0) return 0.0; // past a non-cyclic horizon: no more arrivals
621 double offset = pos - bp_.front();
622 if (cyclic_) {
623 offset = std::fmod(offset, period());
624 if (offset < 0.0) offset += period();
625 }
626 const double to_boundary = (bp_[static_cast<std::size_t>(idx) + 1] - bp_.front()) - offset;
627 const mam::Map<double>& seg = segs_[static_cast<std::size_t>(idx)];
628 const double total = -seg.D0(phase_, phase_);
629 const bool last_segment =
630 (!cyclic_ && static_cast<std::size_t>(idx) + 1 == segs_.size());
631 if (!(total > 0.0)) {
632 if (last_segment) return 0.0;
633 elapsed += to_boundary;
634 pos += to_boundary;
635 continue;
636 }
637 const double holding = -std::log(uniform01(g)) / total;
638 if (holding >= to_boundary) {
639 if (last_segment) return 0.0;
640 elapsed += to_boundary;
641 pos += to_boundary;
642 continue;
643 }
644 elapsed += holding;
645 pos += holding;
646 // The transitions competing out of the current phase, ARRIVALS
647 // FIRST: a D1 entry ends the interval, a D0 entry only moves phase.
648 const double u = uniform01(g) * total;
649 double cum = 0.0;
650 int chosen = -1;
651 for (std::size_t j = 0; j < 2 * H; ++j) {
652 const double w = (j < H) ? seg.D1(phase_, j)
653 : ((j - H == phase_) ? 0.0 : seg.D0(phase_, j - H));
654 cum += w;
655 if (u < cum) {
656 chosen = static_cast<int>(j);
657 break;
658 }
659 }
660 if (chosen < 0)
661 for (std::size_t j = 2 * H; j-- > 0;) {
662 const double w = (j < H) ? seg.D1(phase_, j)
663 : ((j - H == phase_) ? 0.0 : seg.D0(phase_, j - H));
664 if (w > 0.0) {
665 chosen = static_cast<int>(j);
666 break;
667 }
668 }
669 if (chosen < static_cast<int>(H)) {
670 phase_ = static_cast<std::size_t>(chosen);
671 return elapsed;
672 }
673 phase_ = static_cast<std::size_t>(chosen) - H;
674 }
675 return 0.0;
676 }
677
679 std::string where_;
680 double mean_ = 0.0, scv_ = 1.0, rate_ = 0.0;
681 double a_ = 0.0, b_ = 0.0; ///< the family's two shape/scale slots
682 int erlang_k_ = 0; ///< phases of an Erlang, 0 when not recovered
683 double erlang_rate_ = 0.0; ///< per-phase rate of an Erlang
684 std::vector<double> trace_;
685 std::size_t trace_idx_ = 0;
686 mam::Map<double> map_;
687 std::vector<double> entry_; ///< RAP entry law, chained across samples
688 std::shared_ptr<mam::MeSampler<double>> me_; ///< ME inversion table, built once
689 bool has_phase_ = false, phase_known_ = false;
690 // The time-inhomogeneous schedule: boundaries, per-segment (D0,D1), and
691 // whether the schedule repeats past its last boundary.
692 bool has_schedule_ = false, cyclic_ = false;
693 std::vector<double> bp_;
694 std::vector<mam::Map<double>> segs_;
695 std::size_t phase_ = 0;
696};
697
698} // namespace engine
699} // namespace ldes
700} // namespace line
701
702#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.
lang::ProcessType type() const
int phase() const
The phase the process is in, for a caller that must carry it across a preemption or across successive...
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:483
@ NHPP
The time-INHOMOGENEOUS families of Ko and Pender (ORL 45, 2017): an NHPP is a rate schedule lambda(t)...
Definition lang_types.h:536
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 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.
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:759
std::vector< T > trace
Replayer / Trace samples; empty for every other type.
Definition lang_types.h:736
static constexpr double Immediate
Rate of an Immediate distribution; its mean is 1/Immediate = 1e-8.
Definition lang_types.h:674
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:125