LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
environment.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_QN_ENVIRONMENT_H
6#define LINE_LANG_QN_ENVIRONMENT_H
7
8/**
9 * @file
10 * @ingroup line_lang
11 * A random environment: a port of `matlab/src/lang/Environment.m`, restricted
12 * to what `SolverENV` reads out of it.
13 *
14 * WHAT IT MODELS. A queueing network whose PARAMETERS change over time because
15 * the world around it does: a server that breaks down and is repaired, a
16 * workload that has a day phase and a night phase, a system that degrades and
17 * is reset. Each of those is a STAGE, holding its own complete network, and the
18 * environment is a semi-Markov process over the stages. The point is that the
19 * network never restarts empty at a switch -- the jobs in it at the moment of
20 * the switch are carried into the next stage, which is what couples the stages
21 * and what makes this more than solving each stage separately.
22 *
23 * THE HOLDING TIME IS A COMPETING RISK, and this is the part worth reading the
24 * reference for. Every enabled transition e -> h has its own distribution, and
25 * they all run at once: the stage ends when the FIRST of them fires, and which
26 * one fired decides the next stage. `init()` therefore superposes the outgoing
27 * transitions of e by a Kronecker sum into a single marked process
28 * `hold_time[e]`, whose per-mark rates give the embedded jump chain `Pemb`.
29 * The collapse that follows -- each block's row sums moved into its FIRST
30 * column -- is the reference's, and it makes the superposed process restart in
31 * phase one after every jump, i.e. renewal at each stage entry.
32 *
33 * WHAT init() PRODUCES, and what the solver consumes:
34 * proc[e][h] the e -> h transition process, whose CDF weights the stage
35 * transient when computing the metrics AT a switch to h
36 * hold_time[e] the superposed holding time of stage e, whose CDF weights the
37 * transient when computing the metrics OVER the whole stage
38 * prob_env[e] the stationary probability of being in stage e
39 * prob_orig prob_orig(h, e) = P(the previous stage was h | now entering e)
40 *
41 * A SEMI-MARKOV STAGE has no such MAP form: a stage with a non-Markovian arc
42 * (Det, Gamma, Weibull, ...) or with KERNEL arcs (`add_kernel_transition`, a
43 * jump probability and a conditional sojourn law per arc). `init()` then reads
44 * its embedded jump row and mean holding time off the arc CDFs by
45 * `mc::smp_stage_quad`, leaves `hold_time[e]` and `proc[e][h]` as the disabled
46 * pair, and `hold_time_cdf` / `hold_time_mean` evaluate the holding-time law
47 * from the arcs. Only SolverENV method `smp` solves such an environment.
48 *
49 * A DISABLED transition is the 1 x 1 zero pair, exactly as in the reference:
50 * krons(A, 0) leaves A unchanged, so a disabled arc costs nothing and needs no
51 * special case anywhere below.
52 *
53 * NODE BREAKDOWNS are the one stage pattern the reference gives a name to
54 * (`addNodeBreakdown` / `addNodeRepair` / `addNodeFailureRepair`): the UP stage
55 * holds the base network, the DOWN_<node> stage holds the same network with one
56 * node's service replaced by its degraded distribution, and the two arcs
57 * between them are the time to failure and the time to repair. It is a macro
58 * over `set_stage` and `add_transition` and nothing more, EXCEPT for the pair of
59 * queue-length reset policies it attaches, which are the only part of a
60 * breakdown that the expanded stages cannot express -- hence `NodeFailure`,
61 * which records them beside the stages so an environment read back from
62 * `model.json` is the same model it was written from.
63 */
64
65#include <cctype>
66#include <cmath>
67#include <cstddef>
68#include <cstdio>
69#include <functional>
70#include <limits>
71#include <string>
72#include <vector>
73
83#include "line/lang/prior.h"
85#include "line/util/error.h"
86#include "line/util/matrix.h"
87
88namespace line {
89namespace env {
90
91/**
92 * The reset policy of a transition, `resetFun` in the reference.
93 *
94 * It maps the mean queue lengths at the moment of the switch onto the mean
95 * queue lengths the next stage starts from. Identity means the jobs are simply
96 * carried over; zero means the buffer is flushed on the switch.
97 */
98using ResetMarginal = std::function<Matrix<double>(const Matrix<double>&)>;
99
100/**
101 * The two NAMED reset policies of `Environment.resolveResetPolicy`, which are
102 * the only ones the JSON interchange can carry (a function handle is written as
103 * `custom` and warned about by both writers, never reloaded).
104 *
105 * `keep` resolves to the EMPTY function rather than to an explicit identity,
106 * because empty is how this port spells identity everywhere a reset is read:
107 * `SolverEnv::post` skips the call, and the compression's macro-arc fold
108 * compares resets only by whether one is PRESENT, so an explicit identity on
109 * one arc and nothing on another would be refused as a disagreement although
110 * the two mean the same thing.
111 */
112inline ResetMarginal env_reset_policy(const std::string& name) {
113 std::string low;
114 low.reserve(name.size());
115 for (std::size_t i = 0; i < name.size(); ++i)
116 low.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(name[i]))));
117 if (low == "keep") return ResetMarginal();
118 if (low == "clear")
119 return [](const Matrix<double>& q) { return Matrix<double>(q.rows(), q.cols(), 0.0); };
120 throw InputError("Environment: unknown reset policy '" + name +
121 "'. The serializable policies are 'keep' (carry the queue lengths across "
122 "the switch) and 'clear' (empty the queues); a custom policy is a function "
123 "and is installed through add_transition, not by name");
124}
125
126/**
127 * `Environment.nodeFailures{k}`: the declarative record of one node breakdown.
128 *
129 * The expanded stages and transitions already carry the STRUCTURE of a
130 * breakdown; what only this record carries is the pair of reset policies, which
131 * decide what the next stage starts from and therefore change the numbers. It
132 * is kept on the environment for the same reason the reference keeps it: so an
133 * environment read from `model.json` in its expanded form can be given back its
134 * policies, and so it serializes out again as the same model.
135 */
136template <class T>
138 std::string node; ///< the node that breaks down
139 lang::Distrib<T> breakdown; ///< time to failure, the UP -> DOWN transition
140 lang::Distrib<T> down_service; ///< the node's service while it is down
141 lang::Distrib<T> repair; ///< time to repair, the DOWN -> UP transition
142 bool has_repair = false; ///< false for a breakdown with no repair arc
143 std::string breakdown_reset = "keep";
144 std::string repair_reset; ///< empty when there is no repair
145};
146
147/**
148 * One stage: a name, a category, and the model in force while it lasts.
149 *
150 * THE MODEL IS ONE OF TWO KINDS, and which one is the stage's own property
151 * rather than the environment's: a flat `qn::NetworkStruct`, or a
152 * `lqn::LqnStruct` -- a LayeredNetwork, whose stations and classes are those of
153 * the LAYERS SolverLN builds out of it. The reference draws no distinction at
154 * this level either (`Environment.addStage` stores whatever model it is handed
155 * and `SolverENV` branches on its class), and the two fields are kept apart
156 * rather than unified because nothing about them is shared below the name: a
157 * flat stage is integrated directly, a layered one through a whole fixed point
158 * over its layers.
159 *
160 * `has_model` and `has_lqn` are mutually exclusive; `set_stage` and
161 * `set_lqn_stage` each clear the other, so a stage re-declared as the other
162 * kind cannot leave a stale twin behind for a later consumer to read.
163 */
164template <class T>
165struct EnvStage {
166 std::string name;
167 std::string type; ///< the stage category, informational only
169 bool has_model = false;
170 /** The layered model, when this stage holds a LayeredNetwork. */
172 bool has_lqn = false;
173 /** True when the stage carries a model of either kind. */
174 bool declared() const { return has_model || has_lqn; }
175};
176
177/**
178 * `resetEnvRatesFun` in the reference: the state-dependent environment rate.
179 *
180 * It is given the arc's CURRENT transition distribution and the exit metrics of
181 * the stage the arc leaves -- mean queue lengths, utilizations and throughputs,
182 * averaged over when that arc fires -- and returns the distribution the arc
183 * should carry next. It is what makes the environment process depend on the
184 * network it modulates, and `method = "statedep"` is what applies it.
185 */
186template <class T>
188 std::function<lang::Distrib<T>(const lang::Distrib<T>&, const Matrix<double>&,
189 const Matrix<double>&, const Matrix<double>&)>;
190
191/** One arc of the environment process. */
192template <class T>
193struct EnvArc {
194 bool enabled = false;
195 lang::Distrib<T> dist; ///< the e -> h transition time
196 ResetMarginal reset; ///< empty means the identity
197 ResetEnvRates<T> reset_rates; ///< empty means the rate does not depend on the state
198};
199
200/**
201 * The DOWN stage network of `addNodeBreakdown`: the base network with ONE
202 * node's service replaced by its degraded distribution, for EVERY class.
203 *
204 * Every class, and not only the ones that were enabled there, is what the
205 * reference does (`for c = 1:length(classes), nodes{nodeIdx}.setService(...)`),
206 * so a class that was disabled at the node while it was up is served at the
207 * degraded rate while it is down. The whole refresh chain is rerun afterwards
208 * because `rates`, `scv` and the chain-derived tables are all read off the
209 * service table; editing the table alone would leave the struct describing the
210 * UP stage and the solver reading the DOWN one.
211 */
212template <class T>
214 const std::string& node_name,
215 const lang::Distrib<T>& down_service) {
217 std::size_t node = 0;
218 for (std::size_t i = 0; i < sn.nodes.size(); ++i)
219 if (sn.nodes[i].name == node_name) {
220 node = i + 1;
221 break;
222 }
223 if (node == 0)
224 throw InputError("Environment: node '" + node_name +
225 "' is not in the base model, so it has no service to degrade");
226 const std::size_t ist = sn.nodes[node - 1].station;
227 if (ist == 0)
228 throw InputError("Environment: node '" + node_name +
229 "' is not a station and carries no service distribution, so it cannot "
230 "break down into a degraded service");
231 lang::Distrib<T> d = down_service;
232 if (d.is_prior())
234 else
236 for (std::size_t r = 1; r <= sn.classes.size(); ++r) sn.set_service(ist, r, d);
237 sn.refresh_struct();
238 return sn;
239}
240
241template <class T>
243public:
244 Environment(const std::string& nm, std::size_t nstages)
245 : name_(nm), stages_(nstages), arcs_(nstages, std::vector<EnvArc<T>>(nstages)),
246 kernel_prob_(nstages, nstages, std::numeric_limits<double>::quiet_NaN()) {
247 if (nstages == 0) throw InputError("Environment: a random environment needs a stage");
248 }
249
250 std::size_t nstages() const { return stages_.size(); }
251 const std::string& name() const { return name_; }
252
253 /** `addStage`: name the stage and give it its network. */
254 void set_stage(std::size_t e, const std::string& nm, const std::string& type,
255 const qn::NetworkStruct<T>& model) {
256 check(e);
257 stages_[e].name = nm;
258 stages_[e].type = type;
259 stages_[e].model = model;
260 stages_[e].has_model = true;
261 stages_[e].has_lqn = false;
262 stages_[e].lqn_model = lqn::LqnStruct<T>();
263 }
264
265 /**
266 * `addStage` with a LayeredNetwork: the stage holds a LAYERED model.
267 *
268 * The environment itself does nothing with the difference -- probEnv,
269 * probOrig and the holding times are read off the ARCS and never off a
270 * stage model -- so the whole content of this overload is that the stage
271 * records which kind of model it carries and `SolverEnv` runs the matching
272 * stage solver. What the two kinds must still agree on is the (station,
273 * class) SHAPE of the metrics being blended, and for a layered stage that
274 * shape is the block-diagonal union of its layers; `SolverEnv::init` is
275 * where the shapes are compared, because only there is a SolverLN built and
276 * the layer blocks known.
277 */
278 void set_lqn_stage(std::size_t e, const std::string& nm, const std::string& type,
279 const lqn::LqnStruct<T>& model) {
280 check(e);
281 // compare LQN elements, not layers: a zero-capacity stage drops the layers
282 // only its trapped reference tasks reach (SolverLN, ln_zero_capacity)
283 for (std::size_t h = 0; h < stages_.size(); ++h)
284 if (h != e && stages_[h].has_lqn &&
285 (stages_[h].lqn_model.nidx != model.nidx || stages_[h].lqn_model.tshift != model.tshift))
286 throw InputError(ErrorKind{"Model"},
287 "Unsupported feature. Random environment stages must map to layered networks "
288 "with the same hosts, tasks, entries and activities.");
289 stages_[e].name = nm;
290 stages_[e].type = type;
291 stages_[e].lqn_model = model;
292 stages_[e].has_lqn = true;
293 stages_[e].has_model = false;
294 stages_[e].model = qn::NetworkStruct<T>();
295 }
296
297 /** True when stage `e` holds a LayeredNetwork rather than a flat network. */
298 bool is_lqn(std::size_t e) const {
299 check(e);
300 return stages_[e].has_lqn;
301 }
302
303 /** True when ANY stage holds a LayeredNetwork. */
304 bool has_lqn_stages() const {
305 for (std::size_t e = 0; e < stages_.size(); ++e)
306 if (stages_[e].has_lqn) return true;
307 return false;
308 }
309
310 /**
311 * Refuse an environment carrying a LayeredNetwork stage, by name.
312 *
313 * Shared by every consumer that reads `stage(e).model` directly -- the
314 * state-vector coupling, the closed-form limits, the compression -- so that
315 * each says the same thing about the same gap rather than reading an EMPTY
316 * NetworkStruct and reporting a confident answer about a model that is not
317 * there. `who` names the caller and `why` says what about it needs a flat
318 * stage.
319 */
320 void reject_lqn_stages(const std::string& who, const std::string& why) const {
321 for (std::size_t e = 0; e < stages_.size(); ++e)
322 if (stages_[e].has_lqn)
323 throw UnsupportedError(ErrorKind{"LayeredNetwork"},
324 who + ": stage " + std::to_string(e + 1) + " ('" + stages_[e].name +
325 "') holds a LayeredNetwork, and " + why +
326 ". The mean-field coupling (method 'default' / 'meanfield') is the one that "
327 "solves a layered stage, through SolverLN");
328 }
329
330 /**
331 * `addTransition`: enable e -> h with a distribution and a reset policy.
332 *
333 * A COMPETING arc: `d` is a latent clock started on entry to the stage, and
334 * the first of the stage's clocks to ring picks the destination. Any law is
335 * accepted; a non-Markovian one makes the environment semi-Markov, which
336 * only SolverENV method `smp` solves.
337 */
338 void add_transition(std::size_t e, std::size_t h, const lang::Distrib<T>& d,
339 const ResetMarginal& reset = ResetMarginal()) {
340 check(e);
341 check(h);
342 arcs_[e][h].enabled = true;
343 arcs_[e][h].dist = d;
344 arcs_[e][h].reset = reset;
345 kernel_prob_(e, h) = std::numeric_limits<double>::quiet_NaN();
346 }
347
348 /**
349 * `addKernelTransition`: a KERNEL arc of Q(e,h,t) = prob * F(t).
350 *
351 * On entry to e the next stage is h with probability `prob`, and the sojourn
352 * is then distributed as `d`: the sojourn-time parameterization (Approach I
353 * of Asanjarani et al.), beside `add_transition`'s competing clocks (Approach
354 * II). A stage's arcs must be all kernel or all competing, and the
355 * probabilities of a kernel stage must sum to one (`stage_arcs`). Only
356 * SolverENV method `smp` solves an environment with kernel arcs.
357 */
358 void add_kernel_transition(std::size_t e, std::size_t h, double prob,
359 const lang::Distrib<T>& d,
360 const ResetMarginal& reset = ResetMarginal()) {
361 if (!(prob > 0.0 && prob <= 1.0))
362 throw InputError("Environment: the probability of a kernel transition must be a "
363 "scalar in (0,1].");
364 add_transition(e, h, d, reset);
365 kernel_prob_(e, h) = prob;
366 }
367
368 /** `kernelProb(e,h)`: the kernel arc's probability, NaN for a competing arc or none. */
369 double kernel_prob(std::size_t e, std::size_t h) const {
370 check(e);
371 check(h);
372 return kernel_prob_(e, h);
373 }
374
375 /** `isKernelStage`: true if stage e's arcs are kernel arcs. */
376 bool is_kernel_stage(std::size_t e) const {
377 check(e);
378 for (std::size_t h = 0; h < stages_.size(); ++h)
379 if (!std::isnan(kernel_prob_(e, h))) return true;
380 return false;
381 }
382
383 /**
384 * `isSemiMarkovStage`: true if stage e's holding time has no Markovian (MAP)
385 * form, i.e. it has kernel arcs or a competing arc whose law is not
386 * Markovian (the reference's `isa(arc, 'Markovian')`).
387 */
388 bool is_semi_markov_stage(std::size_t e) const {
389 if (is_kernel_stage(e)) return true;
390 for (std::size_t h = 0; h < stages_.size(); ++h) {
391 const EnvArc<T>& a = arcs_[e][h];
392 if (a.enabled && !a.dist.disabled && !arc_is_markovian(a.dist)) return true;
393 }
394 return false;
395 }
396
397 /**
398 * `isAbsorbingStage`: true if stage e has no outgoing transition, so once
399 * entered the environment stays there with an infinite holding time. A
400 * self arc does not count as a way out.
401 */
402 bool is_absorbing_stage(std::size_t e) const {
403 check(e);
404 if (is_kernel_stage(e)) return false;
405 for (std::size_t h = 0; h < stages_.size(); ++h)
406 if (h != e && arcs_[e][h].enabled && !arcs_[e][h].dist.disabled) return false;
407 return true;
408 }
409
410 /**
411 * Refuse a stage with no model. Runs ahead of the structural refusals, since
412 * an undeclared stage also has no arcs and would otherwise be reported as absorbing.
413 */
414 void require_declared() const {
415 for (std::size_t e = 0; e < stages_.size(); ++e)
416 if (!stages_[e].declared())
417 throw InputError("Environment: stage " + std::to_string(e + 1) + " has no model");
418 }
419
420 /** True if some stage is absorbing, i.e. `init()` left probEnv empty. */
421 bool is_absorbing() const {
422 for (std::size_t e = 0; e < stages_.size(); ++e)
423 if (is_absorbing_stage(e)) return true;
424 return false;
425 }
426
427 /**
428 * Refuse an absorbing environment for a coupling that reads probEnv: its
429 * limit depends on the initial stage, so it has no single steady state.
430 */
431 void refuse_absorbing(const std::string& method) const {
432 for (std::size_t e = 0; e < stages_.size(); ++e)
433 if (is_absorbing_stage(e))
434 throw UnsupportedError(ErrorKind{"Structure"},
435 "Stage " + std::to_string(e + 1) + " ('" + stages_[e].name +
436 "') has no outgoing transition, so the environment is absorbing and has no "
437 "single steady state: its limit depends on the initial stage. Method '" +
438 method + "' reads the stationary stage probabilities; use method "
439 "'occupation', with options.config.env_init naming the initial stage law.");
440 }
441
442 /** `isSemiMarkov`: true if any stage is semi-Markov. */
443 bool is_semi_markov() const {
444 for (std::size_t e = 0; e < stages_.size(); ++e)
445 if (is_semi_markov_stage(e)) return true;
446 return false;
447 }
448
449 /**
450 * `getStageArcs`: the transitions out of stage e in the form
451 * `mc::smp_stage_cif` takes. `arcs[h]` is the law of e -> h, DISABLED where
452 * there is none (and for h == e), and `probs` is empty for competing arcs or
453 * holds the kernel probabilities, validated as the reference does.
454 */
455 void stage_arcs(std::size_t e, std::vector<lang::Distrib<T>>& arcs,
456 std::vector<double>& probs) const {
457 check(e);
458 const std::size_t E = stages_.size();
459 arcs.assign(E, lang::Distrib<T>());
460 for (std::size_t h = 0; h < E; ++h)
461 if (h != e && arcs_[e][h].enabled && !arcs_[e][h].dist.disabled)
462 arcs[h] = arcs_[e][h].dist;
463 probs.clear();
464 if (!is_kernel_stage(e)) return;
465 probs.assign(E, 0.0);
466 double tot = 0.0;
467 for (std::size_t h = 0; h < E; ++h) {
468 if (!arcs[h].disabled) {
469 if (std::isnan(kernel_prob_(e, h)))
470 throw InputError("Environment: stage " + std::to_string(e + 1) +
471 " mixes kernel and competing transitions (the one to stage " +
472 std::to_string(h + 1) +
473 " was added by addTransition). Declare all of a stage's "
474 "transitions with addKernelTransition, or none.");
475 probs[h] = kernel_prob_(e, h);
476 tot += probs[h];
477 } else if (!std::isnan(kernel_prob_(e, h))) {
478 throw InputError("Environment: the kernel transition " + std::to_string(e + 1) +
479 "->" + std::to_string(h + 1) + " has no law.");
480 }
481 }
482 if (std::fabs(tot - 1.0) > 1e-8) {
483 char buf[64];
484 std::snprintf(buf, sizeof buf, "%.12g", tot);
485 throw InputError("Environment: the kernel probabilities out of stage " +
486 std::to_string(e + 1) + " sum to " + buf + ", not 1.");
487 }
488 }
489
490 /**
491 * The `methodRefusal` rule for every coupling but `smp`: refuse a kernel
492 * stage, or a competing arc that is not Markovian, with the reference's
493 * sentences. Those couplings read each stage's holding time as a (D0,D1)
494 * race of its transitions, which neither has.
495 */
496 void refuse_semi_markov(const std::string& method) const {
497 if (method == "smp") return;
498 const std::size_t E = stages_.size();
499 for (std::size_t e = 0; e < E; ++e) {
500 if (is_kernel_stage(e))
501 throw UnsupportedError(ErrorKind{"Structure"},
502 "Stage " + std::to_string(e + 1) +
503 " has kernel transitions (addKernelTransition), which only method 'smp' of "
504 "SolverENV solves: the other couplings read each stage's holding time as a "
505 "Markovian (D0,D1) race of its transitions.");
506 for (std::size_t h = 0; h < E; ++h) {
507 const EnvArc<T>& a = arcs_[e][h];
508 if (!a.enabled || a.dist.disabled || arc_is_markovian(a.dist)) continue;
509 throw UnsupportedError(ErrorKind{"Structure"},
510 "The distribution of the environment transition from stage " +
511 std::to_string(e + 1) + " to " + std::to_string(h + 1) + " is a " +
512 lang::process_to_text(a.dist.type) + ", which is not Markovian: method '" +
513 method +
514 "' reads every arc as a (D0,D1) process to build the holding-time laws. Use "
515 "method 'smp', which solves the semi-Markov environment, or fit the arc to a "
516 "phase-type law, e.g. APH.fitMeanAndSCV.");
517 }
518 }
519 }
520
521 const EnvStage<T>& stage(std::size_t e) const {
522 check(e);
523 return stages_[e];
524 }
525 const EnvArc<T>& arc(std::size_t e, std::size_t h) const {
526 check(e);
527 check(h);
528 return arcs_[e][h];
529 }
530
531 /**
532 * `resetEnvRatesFun{e,h}`: make the e -> h transition depend on the state
533 * the stage is left in. Only `method = "statedep"` reads it.
534 */
535 void set_env_rate_reset(std::size_t e, std::size_t h, const ResetEnvRates<T>& f) {
536 check(e);
537 check(h);
538 if (!arcs_[e][h].enabled)
539 throw InputError("Environment: no transition " + stages_[e].name + " -> " +
540 stages_[h].name +
541 " is declared, so its rate cannot be made state dependent");
542 arcs_[e][h].reset_rates = f;
543 }
544
545 /**
546 * Replace the distribution of an arc that is already declared.
547 *
548 * This is what the state-dependent method writes back each iteration; every
549 * other caller declares the arc once through `add_transition`. `init()` must
550 * be rerun afterwards, because the superposed holding times and the stage
551 * probabilities are all derived from these distributions.
552 */
553 void set_transition_dist(std::size_t e, std::size_t h, const lang::Distrib<T>& d) {
554 check(e);
555 check(h);
556 if (!arcs_[e][h].enabled)
557 throw InputError("Environment: no transition " + stages_[e].name + " -> " +
558 stages_[h].name + " is declared, so its distribution cannot be set");
559 arcs_[e][h].dist = d;
560 }
561
562 /** The index of the stage called `nm`, or `nstages()` when there is none. */
563 std::size_t find_stage(const std::string& nm) const {
564 for (std::size_t e = 0; e < stages_.size(); ++e)
565 if (stages_[e].has_model && stages_[e].name == nm) return e;
566 return stages_.size();
567 }
568
569 /**
570 * Install a reset policy on an arc that is already declared.
571 *
572 * The arc must exist: a reset on a disabled arc is a policy for a switch
573 * the environment cannot make, and it would sit there reporting nothing.
574 * The reference reaches this through `setBreakdownResetPolicy`, which
575 * likewise errors when the stages it names are absent.
576 */
577 void set_reset(std::size_t e, std::size_t h, const ResetMarginal& reset) {
578 check(e);
579 check(h);
580 if (!arcs_[e][h].enabled)
581 throw InputError("Environment: no transition " + stages_[e].name + " -> " +
582 stages_[h].name +
583 " is declared, so it cannot be given a reset policy");
584 arcs_[e][h].reset = reset;
585 }
586
587 // ---- node breakdown and repair ---------------------------------------
588
589 /** `nodeFailures`, the declarative record of the breakdowns declared here. */
590 const std::vector<NodeFailure<T>>& node_failures() const { return node_failures_; }
591
592 /** `findNodeFailure`: the descriptor for `nm`, or `node_failures().size()`. */
593 std::size_t find_node_failure(const std::string& nm) const {
594 for (std::size_t i = 0; i < node_failures_.size(); ++i)
595 if (node_failures_[i].node == nm) return i;
596 return node_failures_.size();
597 }
598
599 /** The name `addNodeBreakdown` gives the stage in which `nm` is down. */
600 static std::string down_stage_name(const std::string& nm) { return "DOWN_" + nm; }
601
602 /**
603 * Port of `addNodeBreakdown`, on a FIXED stage count.
604 *
605 * The reference grows its stage graph as breakdowns are declared; this
606 * environment is sized at construction, exactly as `set_stage` is, so the
607 * caller says which slot is UP and which is the DOWN stage of this node.
608 * Everything else is the reference's: the UP stage holds the base model and
609 * is named `UP`, the DOWN stage holds the degraded copy and is named
610 * `DOWN_<node>`, and the UP -> DOWN arc carries the breakdown time and the
611 * breakdown reset policy.
612 */
613 void add_node_breakdown(std::size_t up, std::size_t down, const qn::NetworkStruct<T>& base,
614 const std::string& node_name, const lang::Distrib<T>& breakdown,
615 const lang::Distrib<T>& down_service,
616 const std::string& reset_policy = "keep") {
617 check(up);
618 check(down);
619 if (up == down)
620 throw InputError("Environment: the UP and DOWN stages of node '" + node_name +
621 "' must be different stages");
622 if (!stages_[up].has_model) set_stage(up, "UP", "operational", base);
623 set_stage(down, down_stage_name(node_name), "failed",
624 env_degraded_model(base, node_name, down_service));
625 add_transition(up, down, breakdown, env_reset_policy(reset_policy));
626
628 nf.node = node_name;
629 nf.breakdown = breakdown;
630 nf.down_service = down_service;
631 nf.breakdown_reset = reset_policy;
632 record_node_failure(nf);
633 }
634
635 /** Port of `addNodeRepair`: the DOWN_<node> -> UP arc and its policy. */
636 void add_node_repair(const std::string& node_name, const lang::Distrib<T>& repair,
637 const std::string& reset_policy = "keep") {
638 const std::size_t down = find_stage(down_stage_name(node_name));
639 const std::size_t up = find_stage("UP");
640 if (down == stages_.size())
641 throw InputError("Environment: stage '" + down_stage_name(node_name) +
642 "' is not defined; declare the breakdown of node '" + node_name +
643 "' before its repair");
644 if (up == stages_.size())
645 throw InputError("Environment: no UP stage is defined, so node '" + node_name +
646 "' has nothing to be repaired into");
647 add_transition(down, up, repair, env_reset_policy(reset_policy));
648
649 const std::size_t idx = find_node_failure(node_name);
650 if (idx == node_failures_.size())
651 throw InputError("Environment: no breakdown is recorded for node '" + node_name +
652 "', so its repair would describe a failure that was never declared");
653 node_failures_[idx].repair = repair;
654 node_failures_[idx].has_repair = true;
655 node_failures_[idx].repair_reset = reset_policy;
656 }
657
658 /** `addNodeFailureRepair`: the two calls above, in order. */
659 void add_node_failure_repair(std::size_t up, std::size_t down,
660 const qn::NetworkStruct<T>& base, const std::string& node_name,
661 const lang::Distrib<T>& breakdown, const lang::Distrib<T>& repair,
662 const lang::Distrib<T>& down_service,
663 const std::string& breakdown_reset = "keep",
664 const std::string& repair_reset = "keep") {
665 add_node_breakdown(up, down, base, node_name, breakdown, down_service, breakdown_reset);
666 add_node_repair(node_name, repair, repair_reset);
667 }
668
669 /**
670 * Port of `registerNodeFailure`: attach a breakdown descriptor, and its
671 * reset policies, to stages that ALREADY exist.
672 *
673 * This is the read path of an environment saved in its expanded form: the
674 * `UP` and `DOWN_<node>` stages and their two arcs came off the wire, and
675 * the only thing the wire could not carry is the pair of policies, which is
676 * what this installs.
677 */
678 void register_node_failure(const std::string& node_name, const lang::Distrib<T>& breakdown,
679 const lang::Distrib<T>& down_service, bool has_repair,
680 const lang::Distrib<T>& repair,
681 const std::string& breakdown_reset,
682 const std::string& repair_reset) {
683 const std::size_t up = find_stage("UP");
684 const std::size_t down = find_stage(down_stage_name(node_name));
685 if (up == stages_.size())
686 throw InputError("Environment: cannot register a node failure on '" + node_name +
687 "': no UP stage is defined in this environment");
688 if (down == stages_.size())
689 throw InputError("Environment: cannot register a node failure on '" + node_name +
690 "': no '" + down_stage_name(node_name) +
691 "' stage is defined in this environment");
692 set_reset(up, down, env_reset_policy(breakdown_reset));
693
695 nf.node = node_name;
696 nf.breakdown = breakdown;
697 nf.down_service = down_service;
698 nf.breakdown_reset = breakdown_reset;
699 if (has_repair) {
700 set_reset(down, up, env_reset_policy(repair_reset));
701 nf.repair = repair;
702 nf.has_repair = true;
703 nf.repair_reset = repair_reset;
704 }
705 record_node_failure(nf);
706 }
707
708 /**
709 * Port of `Environment.init()`.
710 *
711 * Superpose the outgoing transitions of each stage, read the embedded jump
712 * chain off the per-destination rates, and solve the resulting semi-Markov
713 * process for its stationary stage probabilities. A semi-Markov stage takes
714 * its jump row and mean holding time from `mc::smp_stage_quad` instead, and
715 * keeps its arcs so `hold_time_cdf` can evaluate the law.
716 *
717 * An ABSORBING stage (`is_absorbing_stage`, no outgoing arc) gets an
718 * infinite mean holding time, a zero embedded row and rate 0. The
719 * environment then has no single steady state, so probEnv and probOrig are
720 * left EMPTY; method 'occupation' reads the embedded chain instead.
721 */
722 void init() {
723 const std::size_t E = stages_.size();
725
726 // proc[e][h], the transition process, as an MMAP marked by destination.
727 // A semi-Markov stage has no MAP form, so its arcs are the disabled pair.
728 sm_stage_.assign(E, 0);
729 sm_arcs_.assign(E, std::vector<lang::Distrib<T>>());
730 sm_probs_.assign(E, std::vector<double>());
731 for (std::size_t e = 0; e < E; ++e) sm_stage_[e] = is_semi_markov_stage(e) ? 1 : 0;
732 proc.assign(E, std::vector<mam::Mmap<double>>(E));
733 for (std::size_t e = 0; e < E; ++e)
734 for (std::size_t h = 0; h < E; ++h)
735 proc[e][h] = sm_stage_[e] ? disabled_mmap(E) : arc_mmap(e, h, E);
736
737 hold_time.assign(E, mam::Mmap<double>());
738 hold_mean.assign(E, 0.0);
739 Matrix<double> Pemb(E, E, 0.0);
740 std::vector<double> lambda(E, 0.0);
741 for (std::size_t e = 0; e < E; ++e) {
742 if (is_absorbing_stage(e)) {
743 // No exit: the stage is never left, so it has no jump law and an
744 // infinite mean holding time (lambda[e] = 0, its Pemb row zero).
745 hold_time[e] = disabled_mmap(E);
746 hold_mean[e] = std::numeric_limits<double>::infinity();
747 continue;
748 }
749 if (sm_stage_[e]) {
750 // The embedded jump probabilities and the mean holding time come
751 // from quadrature on the arc CDFs.
752 stage_arcs(e, sm_arcs_[e], sm_probs_[e]);
753 const mc::SmpStageQuad q = mc::smp_stage_quad(sm_arcs_[e], sm_probs_[e]);
754 for (std::size_t h = 0; h < E; ++h) Pemb(e, h) = q.P[h];
755 hold_mean[e] = q.m;
756 hold_time[e] = disabled_mmap(E);
757 lambda[e] = (q.m > 0.0) ? 1.0 / q.m : 0.0;
758 continue;
759 }
760 // The reference seeds the superposition with the SELF transition
761 // and folds in every other destination.
762 mam::Mmap<double> ht = proc[e][e];
763 for (std::size_t h = 0; h < E; ++h) {
764 if (h == e) continue;
765 ht = superpose_collapsed(ht, proc[e][h]);
766 }
767 hold_time[e] = ht;
768 const std::vector<double> cl = mam::mmap_count_lambda(ht);
769 double tot = 0.0;
770 for (double v : cl) tot += v;
771 if (tot > 0.0)
772 for (std::size_t h = 0; h < E; ++h) Pemb(e, h) = cl[h] / tot;
773 const double m = mam::map_mean(ht.map());
774 hold_mean[e] = m;
775 lambda[e] = (m > 0.0) ? 1.0 / m : 0.0;
776 }
777
778 pemb = Pemb;
779 rate = lambda;
780 bool all_positive = true;
781 for (double v : lambda)
782 if (!(v > 0.0)) all_positive = false;
783 if (!all_positive) {
784 // An absorbing environment has no single steady state: its limit
785 // depends on the initial stage, so it is left to the solver
786 // (SolverENV method 'occupation').
787 prob_env.clear();
789 return;
790 }
791
792 // A = -lambda_e (I - Pemb): the generator of the semi-Markov process
793 // observed at its jumps, whose stationary law is the stage probability.
794 Matrix<double> A(E, E, 0.0);
795 for (std::size_t e = 0; e < E; ++e)
796 for (std::size_t h = 0; h < E; ++h)
797 A(e, h) = -lambda[e] * ((e == h ? 1.0 : 0.0) - Pemb(e, h));
799
800 prob_orig = Matrix<double>(E, E, 0.0);
801 for (std::size_t e = 0; e < E; ++e) {
802 for (std::size_t h = 0; h < E; ++h)
803 prob_orig(h, e) = prob_env[h] * lambda[h] * Pemb(h, e);
804 if (prob_env[e] > 0.0) {
805 double s = 0.0;
806 for (std::size_t h = 0; h < E; ++h) s += prob_orig(h, e);
807 if (s > 0.0)
808 for (std::size_t h = 0; h < E; ++h) prob_orig(h, e) /= s;
809 }
810 }
811 }
812
813 /**
814 * `getReliabilityTable`: MTTF, MTTR, MTBF and availability of an
815 * environment built out of node breakdowns.
816 *
817 * It reads the UP and DOWN_* stages BY NAME, as the reference does, so it
818 * applies to a breakdown environment and to nothing else; a stage set that
819 * carries no such names is refused rather than answered about.
820 *
821 * The four are not independent readings of the same thing. MTTF is the
822 * COMPETING-RISK mean of the arcs out of UP, i.e. one over the SUM of the
823 * breakdown rates, so a second failing node shortens it. MTTR is the mean
824 * repair time weighted by the CONDITIONAL probability of being in each DOWN
825 * stage given that the system is down. Availability is read off probEnv and
826 * not from MTTF/(MTTF+MTTR): the two agree for a Markovian environment and
827 * the stationary law is the one that stays right when the arcs are not.
828 */
829 struct Reliability {
830 double mttf = 0.0; ///< mean time to failure, UP -> any DOWN
831 double mttr = 0.0; ///< mean time to repair, DOWN -> UP
832 double mtbf = 0.0; ///< MTTF + MTTR
833 double availability = 0.0; ///< stationary probability of being UP
834 };
835
837 if (prob_env.empty())
838 throw InputError(
839 "Environment: reliability metrics are read from probEnv; call init() first");
840 const std::size_t E = stages_.size();
841 const std::size_t up = find_stage("UP");
842 if (up == E)
843 throw InputError(
844 "Environment: no UP stage, so there is nothing for a breakdown to leave; "
845 "reliability metrics apply to an environment built with add_node_breakdown");
846 std::vector<std::size_t> down;
847 for (std::size_t e = 0; e < E; ++e)
848 if (stages_[e].name.compare(0, 5, "DOWN_") == 0) down.push_back(e);
849 if (down.empty())
850 throw InputError(
851 "Environment: no DOWN_<node> stage, so no node breaks down; reliability metrics "
852 "apply to an environment built with add_node_breakdown");
853
854 double lambda_total = 0.0;
855 for (std::size_t h : down)
856 if (arcs_[up][h].enabled) lambda_total += arc_rate(up, h);
857 if (!(lambda_total > 0.0))
858 throw InputError(
859 "Environment: no UP -> DOWN transition, so the system never fails and its mean "
860 "time to failure is not a number");
861
862 std::vector<double> mu, p;
863 for (std::size_t e : down)
864 if (arcs_[e][up].enabled) {
865 mu.push_back(is_semi_markov_stage(e) ? 1.0 / hold_mean[e] : arc_rate(e, up));
866 p.push_back(prob_env[e]);
867 }
868 if (mu.empty())
869 throw InputError(
870 "Environment: no DOWN -> UP transition, so the system is never repaired and its "
871 "mean time to repair is not a number");
872
873 Reliability r;
874 r.mttf = 1.0 / lambda_total;
875 // A semi-Markov UP stage has no rates to add: the time to failure is its holding time.
876 if (is_semi_markov_stage(up)) r.mttf = hold_mean[up];
877 double ptot = 0.0;
878 for (double v : p) ptot += v;
879 r.mttr = 0.0;
880 for (std::size_t i = 0; i < mu.size(); ++i)
881 r.mttr += (ptot > 0.0 ? p[i] / ptot : 1.0 / static_cast<double>(mu.size())) / mu[i];
882 r.mtbf = r.mttf + r.mttr;
883 double pup = prob_env[up], pdown = 0.0;
884 for (std::size_t e : down) pdown += prob_env[e];
885 r.availability = (pup + pdown > 0.0) ? pup / (pup + pdown) : 0.0;
886 return r;
887 }
888
889 /**
890 * `getStageTable`: one row per stage. The reference's HoldT column holds the
891 * MAP or the semi-Markov arc struct; here it is the mean holding time and
892 * whether the stage is semi-Markov, which is what a reader of the table uses.
893 */
894 struct StageRow {
895 std::size_t stage = 0; ///< 1-based, as the reference prints it
896 std::string name;
897 std::string type;
898 double prob = 0.0; ///< probEnv
899 double hold_mean = 0.0; ///< holdMean
900 bool semi_markov = false;
901 };
902
903 std::vector<StageRow> stage_table() const {
904 if (prob_env.empty())
905 throw InputError("Environment: the stage table reads probEnv; call init() first");
906 std::vector<StageRow> rows(stages_.size());
907 for (std::size_t e = 0; e < stages_.size(); ++e) {
908 rows[e].stage = e + 1;
909 rows[e].name = stages_[e].name;
910 rows[e].type = stages_[e].type;
911 rows[e].prob = prob_env[e];
912 rows[e].hold_mean = hold_mean[e];
913 rows[e].semi_markov = hold_time_is_semi_markov(e);
914 }
915 return rows;
916 }
917
918 /**
919 * `printStageTable`, as text: the stages with their networks, then every
920 * enabled arc in (from, to) order, a competing one by its rate and a kernel
921 * one by its probability, sojourn family and mean. A layered stage reports
922 * its task and entry counts, having no node or class count of its own.
923 */
924 std::string stage_table_text() const {
925 std::string s = "Stage Table:\n============\n";
926 char buf[512];
927 for (std::size_t e = 0; e < stages_.size(); ++e) {
928 const EnvStage<T>& st = stages_[e];
929 std::snprintf(buf, sizeof buf, "Stage %zu: %s (Type: %s)\n", e + 1, st.name.c_str(), st.type.c_str());
930 s += buf;
931 if (st.has_model) {
932 std::snprintf(buf, sizeof buf, " - Network: %s\n - Nodes: %zu\n - Classes: %zu\n",
933 st.model.name.c_str(), st.model.nodes.size(), st.model.nclasses);
934 s += buf;
935 } else if (st.has_lqn) {
936 std::snprintf(buf, sizeof buf, " - Layered network: %zu tasks, %zu entries\n",
937 st.lqn_model.ntasks, st.lqn_model.nentries);
938 s += buf;
939 }
940 }
941 std::string tr;
942 for (std::size_t e = 0; e < stages_.size(); ++e)
943 for (std::size_t h = 0; h < stages_.size(); ++h) {
944 const EnvArc<T>& a = arcs_[e][h];
945 if (!a.enabled || a.dist.disabled) continue;
946 const double m = num_traits<T>::to_double(a.dist.mean);
947 if (is_kernel_stage(e))
948 std::snprintf(buf, sizeof buf, " %s -> %s: prob = %.4f, sojourn %s, mean = %.4f\n",
949 stages_[e].name.c_str(), stages_[h].name.c_str(), kernel_prob_(e, h),
950 lang::process_to_text(a.dist.type), m);
951 else
952 std::snprintf(buf, sizeof buf, " %s -> %s: rate = %.4f\n", stages_[e].name.c_str(),
953 stages_[h].name.c_str(), 1.0 / m);
954 tr += buf;
955 }
956 if (!tr.empty()) s += "\nTransitions:\n" + tr;
957 return s;
958 }
959
960 /** `printStageTable`: writes `stage_table_text()` to `out`. */
961 void print_stage_table(std::FILE* out = stdout) const {
962 std::fputs(stage_table_text().c_str(), out);
963 }
964
965 /**
966 * `Environment.holdTimeCdf(holdTime{e}, t)`: the holding-time CDF of stage e
967 * at the instants `t`, from the MAP for a Markovian stage and from the arc
968 * race or kernel mixture (`mc::smp_stage_cif`) for a semi-Markov one.
969 */
970 std::vector<double> hold_time_cdf(std::size_t e, const std::vector<double>& t) const {
971 check(e);
972 if (e >= sm_stage_.size())
973 throw InputError("Environment: the holding-time law is built by init(); call it first");
974 if (sm_stage_[e]) return mc::smp_stage_cif(sm_arcs_[e], sm_probs_[e], t).Fhold;
975 return mam::map_cdf(hold_time[e].map(), t);
976 }
977
978 /** `Environment.holdTimeMean(holdTime{e})`: the mean holding time of stage e. */
979 double hold_time_mean(std::size_t e) const {
980 check(e);
981 if (e >= hold_mean.size())
982 throw InputError("Environment: the holding-time law is built by init(); call it first");
983 return hold_mean[e];
984 }
985
986 /** True when init() built stage e as a semi-Markov stage (no MAP holding time). */
987 bool hold_time_is_semi_markov(std::size_t e) const {
988 check(e);
989 return e < sm_stage_.size() && sm_stage_[e];
990 }
991
992 // ---- what init() produced --------------------------------------------
993 std::vector<std::vector<mam::Mmap<double>>> proc; ///< proc[e][h]; disabled for a semi-Markov stage
994 std::vector<mam::Mmap<double>> hold_time; ///< holdTime[e]; disabled for a semi-Markov stage
995 std::vector<double> hold_mean; ///< holdMean
996 std::vector<double> prob_env; ///< probEnv
997 Matrix<double> prob_orig; ///< probOrig(h, e)
998 Matrix<double> pemb; ///< probEmb, the embedded jump chain
999 std::vector<double> rate; ///< 1/E[holding time]
1000
1001private:
1002 /** `isa(arc, 'Markovian')`: a family with a (D0,D1) form that carries one. */
1003 static bool arc_is_markovian(const lang::Distrib<T>& d) {
1004 return lang::process_is_markovian(d.type) && d.has_map();
1005 }
1006
1007 /** The 1 x 1 zero pair a disabled arc is. */
1008 static mam::Mmap<double> disabled_mmap(std::size_t E) {
1010 m.D0 = Matrix<double>(1, 1, 0.0);
1011 m.D1 = Matrix<double>(1, 1, 0.0);
1012 m.Dc.assign(E, Matrix<double>(1, 1, 0.0));
1013 return m;
1014 }
1015
1016 void check(std::size_t e) const {
1017 if (e >= stages_.size()) throw InputError("Environment: stage index out of range");
1018 }
1019
1020 /** `1 / env{e,h}.getMean()`: the arc rate the reliability reading uses. */
1021 double arc_rate(std::size_t e, std::size_t h) const {
1022 const double m = num_traits<T>::to_double(arcs_[e][h].dist.mean);
1023 if (!(m > 0.0))
1024 throw InputError("Environment: the transition " + stages_[e].name + " -> " +
1025 stages_[h].name +
1026 " has no positive mean, so it carries no rate to report");
1027 return 1.0 / m;
1028 }
1029
1030 /** Record a descriptor, replacing the one this node already had. */
1031 void record_node_failure(const NodeFailure<T>& nf) {
1032 const std::size_t idx = find_node_failure(nf.node);
1033 if (idx < node_failures_.size())
1034 node_failures_[idx] = nf;
1035 else
1036 node_failures_.push_back(nf);
1037 }
1038
1039 /**
1040 * `emmap{e}{h}`: the arc's process marked by destination -- its D1 in slot
1041 * h and zero in every other slot. A disabled arc is the 1 x 1 zero pair.
1042 */
1043 mam::Mmap<double> arc_mmap(std::size_t e, std::size_t h, std::size_t E) const {
1044 mam::Mmap<double> m;
1045 if (!arcs_[e][h].enabled) {
1046 m.D0 = Matrix<double>(1, 1, 0.0);
1047 m.D1 = Matrix<double>(1, 1, 0.0);
1048 m.Dc.assign(E, Matrix<double>(1, 1, 0.0));
1049 return m;
1050 }
1051 const lang::Distrib<T>& d = arcs_[e][h].dist;
1052 const std::size_t n = d.D0.rows();
1053 if (n == 0)
1054 throw InputError("Environment: the transition distribution has no representation");
1055 m.D0 = Matrix<double>(n, n, 0.0);
1056 m.D1 = Matrix<double>(n, n, 0.0);
1057 for (std::size_t a = 0; a < n; ++a)
1058 for (std::size_t b = 0; b < n; ++b) {
1059 m.D0(a, b) = num_traits<T>::to_double(d.D0(a, b));
1060 m.D1(a, b) = num_traits<T>::to_double(d.D1(a, b));
1061 }
1062 m.Dc.assign(E, Matrix<double>(n, n, 0.0));
1063 m.Dc[h] = m.D1;
1064 return m;
1065 }
1066
1067 /**
1068 * One fold of the reference's superposition loop: Kronecker-sum the blocks,
1069 * then move each block's row sums into its FIRST column.
1070 *
1071 * The collapse is what makes the superposed holding time RENEW at every
1072 * jump: without it the phase surviving from the losing risks would carry
1073 * into the next stage, which is not the semi-Markov model the solver then
1074 * solves.
1075 */
1076 static mam::Mmap<double> superpose_collapsed(const mam::Mmap<double>& a,
1077 const mam::Mmap<double>& b) {
1078 mam::Mmap<double> s;
1079 s.D0 = mam::krons(a.D0, b.D0);
1080 s.D1 = collapse(mam::krons(a.D1, b.D1));
1081 s.Dc.reserve(a.Dc.size());
1082 for (std::size_t c = 0; c < a.Dc.size(); ++c)
1083 s.Dc.push_back(collapse(mam::krons(a.Dc[c], b.Dc[c])));
1084 return mam::mmap_normalize(s);
1085 }
1086
1087 /** Row sums into column one, the rest zeroed. */
1088 static Matrix<double> collapse(const Matrix<double>& X) {
1089 Matrix<double> Y(X.rows(), X.cols(), 0.0);
1090 for (std::size_t i = 0; i < X.rows(); ++i) {
1091 double s = 0.0;
1092 for (std::size_t j = 0; j < X.cols(); ++j) s += X(i, j);
1093 Y(i, 0) = s;
1094 }
1095 return Y;
1096 }
1097
1098 std::string name_;
1099 std::vector<EnvStage<T>> stages_;
1100 std::vector<std::vector<EnvArc<T>>> arcs_;
1101 std::vector<NodeFailure<T>> node_failures_;
1102 Matrix<double> kernel_prob_; ///< kernelProb, NaN for a competing arc or none
1103 std::vector<char> sm_stage_; ///< per stage, whether init() built it semi-Markov
1104 std::vector<std::vector<lang::Distrib<T>>> sm_arcs_; ///< a semi-Markov stage's arcs
1105 std::vector<std::vector<double>> sm_probs_; ///< and its kernel probabilities
1106};
1107
1108} // namespace env
1109} // namespace line
1110
1111#endif // LINE_LANG_QN_ENVIRONMENT_H
InputError(const std::string &what)
Definition error.h:105
UnsupportedError(const std::string &what)
Definition error.h:119
const std::string & name() const
void set_env_rate_reset(std::size_t e, std::size_t h, const ResetEnvRates< T > &f)
resetEnvRatesFun{e,h}: make the e -> h transition depend on the state the stage is left in.
void register_node_failure(const std::string &node_name, const lang::Distrib< T > &breakdown, const lang::Distrib< T > &down_service, bool has_repair, const lang::Distrib< T > &repair, const std::string &breakdown_reset, const std::string &repair_reset)
Port of registerNodeFailure: attach a breakdown descriptor, and its reset policies,...
const std::vector< NodeFailure< T > > & node_failures() const
nodeFailures, the declarative record of the breakdowns declared here.
void init()
Port of Environment.init().
std::vector< double > rate
1/E[holding time]
void require_declared() const
Refuse a stage with no model.
Environment(const std::string &nm, std::size_t nstages)
void print_stage_table(std::FILE *out=stdout) const
printStageTable: writes stage_table_text() to out.
void add_node_breakdown(std::size_t up, std::size_t down, const qn::NetworkStruct< T > &base, const std::string &node_name, const lang::Distrib< T > &breakdown, const lang::Distrib< T > &down_service, const std::string &reset_policy="keep")
Port of addNodeBreakdown, on a FIXED stage count.
std::vector< double > hold_time_cdf(std::size_t e, const std::vector< double > &t) const
Environment.holdTimeCdf(holdTime{e}, t): the holding-time CDF of stage e at the instants t,...
bool is_semi_markov_stage(std::size_t e) const
isSemiMarkovStage: true if stage e's holding time has no Markovian (MAP) form, i.e.
bool hold_time_is_semi_markov(std::size_t e) const
True when init() built stage e as a semi-Markov stage (no MAP holding time).
void refuse_absorbing(const std::string &method) const
Refuse an absorbing environment for a coupling that reads probEnv: its limit depends on the initial s...
const EnvStage< T > & stage(std::size_t e) const
double kernel_prob(std::size_t e, std::size_t h) const
kernelProb(e,h): the kernel arc's probability, NaN for a competing arc or none.
Matrix< double > prob_orig
probOrig(h, e)
bool is_semi_markov() const
isSemiMarkov: true if any stage is semi-Markov.
std::vector< std::vector< mam::Mmap< double > > > proc
proc[e][h]; disabled for a semi-Markov stage
void add_node_failure_repair(std::size_t up, std::size_t down, const qn::NetworkStruct< T > &base, const std::string &node_name, const lang::Distrib< T > &breakdown, const lang::Distrib< T > &repair, const lang::Distrib< T > &down_service, const std::string &breakdown_reset="keep", const std::string &repair_reset="keep")
addNodeFailureRepair: the two calls above, in order.
void stage_arcs(std::size_t e, std::vector< lang::Distrib< T > > &arcs, std::vector< double > &probs) const
getStageArcs: the transitions out of stage e in the form mc::smp_stage_cif takes.
std::vector< StageRow > stage_table() const
void set_transition_dist(std::size_t e, std::size_t h, const lang::Distrib< T > &d)
Replace the distribution of an arc that is already declared.
void set_lqn_stage(std::size_t e, const std::string &nm, const std::string &type, const lqn::LqnStruct< T > &model)
addStage with a LayeredNetwork: the stage holds a LAYERED model.
bool is_absorbing_stage(std::size_t e) const
isAbsorbingStage: true if stage e has no outgoing transition, so once entered the environment stays t...
std::size_t find_stage(const std::string &nm) const
The index of the stage called nm, or nstages() when there is none.
Reliability reliability() const
std::size_t nstages() const
static std::string down_stage_name(const std::string &nm)
The name addNodeBreakdown gives the stage in which nm is down.
std::vector< mam::Mmap< double > > hold_time
holdTime[e]; disabled for a semi-Markov stage
void add_kernel_transition(std::size_t e, std::size_t h, double prob, const lang::Distrib< T > &d, const ResetMarginal &reset=ResetMarginal())
addKernelTransition: a KERNEL arc of Q(e,h,t) = prob * F(t).
bool is_kernel_stage(std::size_t e) const
isKernelStage: true if stage e's arcs are kernel arcs.
void set_reset(std::size_t e, std::size_t h, const ResetMarginal &reset)
Install a reset policy on an arc that is already declared.
std::string stage_table_text() const
printStageTable, as text: the stages with their networks, then every enabled arc in (from,...
std::size_t find_node_failure(const std::string &nm) const
findNodeFailure: the descriptor for nm, or node_failures().size().
void reject_lqn_stages(const std::string &who, const std::string &why) const
Refuse an environment carrying a LayeredNetwork stage, by name.
bool is_absorbing() const
True if some stage is absorbing, i.e.
double hold_time_mean(std::size_t e) const
Environment.holdTimeMean(holdTime{e}): the mean holding time of stage e.
void set_stage(std::size_t e, const std::string &nm, const std::string &type, const qn::NetworkStruct< T > &model)
addStage: name the stage and give it its network.
bool has_lqn_stages() const
True when ANY stage holds a LayeredNetwork.
Matrix< double > pemb
probEmb, the embedded jump chain
std::vector< double > hold_mean
holdMean
const EnvArc< T > & arc(std::size_t e, std::size_t h) const
void refuse_semi_markov(const std::string &method) const
The methodRefusal rule for every coupling but smp: refuse a kernel stage, or a competing arc that is ...
bool is_lqn(std::size_t e) const
True when stage e holds a LayeredNetwork rather than a flat network.
void add_node_repair(const std::string &node_name, const lang::Distrib< T > &repair, const std::string &reset_policy="keep")
Port of addNodeRepair: the DOWN_<node> -> UP arc and its policy.
void add_transition(std::size_t e, std::size_t h, const lang::Distrib< T > &d, const ResetMarginal &reset=ResetMarginal())
addTransition: enable e -> h with a distribution and a reset policy.
std::vector< double > prob_env
probEnv
A network plus its refreshed NetworkStruct.
Courtois decomposition of a nearly completely decomposable (NCD) CTMC.
What refreshProcessRepresentations and refreshLST compute FROM a distribution: the (D0,...
The exception types the port throws.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
LayeredNetworkStruct, the flattened description of a layered queueing network.
Cumulative distribution of the inter-arrival time of a MAP.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
Dense matrix and non-owning view.
Marked MAP (MMAP) algebra: per-class rates, class probabilities, superposition, normalization and sca...
std::function< Matrix< double >(const Matrix< double > &)> ResetMarginal
The reset policy of a transition, resetFun in the reference.
Definition environment.h:98
qn::NetworkStruct< T > env_degraded_model(const qn::NetworkStruct< T > &base, const std::string &node_name, const lang::Distrib< T > &down_service)
The DOWN stage network of addNodeBreakdown: the base network with ONE node's service replaced by its ...
std::function< lang::Distrib< T >(const lang::Distrib< T > &, const Matrix< double > &, const Matrix< double > &, const Matrix< double > &)> ResetEnvRates
resetEnvRatesFun in the reference: the state-dependent environment rate.
ResetMarginal env_reset_policy(const std::string &name)
The two NAMED reset policies of Environment.resolveResetPolicy, which are the only ones the JSON inte...
void prior_refresh_moments(Distrib< T > &d)
Write the mixture moments onto a Prior, the counterpart of dist_refresh_moments for the Markovian fam...
Definition prior.h:346
const char * process_to_text(ProcessType p)
The MATLAB ProcessType name, as sn.procid prints it.
Definition lang_types.h:601
bool process_is_markovian(ProcessType p)
ProcessType.isMarkovian: true when sn.proc carries an exact matrix representation of the law,...
Definition lang_types.h:655
void dist_refresh_moments(Distrib< T > &d)
Fill in the first two moments of a distribution given by its matrices.
Mmap< T > mmap_normalize(const Mmap< T > &in)
Clamp negative off-diagonal and per-class entries to zero and rebuild D1 and the diagonal of D0 from ...
Matrix< T > krons(const Matrix< T > &A, const Matrix< T > &B)
Kronecker sum, MATLAB's krons: kron(A, I_nb) + kron(I_na, B).
Definition mmap_lambda.h:71
T map_mean(const Map< T > &m)
Mean inter-arrival time, 1/lambda.
Definition map_moment.h:101
std::vector< T > map_cdf(const Map< T > &m, const std::vector< T > &points)
Cumulative distribution of the inter-arrival time at the given points.
Definition map_cdf.h:63
std::vector< T > mmap_count_lambda(const Mmap< T > &m)
Per-class arrival rates, lambda_c = theta D1^(c) e.
ReducibleResult< T > ctmc_solve_reducible(const Matrix< T > &Q, const std::vector< T > &pi0, double zeroColTol=1e-12)
Limiting distribution of a CTMC whose generator may be reducible.
SmpStageQuad smp_stage_quad(const std::vector< lang::Distrib< T > > &arcs, const std::vector< double > &probs)
SmpStageCif smp_stage_cif(const std::vector< lang::Distrib< T > > &arcs, const std::vector< double > &probs, const std::vector< double > &t)
CIF and holding-time CDF of a stage at the ascending instants t.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
Prior: parameter uncertainty as a weighted set of alternative models.
Cumulative incidence functions of one semi-Markov stage.
Embedded jump probabilities and mean holding time of a semi-Markov stage.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
std::string name
Definition error.h:38
One arc of the environment process.
lang::Distrib< T > dist
the e -> h transition time
ResetMarginal reset
empty means the identity
ResetEnvRates< T > reset_rates
empty means the rate does not depend on the state
One stage: a name, a category, and the model in force while it lasts.
std::string type
the stage category, informational only
lqn::LqnStruct< T > lqn_model
The layered model, when this stage holds a LayeredNetwork.
bool declared() const
True when the stage carries a model of either kind.
qn::NetworkStruct< T > model
getReliabilityTable: MTTF, MTTR, MTBF and availability of an environment built out of node breakdowns...
double mttr
mean time to repair, DOWN -> UP
double availability
stationary probability of being UP
double mttf
mean time to failure, UP -> any DOWN
getStageTable: one row per stage.
std::size_t stage
1-based, as the reference prints it
Environment.nodeFailures{k}: the declarative record of one node breakdown.
std::string repair_reset
empty when there is no repair
std::string breakdown_reset
std::string node
the node that breaks down
lang::Distrib< T > down_service
the node's service while it is down
bool has_repair
false for a breakdown with no repair arc
lang::Distrib< T > breakdown
time to failure, the UP -> DOWN transition
lang::Distrib< T > repair
time to repair, the DOWN -> UP transition
bool has_map() const
True when the type carries a (D0,D1) pair of its own.
Matrix< T > D0
The (D0,D1) pair when the type carries one directly.
Definition lang_types.h:863
bool is_prior() const
Definition lang_types.h:888
An MMAP: the underlying MAP plus the per-class arrival matrices.
Definition mmap_lambda.h:45
Map< T > map() const
Definition mmap_lambda.h:52
Matrix< T > D0
Definition mmap_lambda.h:46
Matrix< T > D1
Definition mmap_lambda.h:47
std::vector< Matrix< T > > Dc
per-class matrices, sum_c Dc = D1
Definition mmap_lambda.h:48
std::vector< double > Fhold
What smp_stage_quad returns.
std::vector< double > P
the embedded jump probabilities, one per arc
double m
the mean holding time