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