LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
lang_types.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_LANG_LANG_TYPES_H
6#define LINE_LANG_LANG_TYPES_H
7
8/**
9 * @file
10 * @ingroup line_lang
11 * Enumerations and the minimal distribution descriptor shared by the model
12 * layer of the C++ port.
13 *
14 * The numeric values are the MATLAB ones (matlab/src/lang/constant), not a
15 * fresh numbering, because they cross the JSON boundary and appear in the
16 * dumps used as parity oracles. _kb/11 records that MATLAB and Python disagree
17 * on ProcessType numbering (MATLAB starts at EXP=0, Python at EXP=1); this port
18 * follows MATLAB, the reference implementation, so a numeric comparison against
19 * a MATLAB dump is meaningful and one against a Python dump is not -- compare
20 * by name there.
21 *
22 * SCOPE: the model layer added here exists to run SolverLN over a layered
23 * queueing network whose layers are solved by SolverMVA. It carries the
24 * scheduling disciplines, node kinds and precedence types that path reaches
25 * and REFUSES the rest by name rather than silently mapping them onto a
26 * neighbour, because a discipline that is silently treated as FCFS returns a
27 * plausible number that is wrong.
28 */
29
30#include <algorithm>
31#include <cmath>
32#include <cstddef>
33#include <functional>
34#include <limits>
35#include <memory>
36#include <string>
37#include <vector>
38
41#include "line/num/number.h"
42#include "line/util/error.h"
43#include "line/util/lu.h"
44#include "line/util/matrix.h"
45
46namespace line {
47namespace lang {
48
49/** Solver output metrics, with the numeric values of MATLAB `MetricType`. */
50enum class MetricType {
51 ResidT = 0,
52 RespT = 1,
54 QLen = 3,
55 QueueT = 4,
58 FJQLen = 7,
61 SysQLen = 10,
63 SysTput = 12,
64 Tput = 13,
65 ArvR = 14,
67 Util = 16,
72 Tard = 21,
73 SysTard = 22
74};
75
76/** Port of `MetricType.toText`. */
77inline const char* metric_to_text(MetricType metric) {
78 switch (metric) {
79 case MetricType::ResidT: return "Residence Time";
80 case MetricType::RespT: return "Response Time";
81 case MetricType::DropRate: return "Drop Rate";
82 case MetricType::QLen: return "Number of Customers";
83 case MetricType::QueueT: return "Queue Time";
84 case MetricType::FCRWeight: return "FCR Total Weight";
85 case MetricType::FCRMemOcc: return "FCR Memory Occupation";
86 case MetricType::FJQLen: return "Fork Join Number of Customers";
87 case MetricType::FJRespT: return "Fork Join Response Time";
88 case MetricType::RespTSink: return "Response Time per Sink";
89 case MetricType::SysQLen: return "System Number of Customers";
90 case MetricType::SysRespT: return "System Response Time";
91 case MetricType::SysTput: return "System Throughput";
92 case MetricType::Tput: return "Throughput";
93 case MetricType::ArvR: return "Arrival Rate";
94 case MetricType::TputSink: return "Throughput per Sink";
95 case MetricType::Util: return "Utilization";
96 case MetricType::TranQLen: return "Tran Number of Customers";
97 case MetricType::TranUtil: return "Tran Utilization";
98 case MetricType::TranTput: return "Tran Throughput";
99 case MetricType::TranRespT: return "Tran Response Time";
100 case MetricType::Tard: return "Tardiness";
101 case MetricType::SysTard: return "System Tardiness";
102 default: return "Unknown Metric";
103 }
104}
105
106/**
107 * The events a state can undergo, with the values of MATLAB EventType.
108 *
109 * An event is ACTIVE at the node that schedules it and PASSIVE at the node
110 * that receives it: a DEP at one station is the ARV at the next, and only the
111 * active half carries a rate. The passive half is marked with a rate of -1,
112 * which the generator assembly replaces with the active rate -- a convention
113 * that only reads as a sentinel because a rate can never be negative.
114 */
115enum class EventType {
116 INIT = -1, ///< the model is initialized, t = 0
117 LOCAL = 0, ///< dummy event, no state change outside the node
118 ARV = 1, ///< a job arrives
119 DEP = 2, ///< a job departs
120 PHASE = 3, ///< service advances a phase WITHOUT departing
121 READ = 4, ///< a cache item is read
122 STAGE = 5, ///< a random environment changes stage
123 ENABLE = 6, ///< an SPN mode becomes enabled
124 FIRE = 7, ///< an SPN mode fires
125 PRE = 8, ///< consume from a place or queue buffer, no server effect
126 POST = 9, ///< produce to a place or queue buffer
127 RENEGE = 10, ///< a waiting job abandons the queue (impatience)
128 RETRY = 11, ///< an orbiting job retries entry at a retrial station
129 SWITCH = 12, ///< a polling server advances its switchover timer
130 FAILURE = 13, ///< the server breaks down, going from up to down
131 REPAIR = 14, ///< the server is repaired, going from down to up, resuming
132 ///< the held job, which is why it emits no START
133 START = 15, ///< a job begins or resumes holding a server
134 PREEMPT = 16 ///< a job holding a server is pushed back into the buffer
135};
136// START and PREEMPT are instantaneous tags on the arc of the ARV or DEP that
137// causes them, never the active half of an sn.sync entry: no clock, no state,
138// no change to any numerical result. PREEMPT is spelled in full because PRE
139// already names the Petri-net pre-arc.
140
141inline const char* event_to_text(EventType e) {
142 switch (e) {
143 case EventType::ARV: return "ARV";
144 case EventType::DEP: return "DEP";
145 case EventType::PHASE: return "PHASE";
146 case EventType::READ: return "READ";
147 case EventType::LOCAL: return "LOCAL";
148 case EventType::STAGE: return "STAGE";
149 case EventType::ENABLE: return "ENABLE";
150 case EventType::FIRE: return "FIRE";
151 case EventType::PRE: return "PRE";
152 case EventType::POST: return "POST";
153 case EventType::RENEGE: return "RENEGE";
154 case EventType::RETRY: return "RETRY";
155 case EventType::SWITCH: return "SWITCH";
156 case EventType::FAILURE: return "FAILURE";
157 case EventType::REPAIR: return "REPAIR";
158 case EventType::START: return "START";
159 case EventType::PREEMPT: return "PREEMPT";
160 default: return "INIT";
161 }
162}
163
164/**
165 * G-network signal classes, with the values of MATLAB SignalType.
166 *
167 * A signal is not a job: it never joins a station, it acts on the jobs already
168 * there and is annihilated. REPLY is the odd one out -- it completes a
169 * synchronous call and then joins as an ordinary job.
170 */
171enum class SignalType {
172 REPLY = 0, ///< completes a synchronous call, releasing a held server
173 NEGATIVE = 1, ///< removes a batch of jobs (Gelenbe's negative customer)
174 CATASTROPHE = 2 ///< removes EVERY job at the station
175};
176
177/** Which job a negative signal removes, with the values of MATLAB RemovalPolicy. */
178enum class RemovalPolicy {
179 RANDOM = 0, ///< uniform over waiting AND in-service jobs
180 FCFS = 1, ///< the oldest waiting job; servers only once nobody waits
181 LCFS = 2 ///< the newest waiting job; servers only once nobody waits
182};
183
184/** Scheduling disciplines, with the values of MATLAB SchedStrategy. */
185enum class SchedStrategy {
186 INF = 0,
187 FCFS = 1,
188 LCFS = 2,
189 SIRO = 3,
190 SJF = 4,
191 LJF = 5,
192 PS = 6,
193 DPS = 7,
194 GPS = 8,
195 SEPT = 9,
196 LEPT = 10,
197 HOL = 11,
198 FORK = 12,
199 EXT = 13,
200 REF = 14,
201 LCFSPR = 15,
203 // The preemptive and priority families. PR resumes an interrupted job in
204 // the phase it held; PI restarts it from the entry phase, which is why the
205 // two cannot share an encoding: PR must carry the phase of EVERY preempted
206 // job, PI need not. FCFSPRIO is MATLAB's alias for HOL, so it is not a
207 // distinct enumerator here.
208 PSPRIO = 17,
211 LCFSPI = 20,
215 FCFSPR = 24,
216 FCFSPI = 25,
219 SRPT = 28,
221 EDD = 30,
222 EDF = 31,
223 LPS = 32,
224 PSJF = 33,
225 FB = 34,
226 LAS = 34, // Least Attained Service: an ALIAS of FB (same value), as SchedStrategy.LAS in MATLAB and Python
227 LRPT = 35,
228 SETF = 36,
229 FSP = 37,
230 PAS = 38,
231 OI = 39,
232 NONE = -1
233};
234
235inline const char* sched_to_text(SchedStrategy s) {
236 switch (s) {
237 case SchedStrategy::INF: return "inf";
238 case SchedStrategy::FCFS: return "fcfs";
239 case SchedStrategy::LCFS: return "lcfs";
240 case SchedStrategy::SIRO: return "siro";
241 case SchedStrategy::SJF: return "sjf";
242 case SchedStrategy::LJF: return "ljf";
243 case SchedStrategy::PS: return "ps";
244 case SchedStrategy::DPS: return "dps";
245 case SchedStrategy::GPS: return "gps";
246 case SchedStrategy::SEPT: return "sept";
247 case SchedStrategy::LEPT: return "lept";
248 case SchedStrategy::HOL: return "hol";
249 case SchedStrategy::FORK: return "fork";
250 case SchedStrategy::EXT: return "ext";
251 case SchedStrategy::REF: return "ref";
252 case SchedStrategy::LCFSPR: return "lcfspr";
253 case SchedStrategy::POLLING: return "polling";
254 case SchedStrategy::SRPT: return "srpt";
255 case SchedStrategy::LPS: return "lps";
256 case SchedStrategy::PSJF: return "psjf";
257 case SchedStrategy::FB: return "fb";
258 case SchedStrategy::LRPT: return "lrpt";
259 case SchedStrategy::SETF: return "setf";
260 case SchedStrategy::FSP: return "fsp";
261 case SchedStrategy::PSPRIO: return "psprio";
262 case SchedStrategy::DPSPRIO: return "dpsprio";
263 case SchedStrategy::GPSPRIO: return "gpsprio";
264 case SchedStrategy::LCFSPI: return "lcfspi";
265 case SchedStrategy::LCFSPRIO: return "lcfsprio";
266 case SchedStrategy::LCFSPRPRIO: return "lcfsprprio";
267 case SchedStrategy::LCFSPIPRIO: return "lcfspiprio";
268 case SchedStrategy::FCFSPR: return "fcfspr";
269 case SchedStrategy::FCFSPI: return "fcfspi";
270 case SchedStrategy::FCFSPRPRIO: return "fcfsprprio";
271 case SchedStrategy::FCFSPIPRIO: return "fcfspiprio";
272 case SchedStrategy::SRPTPRIO: return "srptprio";
273 case SchedStrategy::EDD: return "edd";
274 case SchedStrategy::EDF: return "edf";
275 case SchedStrategy::PAS: return "pas";
276 case SchedStrategy::OI: return "oi";
277 default: return "none";
278 }
279}
280
281/** Parse the `scheduling` attribute of an .lqnx processor or task. */
282inline SchedStrategy sched_from_lqnx(const std::string& s) {
283 if (s == "inf" || s == "INF") return SchedStrategy::INF;
284 if (s == "fcfs" || s == "FCFS") return SchedStrategy::FCFS;
285 if (s == "ps" || s == "PS") return SchedStrategy::PS;
286 if (s == "ref" || s == "REF") return SchedStrategy::REF;
287 if (s == "hol" || s == "HOL") return SchedStrategy::HOL;
288 // lqns spells preemptive priority resume (SCHEDULE_PPR) "pri"; "pp" is the
289 // stale lqn-core.xsd spelling, absent from the lqns 6.2.31 sources. It is
290 // preemptive, so it is FCFSPRPRIO and not the non-preemptive HOL
291 if (s == "pri" || s == "PRI" || s == "pp") return SchedStrategy::FCFSPRPRIO;
292 if (s == "rand" || s == "siro") return SchedStrategy::SIRO;
293 if (s == "sjf") return SchedStrategy::SJF;
294 if (s == "ljf") return SchedStrategy::LJF;
295 if (s == "lcfs") return SchedStrategy::LCFS;
296 if (s == "burst" || s == "poll") return SchedStrategy::FCFS;
297 // lqns completely fair scheduling: SchedStrategy.fromText maps it to GPS; <group> shares are not read there either
298 if (s == "cfs" || s == "CFS") return SchedStrategy::GPS;
299 throw UnsupportedError(ErrorKind{"Import"}, "lqn reader: unsupported scheduling discipline '" + s + "'");
300}
301
302/**
303 * The `scheduling` attribute an .lqnx processor or task carries for a strategy.
304 *
305 * The inverse of sched_from_lqnx over the disciplines the schema spells, and a
306 * refusal by name for every other one. It refuses rather than falling back on
307 * fcfs because the file is handed to an external solver: a task written as fcfs
308 * when the model says lcfspr is answered, not rejected, and the discipline
309 * would be lost inside a number that looks ordinary.
310 */
311inline std::string sched_to_lqnx(SchedStrategy s) {
312 switch (s) {
313 case SchedStrategy::INF: return "inf";
314 case SchedStrategy::FCFS: return "fcfs";
315 case SchedStrategy::PS: return "ps";
316 case SchedStrategy::REF: return "ref";
317 case SchedStrategy::HOL: return "hol";
318 case SchedStrategy::FCFSPRPRIO: return "pri";
319 case SchedStrategy::SIRO: return "rand";
320 case SchedStrategy::SJF: return "sjf";
321 case SchedStrategy::LJF: return "ljf";
322 case SchedStrategy::LCFS: return "lcfs";
323 default:
324 throw UnsupportedError(ErrorKind{"Export"},
325 std::string("the LQN XML schema has no spelling for scheduling discipline '") +
326 sched_to_text(s) + "'; it carries inf, fcfs, ps, ref, hol, pri, rand, sjf, ljf and lcfs");
327 }
328}
329
330/** Node kinds, with the values of MATLAB NodeType. */
331enum class NodeType {
332 Queue = 0,
334 Delay = 2,
337 Cache = 5,
339 Fork = 7,
340 Place = 8,
342 Region = 10,
343 Join = 11,
344 Sink = 12
345};
346
347/** Name of a node kind, for diagnostics. The JSON spelling lives in the writer. */
348inline const char* node_type_to_text(NodeType t) {
349 switch (t) {
350 case NodeType::Queue: return "Queue";
351 case NodeType::Source: return "Source";
352 case NodeType::Delay: return "Delay";
353 case NodeType::ClassSwitch: return "ClassSwitch";
354 case NodeType::Logger: return "Logger";
355 case NodeType::Cache: return "Cache";
356 case NodeType::Router: return "Router";
357 case NodeType::Fork: return "Fork";
358 case NodeType::Place: return "Place";
359 case NodeType::Transition: return "Transition";
360 case NodeType::Region: return "Region";
361 case NodeType::Join: return "Join";
362 case NodeType::Sink: return "Sink";
363 }
364 return "Unknown";
365}
366
367/** SPN transition timing, with the values of MATLAB TimingStrategy. */
368enum class TimingStrategy {
369 TIMED = 0, ///< fires after its firing distribution elapses
370 IMMEDIATE = 1 ///< fires with zero delay, resolved by weight and priority
371};
372
373/** Job class kinds, with the values of MATLAB JobClassType. */
374enum class JobClassType { OPEN = 0, CLOSED = 1 };
375
376/** Polling service disciplines, with the values of MATLAB PollingType. */
377enum class PollingType {
378 GATED = 0, ///< serve exactly the jobs present at the polling instant
379 EXHAUSTIVE = 1, ///< serve until the queue empties
380 KLIMITED = 2, ///< serve at most K per visit (K in pollingPar)
381 DECREMENTING = 3 ///< serve until the queue is one shorter than at arrival
382};
383
384/** Cache replacement policies, with the values of MATLAB ReplacementStrategy. */
386 RR = 0, ///< random replacement
387 FIFO = 1, ///< first in, first out
388 SFIFO = 2, ///< strict FIFO
389 LRU = 3, ///< least recently used
390 HLRU = 4, ///< h-LRU / LRU(m): h lists, promote i -> i+1 on a hit
391 CLIMB = 5, ///< move up one position on a hit (transposition rule)
392 QLRU = 6 ///< q-LRU: LRU with probabilistic admission on a miss
393};
394
395/** Routing strategies, with the values of MATLAB RoutingStrategy. */
396enum class RoutingStrategy {
397 RAND = 0,
398 PROB = 1,
401 JSQ = 4,
403 SQ = 6,
404 /** Krzesinski (1987) product-form state-dependent routing. */
405 SDR = 7,
407};
408
409inline const char* routing_to_text(RoutingStrategy r) {
410 switch (r) {
411 case RoutingStrategy::RAND: return "rand";
412 case RoutingStrategy::PROB: return "prob";
413 case RoutingStrategy::RROBIN: return "rrobin";
414 case RoutingStrategy::WRROBIN: return "wrrobin";
415 case RoutingStrategy::JSQ: return "jsq";
416 case RoutingStrategy::FIRING: return "firing";
417 case RoutingStrategy::SQ: return "sq";
418 case RoutingStrategy::SDR: return "sdr";
419 default: return "disabled";
420 }
421}
422
423/**
424 * Blocking and loss rules, with the values of MATLAB DropStrategy.
425 *
426 * WAITQ is -1 and is also the marker `refreshCapacity` writes where the rule is
427 * never consulted (an unbounded station, or a closed class), so it means two
428 * different things depending on the station's capacity; see the comment in
429 * refresh_capacity().
430 */
431enum class DropStrategy {
432 WAITQ = -1,
433 DROP = 1,
434 BAS = 2,
435 BBS = 3,
436 RSRD = 4,
439};
440
441/**
442 * Impatience kinds, with the values of MATLAB ImpatienceType.
443 *
444 * RENEGING is a timer a job started at a queue; BALKING is a decision taken
445 * BEFORE joining, on the state of the queue, and is parameterized by
446 * `Station::balking` rather than by a distribution; RETRIAL sends the job to an
447 * orbit and is parameterized by `RetrialParam`.
448 */
449enum class ImpatienceType { NONE = 0, RENEGING = 1, BALKING = 2, RETRIAL = 3 };
450
451/** Balking rules, with the values of MATLAB BalkingStrategy. */
452enum class BalkingStrategy { NONE = 0, QUEUE_LENGTH = 1, EXPECTED_WAIT = 2, COMBINED = 3 };
453
454/**
455 * How a heterogeneous station picks among its server types, MATLAB
456 * HeteroSchedPolicy. ORDER is the default: the declared order of the types.
457 */
458enum class HeteroSchedPolicy { ORDER = 0, ALIS = 1, ALFS = 2, FAIRNESS = 3, FSF = 4, RAIS = 5 };
459
460/**
461 * When a Place releases a served token, MATLAB DepartureDiscipline. NORMAL is
462 * the standard queueing-Petri-net rule (available on completion); FIFO holds
463 * it until every earlier arrival to the depository has been released.
464 */
465enum class DepartureDiscipline { NORMAL = 0, FIFO = 1 };
466
467/** Join rules, with the values of MATLAB JoinStrategy. */
468enum class JoinStrategy { STD = 1, PARTIAL = 2 };
469
470/** LQN element kinds, with the values of MATLAB LayeredNetworkElement. */
471enum class LqnElement { HOST = 0, TASK = 1, ENTRY = 2, ACTIVITY = 3, CALL = 4 };
472
473/** Call kinds, with the values of MATLAB CallType. */
474enum class CallType { NONE = 0, SYNC = 1, ASYNC = 2, FWD = 3 };
475
476/** Activity precedence kinds, with the values of MATLAB ActivityPrecedenceType. */
488
489/** Distribution kinds, with the values of MATLAB ProcessType. */
490enum class ProcessType {
491 EXP = 0,
493 HYPEREXP = 2,
494 PH = 3,
495 APH = 4,
496 MAP = 5,
498 DET = 7,
500 GAMMA = 9,
501 PARETO = 10,
502 MMPP2 = 11,
506 COX2 = 15,
511 /**
512 * A `Prior`: a weighted set of ALTERNATIVE distributions, or a density over
513 * a scalar parameter plus a factory from it. It is not a mixture -- each
514 * alternative is a separate model realization -- and only SolverUQ consumes
515 * it; every other solver refuses it through Feature::Prior.
516 */
517 PRIOR = 20,
521 BMAP = 24,
522 ME = 25,
523 RAP = 26,
525 ZIPF = 28,
526 DMAP = 29,
527 MMAP = 31,
529 /**
530 * The time-INHOMOGENEOUS families of Ko and Pender (ORL 45, 2017): an
531 * NHPP is a rate schedule lambda(t), a MAPt a (D0(t), D1(t)) schedule and a
532 * PHt an (alpha(t), S(t)) one, all piecewise constant on one breakpoint
533 * vector and optionally cyclic. The numeric values are MATLAB's
534 * (`ProcessType.m:41-43`).
535 *
536 * THEY CARRY A NOMINAL PAIR TOO. `Distrib::D0`/`D1` hold the width-weighted
537 * time average of the schedule, which is what `sn_schedule_nominal` returns
538 * as its first two outputs and what every consumer that has no notion of
539 * time -- the phase count, the rate, the fluid layout -- reads. The schedule
540 * itself lives in `sched_bp`/`sched_D0`/`sched_D1` beside it, and only a
541 * solver that integrates in time looks at it.
542 */
543 NHPP = 33,
544 MAPT = 34,
545 PHT = 35,
546 /**
547 * The MARKED families, MATLAB's `ProcessType.m:36-38`. A mark is a label
548 * carried by an event, and at a Source it selects the class of the arriving
549 * job (`Source.setMarkedArrival`, `sn.markidx`, `marked_classes`).
550 *
551 * MPH is the RENEWAL special case of MMAP: a PH with K marked exits,
552 * lowered by D0 = S and D1k = s_k*alpha. MMAPT is MMAP with a
553 * piecewise-constant matrix schedule, carried in `sched_Dmark` beside
554 * `sched_D0`/`sched_D1`. MPHT is the same lowering applied segment by
555 * segment, so it is STORED CONVERTED to MMAPT form exactly as a PHt is
556 * stored as its equivalent MAPt pair, and the marked schedule walk serves
557 * both.
558 *
559 * MPH keeps an id of its own rather than aliasing MMAP, so a solver that
560 * cannot honour a marked renewal process refuses it explicitly instead of
561 * inheriting MMAP's support. Every `procid == MMAP` test that should also
562 * accept a marked renewal process is therefore a MEMBERSHIP test.
563 */
564 MPH = 36,
565 MMAPT = 37,
566 MPHT = 38,
567 /**
568 * BMMAPT crosses the BATCH axis with the two above: a block is indexed by
569 * segment, mark and batch size, carried in `sched_Dbatch` beside
570 * `sched_Dmark`, and one epoch releases a batch of jobs that all carry the
571 * same mark. It reduces to MMAPT when every batch size is 1 and to BMAP
572 * when the schedule is flat.
573 *
574 * Like MPH it keeps an id of its own so a solver that cannot release
575 * batches refuses it by name instead of inheriting MMAPT's support, and
576 * every `procid == BMAP` test that should serve every batch family is
577 * therefore a MEMBERSHIP test, `process_is_batch`.
578 */
579 BMMAPT = 39,
580 /**
581 * A Gaussian, and the ONE family whose value is not MATLAB's, because
582 * MATLAB has none to copy: `ProcessType.m` stops at 35 and `Normal.m` is a
583 * `ContinuousDistribution` with no id at all, exactly as `Normal.java` and
584 * the python `Normal` have none. That is not an oversight in the reference
585 * -- a Gaussian has mass below zero, so it can never be a service or
586 * interarrival process and can never appear in `sn.proc`. It reaches this
587 * port only as the PARAMETER density of a continuous `Prior`, where it is
588 * read through `dist_cdf` and `dist_quantile` and never through
589 * `dist_to_map`.
590 *
591 * The value is deliberately far outside the ids MATLAB assigns (0..38 as of
592 * the marked families) so that it can never collide with one added later; `sn.procid` must never carry it, and
593 * `dist_to_map` refuses it by name rather than handing back the Erlang fit
594 * its default arm would otherwise produce for a law with negative support.
595 */
596 NORMAL = 100,
597 NONE = -1
598};
599
600/** The MATLAB ProcessType name, as `sn.procid` prints it. */
601inline const char* process_to_text(ProcessType p) {
602 switch (p) {
603 case ProcessType::EXP: return "Exp";
604 case ProcessType::ERLANG: return "Erlang";
605 case ProcessType::HYPEREXP: return "HyperExp";
606 case ProcessType::PH: return "PH";
607 case ProcessType::APH: return "APH";
608 case ProcessType::MAP: return "MAP";
609 case ProcessType::UNIFORM: return "Uniform";
610 case ProcessType::DET: return "Det";
611 case ProcessType::COXIAN: return "Coxian";
612 case ProcessType::GAMMA: return "Gamma";
613 case ProcessType::PARETO: return "Pareto";
614 case ProcessType::MMPP2: return "MMPP2";
615 case ProcessType::REPLAYER: return "Replayer";
616 case ProcessType::IMMEDIATE: return "Immediate";
617 case ProcessType::DISABLED: return "Disabled";
618 case ProcessType::COX2: return "Cox2";
619 case ProcessType::WEIBULL: return "Weibull";
620 case ProcessType::LOGNORMAL: return "Lognormal";
621 case ProcessType::DUNIFORM: return "DiscreteUniform";
622 case ProcessType::BERNOULLI: return "Bernoulli";
623 case ProcessType::BINOMIAL: return "Binomial";
624 case ProcessType::POISSON: return "Poisson";
625 case ProcessType::GEOMETRIC: return "Geometric";
626 case ProcessType::BMAP: return "BMAP";
627 case ProcessType::ME: return "ME";
628 case ProcessType::RAP: return "RAP";
629 case ProcessType::DISCRETESAMPLER: return "DiscreteSampler";
630 case ProcessType::ZIPF: return "Zipf";
631 case ProcessType::DMAP: return "DMAP";
632 case ProcessType::MMAP: return "MMAP";
633 case ProcessType::EMPIRICALCDF: return "EmpiricalCDF";
634 case ProcessType::PRIOR: return "Prior";
635 case ProcessType::NHPP: return "NHPP";
636 case ProcessType::MAPT: return "MAPt";
637 case ProcessType::PHT: return "PHt";
638 case ProcessType::MPH: return "MPH";
639 case ProcessType::MMAPT: return "MMAPt";
640 case ProcessType::MPHT: return "MPHt";
641 case ProcessType::BMMAPT: return "BMMAPt";
642 case ProcessType::NORMAL: return "Normal";
643 default: return "none";
644 }
645}
646
647/**
648 * `ProcessType.isMarkovian`: true when `sn.proc` carries an exact matrix
649 * representation of the law, rather than the Erlang fit `convertToMAP` leaves
650 * there for the parameter-only families. ME and RAP count -- their
651 * representation is the matrix-exponential analogue, not a generator -- and the
652 * discrete families do not, so a solver reading `sn.proc` as the law must gate
653 * on this exactly as MATLAB `ProcessType.m` does.
654 */
656 switch (p) {
657 case ProcessType::EXP:
660 case ProcessType::PH:
661 case ProcessType::APH:
662 case ProcessType::MAP:
666 case ProcessType::ME:
667 case ProcessType::RAP:
671 case ProcessType::MPH:
674 case ProcessType::BMMAPT: return true;
675 default: return false;
676 }
677}
678
679/**
680 * True when the type carries PER-MARK arrival blocks, i.e. an event of this
681 * process is labelled and the label is meaningful to the model. At a Source the
682 * label selects the class of the arriving job (`marked_classes`, `sn.markidx`).
683 */
685 return p == ProcessType::MMAP || p == ProcessType::MPH;
686}
687
688/**
689 * True when `sn.proc` holds the MARKED SCHEDULE slot. MPHt is stored lowered to
690 * MMAPt form segment by segment, so one walk serves both, exactly as one MAPt
691 * walk serves MAPt and PHt.
692 */
696
697/**
698 * True when an EVENT of this process releases (or, as a service process, completes) a
699 * BATCH of jobs whose size the process itself carries in its blocks.
700 *
701 * The batch twin of `process_is_marked_stationary` and `process_is_marked_schedule`,
702 * and the same rule applies: a procid test that should serve every batch family is a
703 * MEMBERSHIP test, never equality with `ProcessType::BMAP`.
704 *
705 * Disjoint from `Station::arrival_batch`, which is a SEPARATE batch-size law bolted
706 * onto a renewal stream by `Source.setArrivalBatch`. A process that is batch already
707 * carries its own sizes, so the two are mutually exclusive by construction.
708 *
709 * BMMAPT is ALSO `process_is_marked_schedule`, and deliberately: a consumer gated on
710 * that predicate reads it as the MMAPt it aggregates down to, which is right for
711 * anything batch-blind and WRONG for anything that releases jobs.
712 */
714 return p == ProcessType::BMAP || p == ProcessType::BMMAPT;
715}
716
717/**
718 * True for every marked family. MPH is the renewal special case of MMAP and
719 * lowers to the same M3A cell (D0 = S, D1k = s_k*alpha), so a consumer reading
720 * that layout must gate on `process_is_marked_stationary` rather than on
721 * equality with `ProcessType::MMAP`.
722 */
726
727/**
728 * A class-dependent scaling map, `sn.cdscaling`.
729 *
730 * It takes the per-class population vector at one station and returns the
731 * per-class rate multipliers, which is the signature `pfqn_cdfun` consumes; the
732 * alias resolves to the same std::function type as `pfqn::CdScaling`, so a map
733 * built here is passed straight through to the api layer.
734 */
735template <class T>
736using CdScaling = std::function<std::vector<T>(const std::vector<T>&)>;
737
738/**
739 * A globally state-dependent scaling, `sn.gdscaling`.
740 *
741 * Unlike CdScaling it is declared on the NETWORK, not on a station: the argument
742 * is the FULL population matrix, given row-major as nstations rows of nclasses
743 * entries, and the result is either one scalar, one entry per station, or one
744 * entry per (station, class) in the same row-major order. This is the Whittle
745 * primitive -- a rate that reads the whole state -- and no per-station scaling
746 * can express it when one route holds several resources at once.
747 */
748template <class T>
749using GdScaling = std::function<std::vector<T>(const std::vector<T>&)>;
750
751// ---------------------------------------------------------------------------
752// Global constants
753// ---------------------------------------------------------------------------
754
755/**
756 * The MATLAB GlobalConstants, as reported by lineStart at its defaults.
757 *
758 * These are doubles on purpose even in the exact instantiation: they are
759 * tolerances and sentinels of the ALGORITHM, not quantities of the model, and
760 * `lineStart` prints exactly these values. Converting them through
761 * num_traits<T>::from_double keeps the exact backend reproducing the same
762 * branch decisions as the reference rather than a mathematically cleaner set.
763 */
765 static constexpr double FineTol = 1e-8;
766 static constexpr double CoarseTol = 1e-3;
767 static constexpr double Zero = 1e-14;
768 /** Below this an off-diagonal entry is NO ARC of the phase / state graph. */
769 static constexpr double ArcTol = 1e-12;
770 /** Rate of an Immediate distribution; its mean is 1/Immediate = 1e-8. */
771 static constexpr double Immediate = 1e8;
772 /**
773 * Stand-in for an unbounded COUNT, MATLAB `GlobalConstants.MaxInt`. Used
774 * where a state row must hold a server count and Inf is not a count.
775 */
776 static constexpr double MaxInt = 2147483647.0;
777};
778
779// ---------------------------------------------------------------------------
780// Distribution descriptor
781// ---------------------------------------------------------------------------
782
783/**
784 * A LINE Distribution, as the model layer and `sn` carry it.
785 *
786 * WHAT EACH CONSUMER READS, which is why all of it is here:
787 * sn.rates, sn.scv the first two moments -- every AMVA path
788 * sn.procid the type tag -- the qsys and QNA dispatch
789 * sn.proc, sn.pie the (D0,D1) pair -- QNA, RQNA, cache, polling
790 * sn.mu, sn.phi the phase rates and completion probabilities
791 * sn.phases the order of that representation
792 * sn.lst the Laplace-Stieltjes transform -- M/G/1 analyzers
793 *
794 * `params` holds the constructor arguments in MATLAB's getParam order, so a
795 * dump can be compared parameter by parameter rather than through the moments,
796 * which two different distributions can share.
797 *
798 * Two values are special and must not be confused, because they enter the
799 * struct as opposite extremes:
800 * Immediate mean = 1/GlobalConstants.Immediate = 1e-8, rate = 1e8
801 * Disabled rate = NaN, which marks a (station, class) pair the class never
802 * visits; the chain and visit machinery keys on it.
803 *
804 * ARITHMETIC. The Markovian families are rational in their parameters and are
805 * built exactly. Gamma, Weibull and Lognormal are not -- their moments call
806 * tgamma or exp -- so their factories refuse by name under exact arithmetic
807 * rather than returning a rounded rational that would look exact.
808 */
809template <class T>
810struct PriorSpec;
811
812template <class T>
813struct Distrib;
814template <class T>
816
817template <class T>
818struct Distrib {
820 /**
821 * The law as DECLARED, when this one is a surrogate fitted over it.
822 *
823 * `sn_nonmarkov_toph` installs a fitted (D0,D1) over a Gamma or a Lognormal
824 * and retags `type` PH or ME, after which nothing names or evaluates the law
825 * the user wrote. MMAP[K]/G[K]/1 reads that law's TRANSFORM rather than the
826 * fit, so it needs the original; every other consumer wants the surrogate
827 * and reads this struct as before. Null when no substitution has happened.
828 * A POINTER, and shared, for the reason `prior` below is one: an inline
829 * member would make the type self-embedding.
830 */
831 std::shared_ptr<Distrib<T>> declared;
834 bool disabled = true;
835 /** Constructor arguments, in MATLAB getParam order. */
836 std::vector<T> params;
837 /** Replayer / Trace samples; empty for every other type. */
838 std::vector<T> trace;
839 /**
840 * The trace FILE a Replayer was read from, when there was one.
841 *
842 * The samples above are what every solver in this port uses, so the path is
843 * carried only for the exporters: `saveServiceStrategy` hands JMT a
844 * `ReplayerPar` naming a file, and a Replayer exported without it is a JMT
845 * model that reads nothing. Empty when the samples were supplied directly.
846 * `network_writer.h` emits it for the same reason: the samples have no wire
847 * form, so without the path a Replayer is written back as the moments and
848 * reloads as a different law.
849 */
850 std::string trace_file;
851 /** Built by cox2(mu1, mu2, phi1), MATLAB's 3-argument `Coxian(mu1, mu2, phi1)`; read only by the code generators. */
852 bool cox_scalar_form = false;
853 /**
854 * The (D0,D1) pair when the type carries one directly.
855 *
856 * EMPTY for Det, Uniform, Pareto, Gamma, Weibull, Lognormal and Replayer:
857 * MATLAB's getProcess returns their raw PARAMETERS there, and
858 * refreshProcessRepresentations replaces them with an Erlang approximation
859 * (`convertToMAP`) on the way into sn.proc. That conversion is a property
860 * of the refresh, not of the distribution, so it is not done here; see
861 * dist_to_map() in lang/distribution.h.
862 */
864 /**
865 * MMAP per-class D1 blocks / BMAP per-batch-size blocks; empty otherwise.
866 *
867 * OVERLOADED, and the type says which reading applies. For a BMMAPt it carries
868 * the MARK reading -- the width-weighted, BATCH-AGGREGATED per-mark averages --
869 * because that is what `mam::mmap_lambda` is asked for when the engine reports
870 * per-mark source throughputs. Its batch nominal is derived from `sched_Dbatch`
871 * instead, so the two axes never contend for this one field.
872 */
873 std::vector<Matrix<T>> Dmark;
874 /**
875 * The alternatives of a `Prior`, set only when `type == PRIOR`.
876 *
877 * A POINTER, and shared: `PriorSpec` holds `Distrib<T>` values, so an
878 * inline member would make the type self-embedding, and the spec is
879 * immutable once built, so copying a service table copies a pointer rather
880 * than a design. `mean` and `scv` beside it are the MIXTURE moments, as
881 * MATLAB's `Prior.getMean`/`getSCV` return: a Prior that reaches
882 * `refresh_rates` therefore lowers to a rate rather than to a NaN. That is
883 * for honesty of the struct dump only -- the featset gate refuses the model
884 * before any solver reads those rates, and `dist_to_map`, `dist_lst` and
885 * `dist_moment` refuse a Prior by name.
886 */
887 std::shared_ptr<PriorSpec<T>> prior;
888 bool is_prior() const { return type == ProcessType::PRIOR; }
889
890 /**
891 * `sn.proc{i}{r} = {breakpoints, A, B, cyclic}` of a MAPt / PHt / NHPP.
892 *
893 * `sched_bp` has one more entry than there are segments -- it is the
894 * BOUNDARY vector, so segment k is in force on [bp(k), bp(k+1)) -- and
895 * `sched_D0[k]`, `sched_D1[k]` are the pair of segment k, ALREADY in MAP
896 * form. A PHt is stored converted, D0 = S and D1 = (-S e) alpha, because
897 * `sn_schedule_nominal` converts it on every read and keeping the raw
898 * (alpha, S) here would make every consumer repeat that conversion and one
899 * of them eventually forget. The raw form is not needed: the conversion is
900 * lossless and nothing downstream asks for alpha again.
901 *
902 * EMPTY FOR EVERY OTHER TYPE. `has_schedule()` is the test, and a solver
903 * with no notion of time simply never calls it -- the nominal pair in
904 * `D0`/`D1` is a complete, time-averaged answer for such a solver.
905 */
906 std::vector<T> sched_bp;
907 std::vector<Matrix<T>> sched_D0, sched_D1;
908 /**
909 * The MARKED schedule, `sched_Dmark[c][k]` being the block of mark c+1 in segment k.
910 * Empty for an unmarked schedule. `sched_D1[k]` stays the per-segment AGGREGATE
911 * sum_c sched_Dmark[c][k], so every consumer that ignores marks reads the same
912 * unmarked schedule it always did; this is the schedule twin of `Dmark` beside `D1`.
913 */
914 std::vector<std::vector<Matrix<T>>> sched_Dmark;
915 /**
916 * The BATCH marked schedule, `sched_Dbatch[c][b][k]` being the block that, in
917 * segment k, releases a batch of b+1 jobs all carrying mark c+1. Empty for every
918 * family but BMMAPt.
919 *
920 * The two derived levels stay populated beside it and are what keeps every
921 * existing consumer working: `sched_Dmark[c][k]` is the batch-aggregated
922 * sum_b sched_Dbatch[c][b][k], and `sched_D1[k]` the aggregate over marks as
923 * well. So a consumer that ignores batches reads the MMAPt this hides down to,
924 * and one that ignores marks too reads the MAPt. Only a consumer that actually
925 * RELEASES jobs has to look here; `has_batch_schedule()` is the test.
926 *
927 * The batch axis is DENSE in b = 1..B, as BMAP's blocks are, so an unused batch
928 * size is a zero block rather than a gap.
929 */
930 std::vector<std::vector<std::vector<Matrix<T>>>> sched_Dbatch;
931 bool sched_cyclic = false;
932 bool has_schedule() const { return !sched_D0.empty(); }
933 bool has_marked_schedule() const { return !sched_Dmark.empty(); }
934 bool has_batch_schedule() const { return !sched_Dbatch.empty(); }
935 /** Largest batch size the schedule declares, or 1 when it carries no batch axis. */
936 std::size_t max_batch_size() const {
937 return sched_Dbatch.empty() ? std::size_t(1) : sched_Dbatch[0].size();
938 }
939
940 static Distrib exp_mean(const T& m) {
941 const T one = num_traits<T>::from_int(1);
942 Distrib d;
944 d.mean = m;
945 d.scv = one;
946 d.disabled = false;
947 const T lambda = m > num_traits<T>::from_int(0) ? T(one / m)
950 d.params.push_back(lambda);
951 d.D0 = Matrix<T>(1, 1, T(-lambda));
952 d.D1 = Matrix<T>(1, 1, lambda);
953 return d;
954 }
955 static Distrib exp_rate(const T& r) {
956 const T zero = num_traits<T>::from_int(0);
957 if (r <= zero) {
958 // Exp.fitRate(0) rationale: see _kb/04-networkstruct.md (cpp port notes)
960 }
961 return exp_mean(T(num_traits<T>::from_int(1) / r));
962 }
963 /**
964 * The point mass at zero.
965 *
966 * The Immediate singleton. Its MEAN is zero and its RATE is 1e8, and the
967 * two are deliberately not reciprocal: MATLAB's Immediate.getMean() returns
968 * 0 while Immediate.getRate() returns GlobalConstants.Immediate, and both
969 * are read, by different callers. SolverLN reads the mean (a task with an
970 * Immediate think time contributes no think time); Network.refreshRates
971 * reads the rate (the station serves the class in 1e-8 time units, not in
972 * zero, which would be an infinite service rate the MVA recursion cannot
973 * carry). Collapsing them onto 1/mean or 1/rate breaks one caller or the
974 * other, so the type tag decides.
975 *
976 * Its SCV is 1, NOT the 0 of a degenerate distribution. The reference makes
977 * this explicit -- `Immediate.getSCV` returns 1 in MATLAB, in the JAR and in
978 * Python -- because Immediate is realised downstream as an exponential of
979 * rate GlobalConstants.Immediate rather than as a Dirac: `rate()` below
980 * returns 1e8, and the SCV has to be the one that goes with it. Declaring 0
981 * here is invisible on a layer of PS or infinite-server stations, where the
982 * AMVA correction does not read the SCV at all, and shows up only once a
983 * layer holds an FCFS or multiserver station -- so it survives a model like
984 * lqn_ofbiz and breaks a model like lqn_basic.
985 */
987 Distrib d;
991 d.disabled = false;
993 d.D0 = Matrix<T>(1, 1, T(-imm));
994 d.D1 = Matrix<T>(1, 1, imm);
995 return d;
996 }
997 static Distrib disabled_dist() { return Distrib(); }
998 static Distrib det(const T& m) {
999 Distrib d;
1001 d.mean = m;
1003 d.disabled = false;
1004 d.params.push_back(m);
1005 return d;
1006 }
1007
1008 // -----------------------------------------------------------------------
1009 // The Markovian families: (D0,D1) is built here, exactly
1010 // -----------------------------------------------------------------------
1011
1012 /** Erlang(alpha, r): r phases of rate alpha, as MATLAB's Erlang(phaseRate, nphases). */
1013 static Distrib erlang(const T& phase_rate, std::size_t r) {
1014 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
1015 if (r == 0) throw InputError("Erlang: the number of phases must be positive");
1016 if (!(phase_rate > zero)) throw InputError("Erlang: the phase rate must be positive");
1017 Distrib d;
1019 d.disabled = false;
1020 d.params.push_back(phase_rate);
1021 d.params.push_back(num_traits<T>::from_int(static_cast<long>(r)));
1022 d.mean = T(num_traits<T>::from_int(static_cast<long>(r)) / phase_rate);
1023 d.scv = T(one / num_traits<T>::from_int(static_cast<long>(r)));
1024 d.D0 = Matrix<T>(r, r, zero);
1025 d.D1 = Matrix<T>(r, r, zero);
1026 for (std::size_t i = 0; i < r; ++i) {
1027 d.D0(i, i) = T(-phase_rate);
1028 if (i + 1 < r) d.D0(i, i + 1) = phase_rate;
1029 }
1030 d.D1(r - 1, 0) = phase_rate;
1031 return d;
1032 }
1033
1034 /**
1035 * Erlang fitted to a mean and an SCV, as MATLAB's Erlang.fitMeanAndSCV.
1036 *
1037 * AN SCV ABOVE ONE IS REFUSED, not answered. An Erlang of order r has
1038 * SCV = 1/r, so the family reaches 1 and no higher; ceil(1/c2) is 1 for
1039 * every c2 > 1, and returning that means handing back an EXPONENTIAL under
1040 * the name of the distribution the caller asked for. MATLAB errors here and
1041 * this port now does too, so a mis-specified SCV is a diagnostic rather
1042 * than a silently different service process.
1043 */
1044 static Distrib erlang_fit(const T& m, const T& c2) {
1045 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
1046 if (!(c2 > zero)) throw InputError("Erlang.fitMeanAndSCV: the SCV must be positive");
1047 if (c2 > one)
1048 throw InputError(
1049 "Erlang.fitMeanAndSCV: the Erlang distribution requires a squared coefficient "
1050 "of variation <= 1; use HyperExp, Coxian or APH above 1");
1051 // MATLAB: r = ceil(1/scv); alpha = r/mean
1052 const double r_d = std::ceil(1.0 / num_traits<T>::to_double(c2));
1053 const std::size_t r = static_cast<std::size_t>(r_d < 1.0 ? 1.0 : r_d);
1054 return erlang(T(num_traits<T>::from_int(static_cast<long>(r)) / m), r);
1055 }
1056
1057 /**
1058 * The same for any number of branches, which is what MATLAB `HyperExp`
1059 * accepts and what the writers emit as vector `p` and `lambda`. The
1060 * two-branch entry point stays because it carries MATLAB's getParam order
1061 * (p, lambda1, lambda2), which a parameter-by-parameter dump compares
1062 * against.
1063 */
1064 static Distrib hyperexp_n(const std::vector<T>& p, const std::vector<T>& lambda) {
1065 const T zero = num_traits<T>::from_int(0), two = num_traits<T>::from_int(2);
1066 const std::size_t n = p.size();
1067 if (n == 0 || lambda.size() != n)
1068 throw InputError("HyperExp: p and lambda must be non-empty and of equal length");
1069 Distrib d;
1071 d.disabled = false;
1072 for (const T& v : p) d.params.push_back(v);
1073 for (const T& v : lambda) d.params.push_back(v);
1074 d.D0 = Matrix<T>(n, n, zero);
1075 d.D1 = Matrix<T>(n, n, zero);
1076 T m1 = zero, m2 = zero;
1077 for (std::size_t i = 0; i < n; ++i) {
1078 if (!(lambda[i] > zero)) throw InputError("HyperExp: the phase rates must be positive");
1079 d.D0(i, i) = T(-lambda[i]);
1080 for (std::size_t j = 0; j < n; ++j) d.D1(i, j) = T(lambda[i] * p[j]);
1081 m1 += T(p[i] / lambda[i]);
1082 m2 += T(two * p[i] / (lambda[i] * lambda[i]));
1083 }
1084 d.mean = m1;
1085 d.scv = T((m2 - m1 * m1) / (m1 * m1));
1086 return d;
1087 }
1088
1089 /**
1090 * HyperExp(p, lambda1, lambda2): phase i chosen with probability p_i.
1091 *
1092 * D1(i,j) = mu(i) p(j) -- the OUTER product. Associating the other way
1093 * gives D1(i,j) = mu(i) p(i) replicated across the row, whose rows no
1094 * longer sum with D0 to zero; _kb records that trap in the MATLAB class.
1095 */
1096 static Distrib hyperexp(const T& p, const T& lambda1, const T& lambda2) {
1097 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
1098 if (!(lambda1 > zero) || !(lambda2 > zero))
1099 throw InputError("HyperExp: the phase rates must be positive");
1100 if (p < zero || p > one) throw InputError("HyperExp: p is not a probability");
1101 Distrib d;
1103 d.disabled = false;
1104 d.params.push_back(p);
1105 d.params.push_back(lambda1);
1106 d.params.push_back(lambda2);
1107 const T q = T(one - p);
1108 d.mean = T(p / lambda1 + q / lambda2);
1109 const T m2 = T(num_traits<T>::from_int(2) *
1110 (p / (lambda1 * lambda1) + q / (lambda2 * lambda2)));
1111 d.scv = T((m2 - d.mean * d.mean) / (d.mean * d.mean));
1112 d.D0 = Matrix<T>(2, 2, zero);
1113 d.D1 = Matrix<T>(2, 2, zero);
1114 d.D0(0, 0) = T(-lambda1);
1115 d.D0(1, 1) = T(-lambda2);
1116 d.D1(0, 0) = T(lambda1 * p);
1117 d.D1(0, 1) = T(lambda1 * q);
1118 d.D1(1, 0) = T(lambda2 * p);
1119 d.D1(1, 1) = T(lambda2 * q);
1120 return d;
1121 }
1122
1123 /**
1124 * Coxian(mu, phi): phase i completes with probability phi(i) and otherwise
1125 * moves to phase i+1. The last phase always completes.
1126 */
1127 static Distrib coxian(const std::vector<T>& mu, const std::vector<T>& phi) {
1128 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
1129 const std::size_t n = mu.size();
1130 if (n == 0 || phi.size() != n)
1131 throw InputError("Coxian: mu and phi must be non-empty and of equal length");
1132 Distrib d;
1134 d.disabled = false;
1135 for (const T& v : mu) d.params.push_back(v);
1136 for (const T& v : phi) d.params.push_back(v);
1137 d.D0 = Matrix<T>(n, n, zero);
1138 d.D1 = Matrix<T>(n, n, zero);
1139 for (std::size_t i = 0; i < n; ++i) {
1140 if (!(mu[i] > zero)) throw InputError("Coxian: the phase rates must be positive");
1141 d.D0(i, i) = T(-mu[i]);
1142 if (i + 1 < n) d.D0(i, i + 1) = T(mu[i] * (one - phi[i]));
1143 d.D1(i, 0) = T(mu[i] * phi[i]);
1144 }
1145 // moments of the absorbing chain started in phase 1: m_k = k! e_1 (-D0)^-k e
1146 d.mean = ph_moment_from(d.D0, 1, 1);
1147 const T m2 = ph_moment_from(d.D0, 1, 2);
1148 d.scv = T((m2 - d.mean * d.mean) / (d.mean * d.mean));
1149 return d;
1150 }
1151
1152 /** Cox2(mu1, mu2, phi1), MATLAB's two-phase Coxian constructor. */
1153 static Distrib cox2(const T& mu1, const T& mu2, const T& phi1) {
1154 std::vector<T> mu, phi;
1155 mu.push_back(mu1);
1156 mu.push_back(mu2);
1157 phi.push_back(phi1);
1158 phi.push_back(num_traits<T>::from_int(1));
1159 Distrib d = coxian(mu, phi);
1160 d.cox_scalar_form = true;
1161 return d;
1162 }
1163
1164 /**
1165 * PH / APH given by (alpha, A): D0 = A and D1 = (-A e) alpha.
1166 *
1167 * `acyclic` selects the type tag only; the representation is the same, and
1168 * no consumer of sn.proc distinguishes them.
1169 */
1170 static Distrib phase_type(const std::vector<T>& alpha, const Matrix<T>& A, bool acyclic) {
1171 const T zero = num_traits<T>::from_int(0);
1172 const std::size_t n = alpha.size();
1173 if (n == 0 || A.rows() != n || A.cols() != n)
1174 throw InputError("PH: alpha and the subgenerator have inconsistent sizes");
1175 Distrib d;
1176 d.type = acyclic ? ProcessType::APH : ProcessType::PH;
1177 d.disabled = false;
1178 for (const T& v : alpha) d.params.push_back(v);
1179 d.D0 = A;
1180 d.D1 = Matrix<T>(n, n, zero);
1181 for (std::size_t i = 0; i < n; ++i) {
1182 T out = zero;
1183 for (std::size_t j = 0; j < n; ++j) out += A(i, j);
1184 for (std::size_t j = 0; j < n; ++j) d.D1(i, j) = T(-out * alpha[j]);
1185 }
1186 d.mean = ph_moment(alpha, A, 1);
1187 const T m2 = ph_moment(alpha, A, 2);
1188 d.scv = T((m2 - d.mean * d.mean) / (d.mean * d.mean));
1189 return d;
1190 }
1191
1192 /** A MAP given by its two matrices; the moments are those of its stationary phase. */
1193 static Distrib map_dist(const Matrix<T>& D0, const Matrix<T>& D1, ProcessType tag) {
1194 if (D0.rows() != D0.cols() || D1.rows() != D1.cols() || D0.rows() != D1.rows())
1195 throw InputError("MAP: D0 and D1 must be square and of the same order");
1196 // A PH/APH given in MAP form is the renewal D1 = s alpha with s = -D0 e: recover
1197 // alpha, so the law carries its entry vector and moments like any phase_type.
1198 if (tag == ProcessType::PH || tag == ProcessType::APH) {
1199 const std::size_t n = D0.rows();
1200 std::vector<T> s(n, num_traits<T>::from_int(0)), alpha(n, num_traits<T>::from_int(0));
1201 T stot = num_traits<T>::from_int(0);
1202 for (std::size_t i = 0; i < n; ++i) {
1203 for (std::size_t j = 0; j < n; ++j) s[i] -= D0(i, j);
1204 stot += s[i];
1205 }
1206 if (n == 0 || num_traits<T>::to_double(stot) <= 0.0)
1207 throw InputError("PH: the subgenerator D0 has no exit rate");
1208 for (std::size_t j = 0; j < n; ++j) {
1209 for (std::size_t i = 0; i < n; ++i) alpha[j] += D1(i, j);
1210 alpha[j] = T(alpha[j] / stot);
1211 }
1212 double scale = 0.0;
1213 for (std::size_t i = 0; i < n; ++i)
1214 for (std::size_t j = 0; j < n; ++j)
1215 scale = std::max(scale, std::fabs(num_traits<T>::to_double(D1(i, j))));
1216 for (std::size_t i = 0; i < n; ++i)
1217 for (std::size_t j = 0; j < n; ++j)
1218 if (std::fabs(num_traits<T>::to_double(T(D1(i, j) - s[i] * alpha[j]))) >
1219 1e-10 * std::max(1.0, scale))
1220 throw InputError("PH: D1 must equal the exit vector -D0 e times the entry "
1221 "vector; this (D0, D1) is not a renewal phase-type");
1222 return phase_type(alpha, D0, tag == ProcessType::APH);
1223 }
1224 Distrib d;
1225 d.type = tag;
1226 d.disabled = false;
1227 d.D0 = D0;
1228 d.D1 = D1;
1229 // The moments of the law itself, so a MAP read before it is installed is not a zero-mean service.
1230 // A pair with no arrivals has no finite mean: keep the placeholders and let installation refuse it.
1233 if (tag == ProcessType::DMAP) {
1235 } else {
1236 mam::Map<T> m;
1237 m.D0 = D0;
1238 m.D1 = D1;
1240 d.mean = mam::map_mean(m);
1241 d.scv = mam::map_scv(m);
1242 }
1243 }
1244 return d;
1245 }
1246
1247 // -----------------------------------------------------------------------
1248 // The time-inhomogeneous families (Ko-Pender)
1249 // -----------------------------------------------------------------------
1250
1251 /**
1252 * Reject a schedule segment that is not a generator, the check MATLAB, the JAR
1253 * and Python all apply and this port used to skip.
1254 *
1255 * D0's off-diagonal and every arrival block must be non-negative, and
1256 * D0 + sum(blocks) must have zero row sums in every segment. Without it a
1257 * negative rate or a leaking row reaches the sampler, where it surfaces as a
1258 * negative holding time or a walk that never fires -- a wrong answer rather
1259 * than a refusal.
1260 */
1261 static void check_sched_generator(const std::string& who,
1262 const std::vector<Matrix<T>>& D0segs,
1263 const std::vector<std::vector<Matrix<T>>>& blocks) {
1264 const T zero = num_traits<T>::from_int(0);
1265 for (std::size_t k = 0; k < D0segs.size(); ++k) {
1266 const Matrix<T>& A = D0segs[k];
1267 for (std::size_t i = 0; i < A.rows(); ++i) {
1268 T rowsum = zero;
1269 for (std::size_t j = 0; j < A.cols(); ++j) {
1270 if (i != j && num_traits<T>::to_double(A(i, j)) < 0.0)
1271 throw InputError(who + ": off-diagonal D0 entries must be non-negative; "
1272 "segment " + std::to_string(k + 1) + " entry (" +
1273 std::to_string(i) + "," + std::to_string(j) + ") is not");
1274 rowsum += A(i, j);
1275 }
1276 for (const std::vector<Matrix<T>>& blk : blocks) {
1277 for (std::size_t j = 0; j < blk[k].cols(); ++j) {
1278 if (num_traits<T>::to_double(blk[k](i, j)) < 0.0)
1279 throw InputError(who + ": every arrival block must be non-negative; "
1280 "segment " + std::to_string(k + 1) + " entry (" +
1281 std::to_string(i) + "," + std::to_string(j) +
1282 ") is not");
1283 rowsum += blk[k](i, j);
1284 }
1285 }
1286 if (std::fabs(num_traits<T>::to_double(rowsum)) > 1e-10)
1287 throw InputError(who + ": D0 plus every arrival block must have zero row sums "
1288 "(generator); segment " + std::to_string(k + 1) + " row " +
1289 std::to_string(i) + " sums to " +
1290 std::to_string(num_traits<T>::to_double(rowsum)));
1291 }
1292 }
1293 }
1294
1295 /**
1296 * Reject a schedule whose matrices do not share one sparsity pattern, the twin of
1297 * MATLAB's `MAPt.checkCommonSupport`.
1298 *
1299 * The fluid solver expresses a segment as a per-entry multiplier on a nominal
1300 * matrix, and that multiplier is undefined where the nominal entry is zero.
1301 */
1302 static void check_common_support(const std::string& who, const std::string& label,
1303 const std::vector<Matrix<T>>& mats, bool ignore_diagonal) {
1304 if (mats.empty()) return;
1305 for (std::size_t k = 1; k < mats.size(); ++k) {
1306 for (std::size_t i = 0; i < mats[0].rows(); ++i)
1307 for (std::size_t j = 0; j < mats[0].cols(); ++j) {
1308 if (ignore_diagonal && i == j) continue;
1309 const bool a = num_traits<T>::to_double(mats[0](i, j)) != 0.0;
1310 const bool b = num_traits<T>::to_double(mats[k](i, j)) != 0.0;
1311 if (a != b)
1312 throw InputError(who + ": the " + label + " sparsity pattern must be "
1313 "identical across segments; segment " +
1314 std::to_string(k + 1) + " differs from segment 1 at (" +
1315 std::to_string(i) + "," + std::to_string(j) + "). A "
1316 "schedule that switches a transition on or off cannot be "
1317 "expressed as a per-entry multiplier on the time-averaged "
1318 "process. Keep the entry present with a small positive "
1319 "rate instead.");
1320 }
1321 }
1322 }
1323
1324 /** The shared constructor of the three schedule families. */
1325 static Distrib sched_dist(const std::vector<T>& breakpoints,
1326 const std::vector<Matrix<T>>& D0segs,
1327 const std::vector<Matrix<T>>& D1segs, bool cyclic, ProcessType tag) {
1328 const T zero = num_traits<T>::from_int(0);
1329 const std::string who = process_to_text(tag);
1330 const std::size_t n = D0segs.size();
1331 if (n == 0 || D1segs.size() != n)
1332 throw InputError(who + ": the two segment lists must be non-empty and equally long");
1333 if (breakpoints.size() != n + 1)
1334 throw InputError(who +
1335 ": breakpoints is the BOUNDARY vector and must hold one more entry "
1336 "than there are segments");
1337 for (std::size_t k = 0; k + 1 < breakpoints.size(); ++k)
1338 if (!(breakpoints[k + 1] > breakpoints[k]))
1339 throw InputError(who + ": breakpoints must be strictly increasing");
1340 const std::size_t order = D0segs[0].rows();
1341 for (std::size_t k = 0; k < n; ++k)
1342 if (D0segs[k].rows() != order || D0segs[k].cols() != order ||
1343 D1segs[k].rows() != order || D1segs[k].cols() != order)
1344 throw InputError(who +
1345 ": every segment must have the same order; the schedule "
1346 "modulates one phase structure and does not switch between them");
1347 // The generator check the other three codebases apply and this port used to
1348 // skip. It runs on the AGGREGATE arrival matrix, so it serves every schedule
1349 // family: a marked or batched one has already summed its blocks into D1segs.
1350 {
1351 std::vector<std::vector<Matrix<T>>> one;
1352 one.push_back(D1segs);
1353 check_sched_generator(who, D0segs, one);
1354 }
1355
1356 Distrib d;
1357 d.type = tag;
1358 d.disabled = false;
1359 d.sched_bp = breakpoints;
1360 d.sched_D0 = D0segs;
1361 d.sched_D1 = D1segs;
1362 d.sched_cyclic = cyclic;
1363 // The nominal pair: the width-weighted time average, i.e. the first two
1364 // outputs of `sn_schedule_nominal`.
1365 T total = zero;
1366 for (std::size_t k = 0; k < n; ++k) total += T(breakpoints[k + 1] - breakpoints[k]);
1367 d.D0 = Matrix<T>(order, order, zero);
1368 d.D1 = Matrix<T>(order, order, zero);
1369 for (std::size_t k = 0; k < n; ++k) {
1370 const T w = T(T(breakpoints[k + 1] - breakpoints[k]) / total);
1371 for (std::size_t a = 0; a < order; ++a)
1372 for (std::size_t b = 0; b < order; ++b) {
1373 d.D0(a, b) += w * D0segs[k](a, b);
1374 d.D1(a, b) += w * D1segs[k](a, b);
1375 }
1376 }
1377 // Same convention as `map_dist`: the moments of a MAP are computed by
1378 // `dist_moment`, not stored.
1379 d.mean = zero;
1381 return d;
1382 }
1383
1384 /**
1385 * MAPt(breakpoints, {D0_k}, {D1_k}, cyclic): a piecewise-constant
1386 * (D0(t), D1(t)).
1387 *
1388 * `breakpoints` is the boundary vector, so it holds one more entry than
1389 * there are segments and must be strictly increasing. Every segment must
1390 * have the SAME ORDER: the schedule modulates one phase structure, it does
1391 * not switch between structures, and a solver that integrated across a
1392 * change of order would have no way to map the phase occupancy across the
1393 * boundary. That is the reference's own constructor requirement.
1394 *
1395 * `D0` / `D1` are set to the WIDTH-WEIGHTED TIME AVERAGE of the segments,
1396 * which is `sn_schedule_nominal`'s nominal pair: it is the stationary
1397 * carrier of the phase structure the schedule modulates, so the phase count
1398 * and the mean rate a time-blind consumer reads are the ones the model
1399 * actually has.
1400 */
1401 static Distrib mapt(const std::vector<T>& breakpoints, const std::vector<Matrix<T>>& D0segs,
1402 const std::vector<Matrix<T>>& D1segs, bool cyclic) {
1403 check_common_support("MAPt", "off-diagonal D0", D0segs, true);
1404 check_common_support("MAPt", "D1", D1segs, false);
1405 return sched_dist(breakpoints, D0segs, D1segs, cyclic, ProcessType::MAPT);
1406 }
1407
1408 /**
1409 * PHt(breakpoints, {alpha_k}, {S_k}, cyclic), stored as its equivalent MAP
1410 * schedule: D0 = S and D1 = (-S e) alpha, the pair `sn_schedule_nominal`
1411 * builds from a PHt slot.
1412 */
1413 static Distrib pht(const std::vector<T>& breakpoints, const std::vector<std::vector<T>>& alphas,
1414 const std::vector<Matrix<T>>& Ssegs, bool cyclic) {
1415 const T zero = num_traits<T>::from_int(0);
1416 if (alphas.size() != Ssegs.size() || alphas.empty())
1417 throw InputError("PHt: alpha and S must have the same, non-zero number of segments");
1418 std::vector<Matrix<T>> D0segs, D1segs;
1419 for (std::size_t k = 0; k < Ssegs.size(); ++k) {
1420 const Matrix<T>& S = Ssegs[k];
1421 const std::vector<T>& a = alphas[k];
1422 if (S.rows() != S.cols() || S.rows() != a.size())
1423 throw InputError("PHt: alpha and the sub-generator disagree in order");
1424 Matrix<T> D1(S.rows(), S.cols(), zero);
1425 for (std::size_t i = 0; i < S.rows(); ++i) {
1426 T s = zero;
1427 for (std::size_t j = 0; j < S.cols(); ++j) s += S(i, j);
1428 for (std::size_t j = 0; j < S.cols(); ++j) D1(i, j) = T(-s * a[j]);
1429 }
1430 D0segs.push_back(S);
1431 D1segs.push_back(D1);
1432 }
1433 Distrib d = sched_dist(breakpoints, D0segs, D1segs, cyclic, ProcessType::PHT);
1434 return d;
1435 }
1436
1437 /**
1438 * MMAPt(breakpoints, {D0_k}, {{D1^(c)_k}}, cyclic): the marked schedule.
1439 *
1440 * `Dmarksegs` is MARK-MAJOR, `Dmarksegs[c][k]` being the block of mark c+1 in segment
1441 * k, matching the wire order of the `D1k` key. The aggregate per segment becomes
1442 * `sched_D1`, so the unmarked schedule this lowers to is exactly the MAPt with the
1443 * same matrices and a K = 1 MMAPt IS that MAPt.
1444 */
1445 static Distrib mmapt(const std::vector<T>& breakpoints, const std::vector<Matrix<T>>& D0segs,
1446 const std::vector<std::vector<Matrix<T>>>& Dmarksegs, bool cyclic) {
1447 const T zero = num_traits<T>::from_int(0);
1448 if (Dmarksegs.empty()) throw InputError("MMAPt: no marked arrival block was given");
1449 const std::size_t n = D0segs.size();
1450 for (const std::vector<Matrix<T>>& blk : Dmarksegs)
1451 if (blk.size() != n)
1452 throw InputError("MMAPt: every mark must be defined on the whole schedule; one "
1453 "carries " + std::to_string(blk.size()) + " segments against " +
1454 std::to_string(n) + " in D0");
1455 std::vector<Matrix<T>> D1segs;
1456 for (std::size_t k = 0; k < n; ++k) {
1457 Matrix<T> agg(D0segs[k].rows(), D0segs[k].cols(), zero);
1458 for (const std::vector<Matrix<T>>& blk : Dmarksegs) {
1459 if (blk[k].rows() != D0segs[k].rows() || blk[k].cols() != D0segs[k].cols())
1460 throw InputError("MMAPt: a mark block and D0 disagree in order in segment " +
1461 std::to_string(k + 1));
1462 for (std::size_t a = 0; a < agg.rows(); ++a)
1463 for (std::size_t b = 0; b < agg.cols(); ++b) agg(a, b) += blk[k](a, b);
1464 }
1465 D1segs.push_back(agg);
1466 }
1467 check_common_support("MMAPt", "off-diagonal D0", D0segs, true);
1468 for (std::size_t c = 0; c < Dmarksegs.size(); ++c)
1469 check_common_support("MMAPt", "D1 of mark " + std::to_string(c + 1), Dmarksegs[c],
1470 false);
1471 check_common_support("MMAPt", "aggregate D1", D1segs, false);
1472 Distrib d = sched_dist(breakpoints, D0segs, D1segs, cyclic, ProcessType::MMAPT);
1473 d.sched_Dmark = Dmarksegs;
1474 // The nominal per-mark blocks, beside the nominal aggregate `sched_dist` built.
1475 T total = zero;
1476 for (std::size_t k = 0; k < n; ++k) total += T(breakpoints[k + 1] - breakpoints[k]);
1477 const std::size_t order = D0segs[0].rows();
1478 for (const std::vector<Matrix<T>>& blk : Dmarksegs) {
1479 Matrix<T> nom(order, order, zero);
1480 for (std::size_t k = 0; k < n; ++k) {
1481 const T w = T(T(breakpoints[k + 1] - breakpoints[k]) / total);
1482 for (std::size_t a = 0; a < order; ++a)
1483 for (std::size_t b = 0; b < order; ++b) nom(a, b) += w * blk[k](a, b);
1484 }
1485 d.Dmark.push_back(nom);
1486 }
1487 return d;
1488 }
1489
1490 /**
1491 * BMMAPt(breakpoints, {D0_k}, {{{D^(c,b)_k}}}, cyclic): the BATCH marked schedule.
1492 *
1493 * `Dbatchsegs` is MARK-MAJOR, then BATCH, then segment: `Dbatchsegs[c][b][k]` is
1494 * the block that, in segment k, releases a batch of b+1 jobs all carrying mark
1495 * c+1. The batch axis is DENSE, as BMAP's blocks are, so an unused batch size is
1496 * declared as a zero block rather than omitted.
1497 *
1498 * Two derived levels are built here and are what makes the family cheap: the
1499 * batch-aggregated per-mark blocks become `sched_Dmark`, so every MMAPt consumer
1500 * reads the marked schedule unchanged, and their sum becomes `sched_D1`, so every
1501 * MAPt consumer reads the unmarked one. A B = 1 BMMAPt therefore IS the MMAPt
1502 * with the same blocks, and with K = 1 as well it IS the MAPt.
1503 *
1504 * The support rule binds the per-mark AGGREGATES and the total, not the
1505 * individual (mark, batch) blocks: requiring one pattern there too would forbid
1506 * a batch composition that changes with the segment, which is the one thing this
1507 * family exists to express and which neither reduction needs.
1508 */
1509 static Distrib bmmapt(const std::vector<T>& breakpoints, const std::vector<Matrix<T>>& D0segs,
1510 const std::vector<std::vector<std::vector<Matrix<T>>>>& Dbatchsegs,
1511 bool cyclic) {
1512 const T zero = num_traits<T>::from_int(0);
1513 if (Dbatchsegs.empty()) throw InputError("BMMAPt: no marked batch block was given");
1514 const std::size_t n = D0segs.size();
1515 if (n == 0) throw InputError("BMMAPt: D0 must carry at least one segment");
1516 const std::size_t B = Dbatchsegs[0].size();
1517 if (B == 0) throw InputError("BMMAPt: mark 1 declares no batch size");
1518 for (std::size_t c = 0; c < Dbatchsegs.size(); ++c) {
1519 if (Dbatchsegs[c].size() != B)
1520 throw InputError("BMMAPt: mark " + std::to_string(c + 1) + " declares " +
1521 std::to_string(Dbatchsegs[c].size()) + " batch sizes against " +
1522 std::to_string(B) + " in mark 1; the batch axis is dense and "
1523 "every mark must span it (use a zero block for an unused size)");
1524 for (std::size_t b = 0; b < B; ++b)
1525 if (Dbatchsegs[c][b].size() != n)
1526 throw InputError("BMMAPt: block (mark " + std::to_string(c + 1) + ", batch " +
1527 std::to_string(b + 1) + ") carries " +
1528 std::to_string(Dbatchsegs[c][b].size()) +
1529 " segments against " + std::to_string(n) + " in D0; every "
1530 "block must be defined on the whole schedule");
1531 }
1532 // The two derived levels: batch-aggregated per mark, then aggregated over marks.
1533 std::vector<std::vector<Matrix<T>>> Dmarksegs;
1534 for (std::size_t c = 0; c < Dbatchsegs.size(); ++c) {
1535 std::vector<Matrix<T>> permark;
1536 for (std::size_t k = 0; k < n; ++k) {
1537 Matrix<T> acc(D0segs[k].rows(), D0segs[k].cols(), zero);
1538 for (std::size_t b = 0; b < B; ++b) {
1539 const Matrix<T>& M = Dbatchsegs[c][b][k];
1540 if (M.rows() != D0segs[k].rows() || M.cols() != D0segs[k].cols())
1541 throw InputError("BMMAPt: block (mark " + std::to_string(c + 1) +
1542 ", batch " + std::to_string(b + 1) + ") and D0 disagree "
1543 "in order in segment " + std::to_string(k + 1));
1544 for (std::size_t a = 0; a < acc.rows(); ++a)
1545 for (std::size_t d = 0; d < acc.cols(); ++d) acc(a, d) += M(a, d);
1546 }
1547 permark.push_back(acc);
1548 }
1549 Dmarksegs.push_back(permark);
1550 }
1551 // Through mmapt, which builds sched_D1, the nominal pair and the nominal
1552 // per-mark blocks, and applies the generator and common-support checks to
1553 // exactly the three levels the reductions are constructed from.
1554 Distrib d = mmapt(breakpoints, D0segs, Dmarksegs, cyclic);
1556 d.sched_Dbatch = Dbatchsegs;
1557 return d;
1558 }
1559
1560 /**
1561 * MPHt(breakpoints, {alpha_k}, {S_k}, {{s^(c)_k}}, cyclic), stored LOWERED to MMAPt
1562 * form segment by segment: D0_k = S_k and D1^(c)_k = s^(c)_k alpha_k.
1563 *
1564 * Same treatment a PHt gets, for the same reason: one representation means one walk.
1565 * The exit vectors must partition the absorption rate, sum_c s^(c)_k = -S_k e, which
1566 * is what makes D0 plus the blocks a generator in every segment.
1567 */
1568 static Distrib mpht(const std::vector<T>& breakpoints,
1569 const std::vector<std::vector<T>>& alphas,
1570 const std::vector<Matrix<T>>& Ssegs,
1571 const std::vector<std::vector<std::vector<T>>>& exits, bool cyclic) {
1572 const T zero = num_traits<T>::from_int(0);
1573 if (alphas.size() != Ssegs.size() || alphas.empty())
1574 throw InputError("MPHt: alpha and S must have the same, non-zero number of segments");
1575 if (exits.empty()) throw InputError("MPHt: no marked exit vector was given");
1576 const std::size_t n = Ssegs.size();
1577 for (const std::vector<std::vector<T>>& ex : exits)
1578 if (ex.size() != n)
1579 throw InputError("MPHt: every mark must be defined on the whole schedule");
1580 std::vector<std::vector<Matrix<T>>> Dmarksegs;
1581 for (const std::vector<std::vector<T>>& ex : exits) {
1582 std::vector<Matrix<T>> blocks;
1583 for (std::size_t k = 0; k < n; ++k) {
1584 const Matrix<T>& S = Ssegs[k];
1585 const std::vector<T>& a = alphas[k];
1586 if (S.rows() != S.cols() || S.rows() != a.size() || ex[k].size() != S.rows())
1587 throw InputError("MPHt: alpha, S and the exit vector disagree in order in "
1588 "segment " + std::to_string(k + 1));
1589 Matrix<T> block(S.rows(), S.cols(), zero);
1590 for (std::size_t i = 0; i < S.rows(); ++i)
1591 for (std::size_t j = 0; j < S.cols(); ++j) block(i, j) = T(ex[k][i] * a[j]);
1592 blocks.push_back(block);
1593 }
1594 Dmarksegs.push_back(blocks);
1595 }
1596 // The partition identity, checked where it is still readable as exit vectors.
1597 for (std::size_t k = 0; k < n; ++k) {
1598 const Matrix<T>& S = Ssegs[k];
1599 for (std::size_t i = 0; i < S.rows(); ++i) {
1600 T rowsum = zero, marked = zero;
1601 for (std::size_t j = 0; j < S.cols(); ++j) rowsum += S(i, j);
1602 for (const std::vector<std::vector<T>>& ex : exits) marked += ex[k][i];
1603 const double slack = num_traits<T>::to_double(T(marked + rowsum));
1604 if (std::fabs(slack) > 1e-10)
1605 throw InputError("MPHt: the exit vectors must partition the absorption rate "
1606 "of S; in segment " + std::to_string(k + 1) + " phase " +
1607 std::to_string(i) + " they miss it by " +
1608 std::to_string(slack));
1609 }
1610 }
1611 Distrib d = mmapt(breakpoints, Ssegs, Dmarksegs, cyclic);
1613 return d;
1614 }
1615
1616 /**
1617 * NHPP(breakpoints, rates, cyclic): a MAPt of ORDER ONE, which is what an
1618 * inhomogeneous Poisson process is. Building it through the same path is
1619 * what makes every schedule consumer see one representation.
1620 */
1621 static Distrib nhpp(const std::vector<T>& breakpoints, const std::vector<T>& rates,
1622 bool cyclic) {
1623 std::vector<Matrix<T>> D0segs, D1segs;
1624 for (std::size_t k = 0; k < rates.size(); ++k) {
1625 Matrix<T> a(1, 1, T(-rates[k])), b(1, 1, rates[k]);
1626 D0segs.push_back(a);
1627 D1segs.push_back(b);
1628 }
1629 return sched_dist(breakpoints, D0segs, D1segs, cyclic, ProcessType::NHPP);
1630 }
1631
1632 // -----------------------------------------------------------------------
1633 // The non-Markovian families: parameters only, as MATLAB's getProcess
1634 // -----------------------------------------------------------------------
1635
1636 /** Uniform(a, b). */
1637 static Distrib uniform(const T& a, const T& b) {
1638 if (!(b > a)) throw InputError("Uniform: the upper bound must exceed the lower bound");
1639 Distrib d;
1641 d.disabled = false;
1642 d.params.push_back(a);
1643 d.params.push_back(b);
1644 d.mean = T((a + b) / num_traits<T>::from_int(2));
1645 const T w = T(b - a);
1646 d.scv = T((w * w / num_traits<T>::from_int(12)) / (d.mean * d.mean));
1647 return d;
1648 }
1649
1650 /** Pareto(shape, scale), with the MATLAB parameter order (alpha, k). */
1651 static Distrib pareto(const T& shape, const T& scale) {
1652 const T one = num_traits<T>::from_int(1), two = num_traits<T>::from_int(2);
1653 if (!(shape > two))
1654 throw InputError("Pareto: the shape must exceed 2 for a finite variance");
1655 Distrib d;
1657 d.disabled = false;
1658 d.params.push_back(shape);
1659 d.params.push_back(scale);
1660 d.mean = T(shape * scale / (shape - one));
1661 const T var = T(scale * scale * shape / ((shape - one) * (shape - one)) / (shape - two));
1662 d.scv = T(var / (d.mean * d.mean));
1663 return d;
1664 }
1665
1666 /**
1667 * Gamma(shape, scale), Weibull(scale, shape) and Lognormal(mu, sigma).
1668 *
1669 * Their moments are values of the gamma function or of exp, so they exist
1670 * only where the arithmetic has transcendentals. Under exact arithmetic the
1671 * factory REFUSES rather than storing a rounded rational: a rational that
1672 * came out of tgamma is not the exact moment of the distribution, and every
1673 * downstream claim of exactness would be false.
1674 */
1675 static Distrib gamma_dist(const T& shape, const T& scale) {
1676 const T one = num_traits<T>::from_int(1);
1677 Distrib d;
1679 d.disabled = false;
1680 d.params.push_back(shape);
1681 d.params.push_back(scale);
1682 d.mean = T(shape * scale);
1683 d.scv = T(one / shape);
1684 return d;
1685 }
1686
1687 static Distrib weibull(const T& scale, const T& shape) {
1688 if constexpr (!num_traits<T>::has_transcendental) {
1689 throw UnsupportedError(ErrorKind{"Arithmetic"},
1690 "Weibull: its moments are values of the gamma function, which exact arithmetic "
1691 "has no representation for; use the double or real backend");
1692 } else {
1693 const double a = num_traits<T>::to_double(scale);
1694 const double r = num_traits<T>::to_double(shape);
1695 if (!(a > 0.0) || !(r > 0.0))
1696 throw InputError("Weibull: the scale and the shape must be positive");
1697 const double g1 = std::tgamma(1.0 + 1.0 / r);
1698 const double g2 = std::tgamma(1.0 + 2.0 / r);
1699 Distrib d;
1701 d.disabled = false;
1702 d.params.push_back(scale);
1703 d.params.push_back(shape);
1704 d.mean = num_traits<T>::from_double(a * g1);
1705 d.scv = num_traits<T>::from_double((g2 - g1 * g1) / (g1 * g1));
1706 return d;
1707 }
1708 }
1709
1710 static Distrib lognormal(const T& logmean, const T& logsigma) {
1711 if constexpr (!num_traits<T>::has_transcendental) {
1712 throw UnsupportedError(ErrorKind{"Arithmetic"},
1713 "Lognormal: its moments are values of exp, which exact arithmetic has no "
1714 "representation for; use the double or real backend");
1715 } else {
1716 const double mu = num_traits<T>::to_double(logmean);
1717 const double sg = num_traits<T>::to_double(logsigma);
1718 if (!(sg > 0.0)) throw InputError("Lognormal: sigma must be positive");
1719 Distrib d;
1721 d.disabled = false;
1722 d.params.push_back(logmean);
1723 d.params.push_back(logsigma);
1724 d.mean = num_traits<T>::from_double(std::exp(mu + sg * sg / 2.0));
1725 d.scv = num_traits<T>::from_double(std::exp(sg * sg) - 1.0);
1726 return d;
1727 }
1728 }
1729
1730 /**
1731 * `Normal(mu, sigma)`: the Gaussian, for use as a continuous `Prior`'s
1732 * parameter density.
1733 *
1734 * `scv` is Inf at mu = 0, which is `Normal.m:52-63`'s own answer and not a
1735 * degradation: the SCV of a zero-mean law is not defined, and the reference
1736 * says so rather than dividing.
1737 */
1738 static Distrib normal(const T& mu, const T& sigma) {
1739 if (!(num_traits<T>::to_double(sigma) > 0.0))
1740 throw InputError("Normal: sigma must be positive");
1741 Distrib d;
1743 d.disabled = false;
1744 d.params.push_back(mu);
1745 d.params.push_back(sigma);
1746 d.mean = mu;
1747 const double m = num_traits<T>::to_double(mu), s = num_traits<T>::to_double(sigma);
1748 d.scv = std::abs(m) < GlobalConstants::FineTol
1749 ? num_traits<T>::from_double(std::numeric_limits<double>::infinity())
1750 : num_traits<T>::from_double((s * s) / (m * m));
1751 return d;
1752 }
1753
1754 /**
1755 * Replayer / Trace read FROM A FILE, which keeps the path beside the samples.
1756 *
1757 * Every solver in this port replays `trace`; the path is what the EXPORTERS
1758 * need. JMT is handed a `ReplayerPar` naming a file and has no way to take
1759 * samples inline, so a Replayer exported without it is a JMT model that
1760 * reads nothing -- `oqn_trace_driven` was refused outright for exactly that.
1761 */
1762 static Distrib replayer_from(const std::vector<T>& samples, const std::string& path) {
1763 Distrib d = replayer(samples);
1764 d.trace_file = path;
1765 return d;
1766 }
1767
1768 /** Replayer / Trace: the samples, with their empirical first two moments. */
1769 static Distrib replayer(const std::vector<T>& samples) {
1770 if (samples.empty()) throw InputError("Replayer: the trace is empty");
1771 Distrib d;
1773 d.disabled = false;
1774 d.trace = samples;
1775 const T n = num_traits<T>::from_int(static_cast<long>(samples.size()));
1777 for (const T& x : samples) {
1778 s1 += x;
1779 s2 += T(x * x);
1780 }
1781 d.mean = T(s1 / n);
1782 d.scv = T((s2 / n - d.mean * d.mean) / (d.mean * d.mean));
1783 return d;
1784 }
1785
1786 // -----------------------------------------------------------------------
1787 // The discrete families
1788 //
1789 // MATLAB's getProcess for each of these returns [mean, scv] and nothing
1790 // else, so they carry NO (D0,D1): refreshProcessRepresentations replaces
1791 // them with the Erlang fit convertToMAP, which `dist_to_map` reproduces
1792 // from the two moments alone. Storing an invented representation here
1793 // would make sn.proc disagree with the reference for the same model.
1794 // -----------------------------------------------------------------------
1795
1796 /** DiscreteUniform(a, b) over the integers a..b inclusive. */
1797 static Distrib discrete_uniform(const T& a, const T& b) {
1798 const T two = num_traits<T>::from_int(2), one = num_traits<T>::from_int(1);
1799 if (!(b >= a)) throw InputError("DiscreteUniform: the upper bound must not be below the lower");
1800 Distrib d;
1802 d.disabled = false;
1803 d.params.push_back(a);
1804 d.params.push_back(b);
1805 d.mean = T((a + b) / two);
1806 const T w = T(b - a + one);
1807 const T var = T((w * w - one) / num_traits<T>::from_int(12));
1808 d.scv = T(var / (d.mean * d.mean));
1809 return d;
1810 }
1811
1812 /** Bernoulli(p): one trial, mean p and variance p(1-p). */
1813 static Distrib bernoulli(const T& p) {
1814 const T one = num_traits<T>::from_int(1);
1815 Distrib d;
1817 d.disabled = false;
1818 d.params.push_back(p);
1819 d.mean = p;
1820 d.scv = T((one - p) / p);
1821 return d;
1822 }
1823
1824 /** Binomial(n, p). */
1825 static Distrib binomial(const T& n, const T& p) {
1826 const T one = num_traits<T>::from_int(1);
1827 Distrib d;
1829 d.disabled = false;
1830 d.params.push_back(n);
1831 d.params.push_back(p);
1832 d.mean = T(n * p);
1833 d.scv = T((one - p) / (n * p));
1834 return d;
1835 }
1836
1837 /**
1838 * Poisson(lambda), whose SCV is 1/lambda -- the count's variance is lambda
1839 * and its mean is lambda, so this is NOT the exponential's SCV of 1.
1840 */
1841 static Distrib poisson(const T& lambda) {
1842 const T one = num_traits<T>::from_int(1);
1843 Distrib d;
1845 d.disabled = false;
1846 d.params.push_back(lambda);
1847 d.mean = lambda;
1848 d.scv = T(one / lambda);
1849 return d;
1850 }
1851
1852 /**
1853 * Geometric(p) on the MATLAB convention: the NUMBER OF TRIALS to the first
1854 * success, support {1, 2, ...}, so the mean is 1/p and the SCV is 1-p.
1855 */
1856 static Distrib geometric(const T& p) {
1857 const T one = num_traits<T>::from_int(1);
1858 Distrib d;
1860 d.disabled = false;
1861 d.params.push_back(p);
1862 d.mean = T(one / p);
1863 d.scv = T(one - p);
1864 return d;
1865 }
1866
1867 /**
1868 * Zipf(s, n) over the ranks 1..n, with the generalized harmonic moments
1869 * H(s-1,n)/H(s,n) and H(s-2,n)/H(s,n) MATLAB `Zipf.m` uses.
1870 *
1871 * The harmonic sums call pow for a non-integer shape, so the factory is
1872 * gated on transcendental arithmetic exactly as Weibull and Lognormal are.
1873 */
1874 static Distrib zipf(const T& s, std::size_t n) {
1875 if constexpr (!num_traits<T>::has_transcendental) {
1876 (void)s;
1877 (void)n;
1878 throw UnsupportedError(ErrorKind{"Arithmetic"},
1879 "Zipf: its moments are generalized harmonic sums of a real exponent, which exact "
1880 "arithmetic has no representation for; use the double or real backend");
1881 } else {
1882 if (n == 0) throw InputError("Zipf: the item count must be positive");
1883 const double sv = num_traits<T>::to_double(s);
1884 auto harmonic = [n](double e) {
1885 double acc = 0.0;
1886 for (std::size_t k = 1; k <= n; ++k) acc += std::pow(double(k), -e);
1887 return acc;
1888 };
1889 const double h0 = harmonic(sv), h1 = harmonic(sv - 1.0), h2 = harmonic(sv - 2.0);
1890 Distrib d;
1892 d.disabled = false;
1893 d.params.push_back(s);
1894 d.params.push_back(num_traits<T>::from_int(static_cast<long>(n)));
1895 const double m1 = h1 / h0;
1897 d.scv = num_traits<T>::from_double((h2 / h0 - m1 * m1) / (m1 * m1));
1898 return d;
1899 }
1900 }
1901
1902 /**
1903 * DiscreteSampler(p, x): the pmf p over the points x.
1904 *
1905 * THE MOMENTS ARE TAKEN OVER x. This port, and MATLAB
1906 * `DiscreteSampler.getMean` with it, used to weight by the RANKS 1..n
1907 * instead; the two agree on the default x = 1:n, which is the form the
1908 * cache popularity vectors are written in, so the rank form survived
1909 * unnoticed until a fork's jobs-per-link distribution arrived on a shifted
1910 * support. The JAR and native Python already weighted by x.
1911 */
1912 static Distrib discrete_sampler(const std::vector<T>& p, const std::vector<T>& x) {
1913 if (p.empty()) throw InputError("DiscreteSampler: the probability vector is empty");
1914 if (!x.empty() && x.size() != p.size())
1915 throw InputError("DiscreteSampler: p and x must have the same length");
1916 Distrib d;
1918 d.disabled = false;
1919 d.params = p;
1920 d.trace = x;
1922 T tot = num_traits<T>::from_int(0);
1923 for (std::size_t k = 0; k < p.size(); ++k) {
1924 // an absent x is the default support 1..n
1925 const T pt = x.empty() ? num_traits<T>::from_int(static_cast<long>(k + 1)) : x[k];
1926 m1 += T(p[k] * pt);
1927 m2 += T(p[k] * pt * pt);
1928 tot += p[k];
1929 }
1930 m1 = T(m1 / tot);
1931 m2 = T(m2 / tot);
1932 d.mean = m1;
1933 d.scv = T((m2 - m1 * m1) / (m1 * m1));
1934 return d;
1935 }
1936
1937 /**
1938 * EmpiricalCDF(x, F): the moments of the MIDPOINT rule over the CDF bins,
1939 * which is what MATLAB `EmpiricalCDF.getMoments` integrates -- the atom
1940 * F_1 at x_1, then each bin's midpoint raised to the moment order, weighted
1941 * by the CDF increment. The rows are the (F, x) pairs in the order they arrive.
1942 */
1943 static Distrib empirical_cdf(const std::vector<T>& x, const std::vector<T>& F) {
1944 if (x.size() != F.size() || x.size() < 2)
1945 throw InputError("EmpiricalCDF: x and F must be equally long and hold at least two points");
1946 const T two = num_traits<T>::from_int(2);
1947 Distrib d;
1949 d.disabled = false;
1950 d.trace = x;
1951 d.params = F;
1952 // the F_1 atom at x_1 (mass the first point already holds), then the bins
1953 T m1 = T(F[0] * x[0]), m2 = T(F[0] * x[0] * x[0]);
1954 for (std::size_t i = 0; i + 1 < x.size(); ++i) {
1955 const T mid = T((x[i + 1] - x[i]) / two + x[i]);
1956 const T w = T(F[i + 1] - F[i]);
1957 m1 += T(mid * w);
1958 m2 += T(mid * mid * w);
1959 }
1960 d.mean = m1;
1961 d.scv = T(m2 / (m1 * m1) - num_traits<T>::from_int(1));
1962 return d;
1963 }
1964
1965 // -----------------------------------------------------------------------
1966 // The matrix-exponential and discrete-time Markovian families
1967 // -----------------------------------------------------------------------
1968
1969 /**
1970 * ME(alpha, A): the matrix-exponential distribution, whose moments are the
1971 * phase-type ones -- k! alpha (-A)^-k e -- evaluated by DEFINITION rather
1972 * than through the stationary vector of A + (-Ae)alpha. MATLAB `ME.getMean`
1973 * makes the same choice and says why: the stationary solve is a
1974 * probabilistic object that a non-Markovian A degrades badly (a CME of
1975 * order 101 lost 2.6e-4 in the SCV that way, against 1e-13 by definition).
1976 */
1977 static Distrib me(const std::vector<T>& alpha, const Matrix<T>& A) {
1978 Distrib d = phase_type(alpha, A, false);
1980 return d;
1981 }
1982
1983 /** RAP(H0, H1): a rational arrival process, whose moments are the MAP ones. */
1984 static Distrib rap(const Matrix<T>& H0, const Matrix<T>& H1) {
1985 return map_dist(H0, H1, ProcessType::RAP);
1986 }
1987
1988 /**
1989 * DMAP(D0, D1): a DISCRETE-time MAP, where D0 + D1 is stochastic rather
1990 * than a generator. Its moments cannot come from `dist_refresh_moments`'s
1991 * continuous formulas; `map_dist` fills them through `dmap_refresh_moments`.
1992 */
1993 static Distrib dmap(const Matrix<T>& D0, const Matrix<T>& D1) {
1994 return map_dist(D0, D1, ProcessType::DMAP);
1995 }
1996
1997 /**
1998 * MMAP: D0 plus one D1 block per mark. `D1` is their sum, the aggregate
1999 * arrival matrix every unmarked consumer reads, and the blocks stay in
2000 * `Dmark` for the ones that distinguish marks.
2001 */
2002 static Distrib mmap(const Matrix<T>& D0, const std::vector<Matrix<T>>& D1k) {
2003 if (D1k.empty()) throw InputError("MMAP: no marked arrival block was given");
2005 for (const Matrix<T>& Dk : D1k) {
2006 if (Dk.rows() != D0.rows() || Dk.cols() != D0.cols())
2007 throw InputError("MMAP: every marked block must have the order of D0");
2008 for (std::size_t i = 0; i < agg.rows(); ++i)
2009 for (std::size_t j = 0; j < agg.cols(); ++j) agg(i, j) += Dk(i, j);
2010 }
2012 d.Dmark = D1k;
2013 return d;
2014 }
2015
2016 /**
2017 * MPH(alpha, S, {s_k}): the marked phase-type law, a PH with K labelled exits.
2018 * It is the RENEWAL special case of an MMAP and is stored LOWERED to that cell,
2019 * D0 = S and D1k = s_k alpha, keeping the MPH process type so a featset gate can
2020 * still tell the two apart. Validated as MATLAB's MPH constructor: alpha is a
2021 * proper (non-defective) law, S a sub-generator, the exit vectors non-negative,
2022 * not all zero, and partitioning the absorption rate, sum_k s_k = -S e.
2023 */
2024 static Distrib mph(const std::vector<T>& alpha, const Matrix<T>& S,
2025 const std::vector<std::vector<T>>& exits) {
2026 const double tol = 1e-10;
2027 const T zero = num_traits<T>::from_int(0);
2028 const std::size_t h = alpha.size();
2029 if (h == 0 || exits.empty())
2030 throw InputError("MPH: alpha, S and a non-empty list of exit vectors are all required");
2031 if (S.rows() != h || S.cols() != h)
2032 throw InputError("MPH: S must be square of order " + std::to_string(h) + " to match alpha");
2033 double asum = 0.0;
2034 for (const T& a : alpha) {
2035 if (num_traits<T>::to_double(a) < 0.0) throw InputError("MPH: alpha must be non-negative");
2036 asum += num_traits<T>::to_double(a);
2037 }
2038 if (std::fabs(asum - 1.0) > tol)
2039 throw InputError("MPH: alpha must sum to one; it sums to " + std::to_string(asum) +
2040 ". A defective entry law would put mass on an instantaneous "
2041 "completion that carries no mark.");
2042 for (std::size_t i = 0; i < h; ++i)
2043 for (std::size_t j = 0; j < h; ++j) {
2044 const double v = num_traits<T>::to_double(S(i, j));
2045 if (i == j && v >= 0.0) throw InputError("MPH: the diagonal of S must be negative");
2046 if (i != j && v < 0.0)
2047 throw InputError("MPH: off-diagonal entries of S must be non-negative");
2048 }
2049 std::vector<double> marked(h, 0.0);
2050 for (std::size_t k = 0; k < exits.size(); ++k) {
2051 if (exits[k].size() != h)
2052 throw InputError("MPH: exit vector " + std::to_string(k + 1) + " must have " +
2053 std::to_string(h) + " entries");
2054 for (std::size_t i = 0; i < h; ++i) {
2055 const double v = num_traits<T>::to_double(exits[k][i]);
2056 if (v < 0.0)
2057 throw InputError("MPH: exit vector " + std::to_string(k + 1) + " must be non-negative");
2058 marked[i] += v;
2059 }
2060 }
2061 bool any_exit = false;
2062 for (std::size_t i = 0; i < h; ++i) {
2063 double absorb = 0.0;
2064 for (std::size_t j = 0; j < h; ++j) absorb -= num_traits<T>::to_double(S(i, j));
2065 if (std::fabs(marked[i] - absorb) > tol)
2066 throw InputError("MPH: the exit vectors must partition the absorption rate of S. "
2067 "Every way of leaving the transient phases must carry exactly one mark.");
2068 if (marked[i] != 0.0) any_exit = true;
2069 }
2070 if (!any_exit) throw InputError("MPH: every exit vector is zero, so absorption can never occur");
2071 std::vector<Matrix<T>> blocks;
2072 for (const std::vector<T>& sk : exits) {
2073 Matrix<T> block(h, h, zero);
2074 for (std::size_t i = 0; i < h; ++i)
2075 for (std::size_t j = 0; j < h; ++j) block(i, j) = T(sk[i] * alpha[j]);
2076 blocks.push_back(block);
2077 }
2078 Distrib d = mmap(S, blocks);
2080 return d;
2081 }
2082
2083 /**
2084 * BMAP: the batch-size blocks D0, D1, ..., Dk, where Dj carries an arrival
2085 * of batch size j. The wire form is the whole list including D0, so the
2086 * head is split off here.
2087 */
2088 static Distrib bmap(const std::vector<Matrix<T>>& D) {
2089 if (D.size() < 2) throw InputError("BMAP: the block list must carry D0 and at least one batch block");
2090 const std::vector<Matrix<T>> batches(D.begin() + 1, D.end());
2091 Distrib d = mmap(D[0], batches);
2093 return d;
2094 }
2095
2096 bool is_immediate() const { return type == ProcessType::IMMEDIATE; }
2097
2098 /** True when the type carries a (D0,D1) pair of its own. */
2099 bool has_map() const { return D0.rows() > 0; }
2100
2101 /**
2102 * Phase rates, MATLAB's getMu: the total outgoing rate of each phase.
2103 *
2104 * Empty when the type carries no representation, which is what
2105 * refreshProcessPhases writes as NaN for a Fork or a Join.
2106 */
2107 std::vector<T> mu_vec() const {
2108 std::vector<T> v;
2109 if (!has_map()) {
2110 if (disabled) return v;
2111 v.push_back(rate()); // one phase at the mean rate, as MATLAB does
2112 return v;
2113 }
2114 for (std::size_t i = 0; i < D0.rows(); ++i) v.push_back(T(-D0(i, i)));
2115 return v;
2116 }
2117
2118 /** Completion probabilities, MATLAB's getPhi: (D1 e) ./ (-diag(D0)). */
2119 std::vector<T> phi_vec() const {
2120 std::vector<T> v;
2121 if (!has_map()) {
2122 if (disabled) return v;
2123 v.push_back(num_traits<T>::from_int(1));
2124 return v;
2125 }
2126 const T zero = num_traits<T>::from_int(0);
2127 for (std::size_t i = 0; i < D1.rows(); ++i) {
2128 T s = zero;
2129 for (std::size_t j = 0; j < D1.cols(); ++j) s += D1(i, j);
2130 const T out = T(-D0(i, i));
2131 v.push_back(out == zero ? num_traits<T>::from_int(1) : T(s / out));
2132 }
2133 return v;
2134 }
2135
2136 /** The order of the representation, MATLAB's sn.phases. */
2137 std::size_t phases() const {
2138 if (disabled) return 0;
2139 return has_map() ? D0.rows() : 1;
2140 }
2141
2142 /**
2143 * The k-th raw moment of a phase-type (alpha, A): k! alpha (-A)^-k e.
2144 *
2145 * The inverse is never formed: the powers are accumulated by repeated
2146 * solves of (-A) x = b, which is exact in rational arithmetic and stable
2147 * in floating point.
2148 */
2149 static T ph_moment(const std::vector<T>& alpha, const Matrix<T>& A, unsigned k) {
2150 const std::size_t n = alpha.size();
2151 std::vector<T> x(n, num_traits<T>::from_int(1));
2152 for (unsigned i = 0; i < k; ++i) x = solve_neg(A, x);
2153 T acc = num_traits<T>::from_int(0);
2154 for (std::size_t i = 0; i < n; ++i) acc += alpha[i] * x[i];
2155 T fact = num_traits<T>::from_int(1);
2156 for (unsigned i = 2; i <= k; ++i) fact *= num_traits<T>::from_int(static_cast<long>(i));
2157 return T(fact * acc);
2158 }
2159
2160 /** The same, for a representation entered in a single phase (1-based). */
2161 static T ph_moment_from(const Matrix<T>& A, std::size_t start, unsigned k) {
2162 std::vector<T> alpha(A.rows(), num_traits<T>::from_int(0));
2163 alpha[start - 1] = num_traits<T>::from_int(1);
2164 return ph_moment(alpha, A, k);
2165 }
2166
2167 private:
2168 /** Solve (-A) x = b by Gaussian elimination with partial pivoting. */
2169 static std::vector<T> solve_neg(const Matrix<T>& A, const std::vector<T>& b) {
2170 const T zero = num_traits<T>::from_int(0);
2171 const std::size_t n = A.rows();
2172 if (A.cols() != n || b.size() != n)
2173 throw InputError("phase-type moment: the subgenerator is not square");
2174 Matrix<T> M(n, n, zero);
2175 std::vector<T> x = b;
2176 for (std::size_t i = 0; i < n; ++i)
2177 for (std::size_t j = 0; j < n; ++j) M(i, j) = T(-A(i, j));
2178 for (std::size_t col = 0; col < n; ++col) {
2179 std::size_t best = col;
2180 double bv = std::fabs(num_traits<T>::to_double(M(col, col)));
2181 for (std::size_t r = col + 1; r < n; ++r) {
2182 const double v = std::fabs(num_traits<T>::to_double(M(r, col)));
2183 if (v > bv) {
2184 bv = v;
2185 best = r;
2186 }
2187 }
2188 if (best != col) {
2189 for (std::size_t j = 0; j < n; ++j) std::swap(M(col, j), M(best, j));
2190 std::swap(x[col], x[best]);
2191 }
2192 if (M(col, col) == zero)
2193 throw NumericError(ErrorKind{"Singular"}, "phase-type moment: the subgenerator is singular");
2194 for (std::size_t r = 0; r < n; ++r) {
2195 if (r == col) continue;
2196 const T f = T(M(r, col) / M(col, col));
2197 if (f == zero) continue;
2198 for (std::size_t j = 0; j < n; ++j) M(r, j) = T(M(r, j) - f * M(col, j));
2199 x[r] = T(x[r] - f * x[col]);
2200 }
2201 }
2202 for (std::size_t i = 0; i < n; ++i) x[i] = T(x[i] / M(i, i));
2203 return x;
2204 }
2205
2206 public:
2207
2208
2209 /**
2210 * The rate MATLAB's refreshRates would store: 1/mean, with the Immediate
2211 * singleton short-circuited to its declared rate so that the reciprocal of
2212 * 1e-8 is exactly 1e8 in every arithmetic rather than 1e8 plus rounding.
2213 */
2220};
2221
2222/**
2223 * What a `Prior` carries, in either of its two forms.
2224 *
2225 * DISCRETE: an explicit set of alternative distributions and their prior
2226 * weights, which must sum to one. This is the form the model.json wire carries
2227 * (`{"type":"Prior","distributions":[...],"probabilities":[...]}`), because a
2228 * factory cannot cross JSON.
2229 *
2230 * CONTINUOUS: a density over a scalar parameter theta plus a map theta ->
2231 * Distribution, the form the epistemic propagation of Trivedi and Bobbio
2232 * (2017), Sec. 3.4 needs. It is reduced to the discrete form by
2233 * `prior_discretize` (lang/prior.h) before anything downstream sees it, so both
2234 * forms are consumed identically. It can only be BUILT programmatically.
2235 *
2236 * IT IS NOT A MIXTURE. Each alternative is a separate model realization whose
2237 * weight is a prior probability over models, not a branching probability inside
2238 * one model. The mixture moments are still computed (see `Distrib::prior`)
2239 * because MATLAB's `Prior.getMean`/`getSCV` do, but they are a summary of the
2240 * epistemic uncertainty and not the law any station serves.
2241 */
2242template <class T>
2244 /** True for the parameter-density form, false for the alternative-set form. */
2245 bool continuous = false;
2246 /** The alternatives and their weights; the discrete form only. */
2247 std::vector<Distrib<T>> alternatives;
2248 std::vector<T> probabilities;
2249 /** The law of the scalar parameter; the continuous form only. */
2251 /**
2252 * theta -> Distribution; the continuous form only.
2253 *
2254 * A `std::function` and not a serializable description, exactly as MATLAB's
2255 * `distFactory` is a function handle: the map is arbitrary code (a rate
2256 * becomes an Exp, a scale becomes an Erlang of fixed order), and no wire
2257 * format in this codebase encodes it. That is why the JSON reader builds
2258 * the discrete form only.
2259 */
2260 std::function<Distrib<T>(const T&)> factory;
2261};
2262
2263/**
2264 * The first two moments of a DISCRETE-time MAP, from its own law.
2265 *
2266 * With alpha the arrival-epoch stationary vector -- `dmap_pie`, the stationary
2267 * vector of (I - D0)^-1 D1 -- the interarrival count has
2268 * P(N = k) = alpha D0^(k-1) D1 e, so
2269 *
2270 * E[N] = alpha (I - D0)^-1 e (MATLAB `DMAP.getMean`)
2271 * E[N(N-1)] = 2 alpha D0 (I - D0)^-2 e
2272 *
2273 * THE SCV IS NOT MATLAB'S INHERITED ONE. `DMAP` declares no getSCV, so it falls
2274 * through to `Markovian.getSCV` = map_scv({D0,D1}), a CONTINUOUS-time formula
2275 * that reads D0 + D1 as a generator; for a DMAP that matrix is stochastic, so
2276 * the stationary solve behind it is singular and the number it returns is not
2277 * the SCV of anything. Reproducing it would propagate an undefined value into
2278 * every AMVA path, so the discrete second moment is computed here and the
2279 * reference defect is recorded in BUGS.md.
2280 */
2281template <class T>
2283 const std::size_t n = d.D0.rows();
2284 const T one = num_traits<T>::from_int(1), two = num_traits<T>::from_int(2);
2285 Matrix<T> ImD0(n, n);
2286 for (std::size_t i = 0; i < n; ++i)
2287 for (std::size_t j = 0; j < n; ++j) ImD0(i, j) = T((i == j ? one : T(0)) - d.D0(i, j));
2288 // alpha: the stationary vector of P = (I - D0)^-1 D1, formed column by
2289 // column through one factorization rather than by inverting ImD0.
2290 Matrix<T> LU = ImD0;
2291 const std::vector<std::size_t> piv = lu_factor(LU);
2292 Matrix<T> P(n, n);
2293 for (std::size_t j = 0; j < n; ++j) {
2294 std::vector<T> col(n);
2295 for (std::size_t i = 0; i < n; ++i) col[i] = d.D1(i, j);
2296 lu_solve(LU, piv, col);
2297 for (std::size_t i = 0; i < n; ++i) P(i, j) = col[i];
2298 }
2299 const std::vector<T> alpha = mc::dtmc_solve(P);
2300 std::vector<T> y(n, one);
2301 lu_solve(LU, piv, y); // (I - D0)^-1 e
2302 std::vector<T> z = y;
2303 lu_solve(LU, piv, z); // (I - D0)^-2 e
2305 for (std::size_t i = 0; i < n; ++i) {
2306 m1 += T(alpha[i] * y[i]);
2307 T dz = num_traits<T>::from_int(0);
2308 for (std::size_t j = 0; j < n; ++j) dz += T(d.D0(i, j) * z[j]);
2309 fac2 += T(alpha[i] * dz);
2310 }
2311 fac2 = T(two * fac2);
2312 const T m2 = T(fac2 + m1);
2313 d.mean = m1;
2314 d.scv = T((m2 - m1 * m1) / (m1 * m1));
2315}
2316
2317} // namespace lang
2318} // namespace line
2319
2320#endif // LINE_LANG_LANG_TYPES_H
Cache(model, name, params).
Definition nodes.h:235
ClassSwitch(model, name, C).
Definition nodes.h:204
Delay(model, name): the infinite-server station.
Definition nodes.h:154
Fork(model, name).
Definition nodes.h:217
InputError(const std::string &what)
Definition error.h:105
Join(model, name, fork).
Definition nodes.h:224
std::size_t size() const
Definition matrix.h:91
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
NumericError(const std::string &what)
Definition error.h:112
Place(model, name): a Petri-net place.
Definition nodes.h:255
Queue(model, name, strategy).
Definition nodes.h:146
Router(model, name): a stateless routing node.
Definition nodes.h:198
Sink(model, name): the external departure node, which holds no jobs.
Definition nodes.h:192
Source(model, name): the external arrival station.
Definition nodes.h:160
Transition(model, name): a Petri-net transition, with its modes declared after it as MATLAB,...
Definition nodes.h:304
UnsupportedError(const std::string &what)
Definition error.h:119
Equilibrium distribution of a discrete-time Markov chain, and stochastic complementation.
The exception types the port throws.
LU factorization with partial pivoting, templated on the number type.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
Dense matrix and non-owning view.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:185
DropStrategy
Blocking and loss rules, with the values of MATLAB DropStrategy.
Definition lang_types.h:431
TimingStrategy
SPN transition timing, with the values of MATLAB TimingStrategy.
Definition lang_types.h:368
@ IMMEDIATE
fires with zero delay, resolved by weight and priority
Definition lang_types.h:370
@ TIMED
fires after its firing distribution elapses
Definition lang_types.h:369
BalkingStrategy
Balking rules, with the values of MATLAB BalkingStrategy.
Definition lang_types.h:452
SignalType
G-network signal classes, with the values of MATLAB SignalType.
Definition lang_types.h:171
@ REPLY
completes a synchronous call, releasing a held server
Definition lang_types.h:172
@ NEGATIVE
removes a batch of jobs (Gelenbe's negative customer)
Definition lang_types.h:173
@ CATASTROPHE
removes EVERY job at the station
Definition lang_types.h:174
bool process_is_batch(ProcessType p)
True when an EVENT of this process releases (or, as a service process, completes) a BATCH of jobs who...
Definition lang_types.h:713
JoinStrategy
Join rules, with the values of MATLAB JoinStrategy.
Definition lang_types.h:468
PrecedenceType
Activity precedence kinds, with the values of MATLAB ActivityPrecedenceType.
Definition lang_types.h:477
LqnElement
LQN element kinds, with the values of MATLAB LayeredNetworkElement.
Definition lang_types.h:471
RoutingStrategy
Routing strategies, with the values of MATLAB RoutingStrategy.
Definition lang_types.h:396
@ SDR
Krzesinski (1987) product-form state-dependent routing.
Definition lang_types.h:405
SchedStrategy sched_from_lqnx(const std::string &s)
Parse the scheduling attribute of an .lqnx processor or task.
Definition lang_types.h:282
DepartureDiscipline
When a Place releases a served token, MATLAB DepartureDiscipline.
Definition lang_types.h:465
bool process_is_marked(ProcessType p)
True for every marked family.
Definition lang_types.h:723
CallType
Call kinds, with the values of MATLAB CallType.
Definition lang_types.h:474
const char * metric_to_text(MetricType metric)
Port of MetricType.toText.
Definition lang_types.h:77
RemovalPolicy
Which job a negative signal removes, with the values of MATLAB RemovalPolicy.
Definition lang_types.h:178
@ LCFS
the newest waiting job; servers only once nobody waits
Definition lang_types.h:181
@ RANDOM
uniform over waiting AND in-service jobs
Definition lang_types.h:179
EventType
The events a state can undergo, with the values of MATLAB EventType.
Definition lang_types.h:115
@ PHASE
service advances a phase WITHOUT departing
Definition lang_types.h:120
@ READ
a cache item is read
Definition lang_types.h:121
@ FAILURE
the server breaks down, going from up to down
Definition lang_types.h:130
@ STAGE
a random environment changes stage
Definition lang_types.h:122
@ SWITCH
a polling server advances its switchover timer
Definition lang_types.h:129
@ LOCAL
dummy event, no state change outside the node
Definition lang_types.h:117
@ RENEGE
a waiting job abandons the queue (impatience)
Definition lang_types.h:127
@ POST
produce to a place or queue buffer
Definition lang_types.h:126
@ DEP
a job departs
Definition lang_types.h:119
@ START
a job begins or resumes holding a server
Definition lang_types.h:133
@ ENABLE
an SPN mode becomes enabled
Definition lang_types.h:123
@ FIRE
an SPN mode fires
Definition lang_types.h:124
@ RETRY
an orbiting job retries entry at a retrial station
Definition lang_types.h:128
@ REPAIR
the server is repaired, going from down to up, resuming the held job, which is why it emits no START
Definition lang_types.h:131
@ ARV
a job arrives
Definition lang_types.h:118
@ PRE
consume from a place or queue buffer, no server effect
Definition lang_types.h:125
@ INIT
the model is initialized, t = 0
Definition lang_types.h:116
@ PREEMPT
a job holding a server is pushed back into the buffer
Definition lang_types.h:134
MetricType
Solver output metrics, with the numeric values of MATLAB MetricType.
Definition lang_types.h:50
HeteroSchedPolicy
How a heterogeneous station picks among its server types, MATLAB HeteroSchedPolicy.
Definition lang_types.h:458
PollingType
Polling service disciplines, with the values of MATLAB PollingType.
Definition lang_types.h:377
@ KLIMITED
serve at most K per visit (K in pollingPar)
Definition lang_types.h:380
@ EXHAUSTIVE
serve until the queue empties
Definition lang_types.h:379
@ GATED
serve exactly the jobs present at the polling instant
Definition lang_types.h:378
@ DECREMENTING
serve until the queue is one shorter than at arrival
Definition lang_types.h:381
JobClassType
Job class kinds, with the values of MATLAB JobClassType.
Definition lang_types.h:374
std::string sched_to_lqnx(SchedStrategy s)
The scheduling attribute an .lqnx processor or task carries for a strategy.
Definition lang_types.h:311
bool process_is_marked_schedule(ProcessType p)
True when sn.proc holds the MARKED SCHEDULE slot.
Definition lang_types.h:693
const char * node_type_to_text(NodeType t)
Name of a node kind, for diagnostics.
Definition lang_types.h:348
ProcessType
Distribution kinds, with the values of MATLAB ProcessType.
Definition lang_types.h:490
@ NORMAL
A Gaussian, and the ONE family whose value is not MATLAB's, because MATLAB has none to copy: ProcessT...
Definition lang_types.h:596
@ BMMAPT
BMMAPT crosses the BATCH axis with the two above: a block is indexed by segment, mark and batch size,...
Definition lang_types.h:579
@ MPH
The MARKED families, MATLAB's ProcessType.m:36-38.
Definition lang_types.h:564
@ NHPP
The time-INHOMOGENEOUS families of Ko and Pender (ORL 45, 2017): an NHPP is a rate schedule lambda(t)...
Definition lang_types.h:543
@ PRIOR
A Prior: a weighted set of ALTERNATIVE distributions, or a density over a scalar parameter plus a fac...
Definition lang_types.h:517
void dmap_refresh_moments(Distrib< T > &d)
The first two moments of a DISCRETE-time MAP, from its own law.
std::function< std::vector< T >(const std::vector< T > &)> GdScaling
A globally state-dependent scaling, sn.gdscaling.
Definition lang_types.h:749
const char * sched_to_text(SchedStrategy s)
Definition lang_types.h:235
const char * process_to_text(ProcessType p)
The MATLAB ProcessType name, as sn.procid prints it.
Definition lang_types.h:601
bool process_is_markovian(ProcessType p)
ProcessType.isMarkovian: true when sn.proc carries an exact matrix representation of the law,...
Definition lang_types.h:655
bool process_is_marked_stationary(ProcessType p)
True when the type carries PER-MARK arrival blocks, i.e.
Definition lang_types.h:684
std::function< std::vector< T >(const std::vector< T > &)> CdScaling
A class-dependent scaling map, sn.cdscaling.
Definition lang_types.h:736
const char * routing_to_text(RoutingStrategy r)
Definition lang_types.h:409
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:331
const char * event_to_text(EventType e)
Definition lang_types.h:141
ReplacementStrategy
Cache replacement policies, with the values of MATLAB ReplacementStrategy.
Definition lang_types.h:385
@ HLRU
h-LRU / LRU(m): h lists, promote i -> i+1 on a hit
Definition lang_types.h:390
@ CLIMB
move up one position on a hit (transposition rule)
Definition lang_types.h:391
@ QLRU
q-LRU: LRU with probabilistic admission on a miss
Definition lang_types.h:392
@ LRU
least recently used
Definition lang_types.h:389
@ FIFO
first in, first out
Definition lang_types.h:387
ImpatienceType
Impatience kinds, with the values of MATLAB ImpatienceType.
Definition lang_types.h:449
T map_mean(const Map< T > &m)
Mean inter-arrival time, 1/lambda.
Definition map_moment.h:101
T map_scv(const Map< T > &m)
Squared coefficient of variation.
Definition map_moment.h:140
T map_lambda(const Map< T > &m)
Stationary arrival rate, lambda = pi D1 e.
Definition map_moment.h:79
std::vector< T > dtmc_solve(const Matrix< T > &P)
Stationary distribution of a stochastic matrix P.
Definition dtmc_solve.h:106
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
void lu_solve(const Matrix< T > &LU, const std::vector< std::size_t > &piv, std::vector< T > &b)
Solve LUx = Pb in place on b, using the factors from lu_factor.
Definition lu.h:94
std::vector< std::size_t > lu_factor(Matrix< T > &A)
In-place LU of A (n x n).
Definition lu.h:48
Number-type abstraction for the templated API port.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
bool is_immediate() const
static Distrib sched_dist(const std::vector< T > &breakpoints, const std::vector< Matrix< T > > &D0segs, const std::vector< Matrix< T > > &D1segs, bool cyclic, ProcessType tag)
The shared constructor of the three schedule families.
static Distrib empirical_cdf(const std::vector< T > &x, const std::vector< T > &F)
EmpiricalCDF(x, F): the moments of the MIDPOINT rule over the CDF bins, which is what MATLAB Empirica...
std::vector< std::vector< std::vector< Matrix< double > > > > sched_Dbatch
Definition lang_types.h:930
static Distrib nhpp(const std::vector< T > &breakpoints, const std::vector< T > &rates, bool cyclic)
NHPP(breakpoints, rates, cyclic): a MAPt of ORDER ONE, which is what an inhomogeneous Poisson process...
std::vector< T > mu_vec() const
Phase rates, MATLAB's getMu: the total outgoing rate of each phase.
static void check_common_support(const std::string &who, const std::string &label, const std::vector< Matrix< T > > &mats, bool ignore_diagonal)
Reject a schedule whose matrices do not share one sparsity pattern, the twin of MATLAB's MAPt....
static Distrib mph(const std::vector< T > &alpha, const Matrix< T > &S, const std::vector< std::vector< T > > &exits)
MPH(alpha, S, {s_k}): the marked phase-type law, a PH with K labelled exits.
bool has_map() const
True when the type carries a (D0,D1) pair of its own.
static Distrib mmapt(const std::vector< T > &breakpoints, const std::vector< Matrix< T > > &D0segs, const std::vector< std::vector< Matrix< T > > > &Dmarksegs, bool cyclic)
MMAPt(breakpoints, {D0_k}, {{D1^(c)_k}}, cyclic): the marked schedule.
std::vector< std::vector< Matrix< double > > > sched_Dmark
Definition lang_types.h:914
static Distrib replayer(const std::vector< double > &samples)
std::vector< double > sched_bp
Definition lang_types.h:906
static Distrib normal(const T &mu, const T &sigma)
Normal(mu, sigma): the Gaussian, for use as a continuous Prior's parameter density.
static Distrib dmap(const Matrix< T > &D0, const Matrix< T > &D1)
DMAP(D0, D1): a DISCRETE-time MAP, where D0 + D1 is stochastic rather than a generator.
static Distrib exp_rate(const T &r)
Definition lang_types.h:955
static Distrib phase_type(const std::vector< T > &alpha, const Matrix< T > &A, bool acyclic)
PH / APH given by (alpha, A): D0 = A and D1 = (-A e) alpha.
std::vector< Matrix< double > > Dmark
Definition lang_types.h:873
static Distrib mapt(const std::vector< T > &breakpoints, const std::vector< Matrix< T > > &D0segs, const std::vector< Matrix< T > > &D1segs, bool cyclic)
MAPt(breakpoints, {D0_k}, {D1_k}, cyclic): a piecewise-constant (D0(t), D1(t)).
std::shared_ptr< Distrib< double > > declared
Definition lang_types.h:831
static Distrib weibull(const T &scale, const T &shape)
static Distrib replayer_from(const std::vector< T > &samples, const std::string &path)
Replayer / Trace read FROM A FILE, which keeps the path beside the samples.
bool has_schedule() const
Definition lang_types.h:932
static Distrib bmap(const std::vector< Matrix< T > > &D)
BMAP: the batch-size blocks D0, D1, ..., Dk, where Dj carries an arrival of batch size j.
static Distrib mmap(const Matrix< T > &D0, const std::vector< Matrix< T > > &D1k)
MMAP: D0 plus one D1 block per mark.
static Distrib disabled_dist()
Definition lang_types.h:997
static Distrib pht(const std::vector< T > &breakpoints, const std::vector< std::vector< T > > &alphas, const std::vector< Matrix< T > > &Ssegs, bool cyclic)
PHt(breakpoints, {alpha_k}, {S_k}, cyclic), stored as its equivalent MAP schedule: D0 = S and D1 = (-...
static double ph_moment(const std::vector< double > &alpha, const Matrix< double > &A, unsigned k)
static Distrib erlang_fit(const T &m, const T &c2)
Erlang fitted to a mean and an SCV, as MATLAB's Erlang.fitMeanAndSCV.
static Distrib pareto(const T &shape, const T &scale)
Pareto(shape, scale), with the MATLAB parameter order (alpha, k).
std::vector< double > params
Definition lang_types.h:836
static Distrib gamma_dist(const T &shape, const T &scale)
Gamma(shape, scale), Weibull(scale, shape) and Lognormal(mu, sigma).
static Distrib cox2(const T &mu1, const T &mu2, const T &phi1)
Cox2(mu1, mu2, phi1), MATLAB's two-phase Coxian constructor.
static Distrib map_dist(const Matrix< T > &D0, const Matrix< T > &D1, ProcessType tag)
A MAP given by its two matrices; the moments are those of its stationary phase.
static Distrib poisson(const T &lambda)
Poisson(lambda), whose SCV is 1/lambda – the count's variance is lambda and its mean is lambda,...
std::vector< double > trace
Definition lang_types.h:838
static Distrib bernoulli(const T &p)
Bernoulli(p): one trial, mean p and variance p(1-p).
static double ph_moment_from(const Matrix< double > &A, std::size_t start, unsigned k)
static Distrib hyperexp_n(const std::vector< T > &p, const std::vector< T > &lambda)
The same for any number of branches, which is what MATLAB HyperExp accepts and what the writers emit ...
static Distrib discrete_uniform(const T &a, const T &b)
DiscreteUniform(a, b) over the integers a..b inclusive.
std::shared_ptr< PriorSpec< double > > prior
Definition lang_types.h:887
static Distrib geometric(const T &p)
Geometric(p) on the MATLAB convention: the NUMBER OF TRIALS to the first success, support {1,...
static Distrib det(const T &m)
Definition lang_types.h:998
static Distrib bmmapt(const std::vector< T > &breakpoints, const std::vector< Matrix< T > > &D0segs, const std::vector< std::vector< std::vector< Matrix< T > > > > &Dbatchsegs, bool cyclic)
BMMAPt(breakpoints, {D0_k}, {{{D^(c,b)_k}}}, cyclic): the BATCH marked schedule.
static Distrib rap(const Matrix< T > &H0, const Matrix< T > &H1)
RAP(H0, H1): a rational arrival process, whose moments are the MAP ones.
bool has_batch_schedule() const
Definition lang_types.h:934
static Distrib uniform(const T &a, const T &b)
Uniform(a, b).
std::vector< Matrix< double > > sched_D1
Definition lang_types.h:907
static Distrib lognormal(const T &logmean, const T &logsigma)
static Distrib hyperexp(const T &p, const T &lambda1, const T &lambda2)
HyperExp(p, lambda1, lambda2): phase i chosen with probability p_i.
std::vector< Matrix< double > > sched_D0
Definition lang_types.h:907
static Distrib me(const std::vector< T > &alpha, const Matrix< T > &A)
ME(alpha, A): the matrix-exponential distribution, whose moments are the phase-type ones – k!
static Distrib immediate()
The point mass at zero.
Definition lang_types.h:986
std::vector< T > phi_vec() const
Completion probabilities, MATLAB's getPhi: (D1 e) .
static Distrib erlang(const T &phase_rate, std::size_t r)
Erlang(alpha, r): r phases of rate alpha, as MATLAB's Erlang(phaseRate, nphases).
bool has_marked_schedule() const
Definition lang_types.h:933
static Distrib exp_mean(const T &m)
Definition lang_types.h:940
std::size_t max_batch_size() const
Largest batch size the schedule declares, or 1 when it carries no batch axis.
Definition lang_types.h:936
static Distrib mpht(const std::vector< T > &breakpoints, const std::vector< std::vector< T > > &alphas, const std::vector< Matrix< T > > &Ssegs, const std::vector< std::vector< std::vector< T > > > &exits, bool cyclic)
MPHt(breakpoints, {alpha_k}, {S_k}, {{s^(c)_k}}, cyclic), stored LOWERED to MMAPt form segment by seg...
std::size_t phases() const
The order of the representation, MATLAB's sn.phases.
static void check_sched_generator(const std::string &who, const std::vector< Matrix< T > > &D0segs, const std::vector< std::vector< Matrix< T > > > &blocks)
Reject a schedule segment that is not a generator, the check MATLAB, the JAR and Python all apply and...
static Distrib coxian(const std::vector< T > &mu, const std::vector< T > &phi)
Coxian(mu, phi): phase i completes with probability phi(i) and otherwise moves to phase i+1.
bool is_prior() const
Definition lang_types.h:888
static Distrib discrete_sampler(const std::vector< T > &p, const std::vector< T > &x)
DiscreteSampler(p, x): the pmf p over the points x.
static Distrib binomial(const T &n, const T &p)
Binomial(n, p).
static Distrib zipf(const T &s, std::size_t n)
Zipf(s, n) over the ranks 1..n, with the generalized harmonic moments H(s-1,n)/H(s,...
The MATLAB GlobalConstants, as reported by lineStart at its defaults.
Definition lang_types.h:764
static constexpr double Immediate
Rate of an Immediate distribution; its mean is 1/Immediate = 1e-8.
Definition lang_types.h:771
static constexpr double FineTol
Definition lang_types.h:765
static constexpr double ArcTol
Below this an off-diagonal entry is NO ARC of the phase / state graph.
Definition lang_types.h:769
static constexpr double Zero
Definition lang_types.h:767
static constexpr double CoarseTol
Definition lang_types.h:766
static constexpr double MaxInt
Stand-in for an unbounded COUNT, MATLAB GlobalConstants.MaxInt.
Definition lang_types.h:776
A LINE Distribution, as the model layer and sn carry it.
std::function< Distrib< T >(const T &)> factory
theta -> Distribution; the continuous form only.
bool continuous
True for the parameter-density form, false for the alternative-set form.
Distrib< T > param_dist
The law of the scalar parameter; the continuous form only.
std::vector< T > probabilities
std::vector< Distrib< T > > alternatives
The alternatives and their weights; the discrete form only.
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
Matrix< T > D1
Definition map_moment.h:55
Matrix< T > D0
Definition map_moment.h:54