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 DISABLED transition is the 1 x 1 zero pair, exactly as in the reference:
42 * krons(A, 0) leaves A unchanged, so a disabled arc costs nothing and needs no
43 * special case anywhere below.
44 *
45 * NODE BREAKDOWNS are the one stage pattern the reference gives a name to
46 * (`addNodeBreakdown` / `addNodeRepair` / `addNodeFailureRepair`): the UP stage
47 * holds the base network, the DOWN_<node> stage holds the same network with one
48 * node's service replaced by its degraded distribution, and the two arcs
49 * between them are the time to failure and the time to repair. It is a macro
50 * over `set_stage` and `add_transition` and nothing more, EXCEPT for the pair of
51 * queue-length reset policies it attaches, which are the only part of a
52 * breakdown that the expanded stages cannot express -- hence `NodeFailure`,
53 * which records them beside the stages so an environment read back from
54 * `model.json` is the same model it was written from.
55 */
56
57#include <cctype>
58#include <cstddef>
59#include <functional>
60#include <string>
61#include <vector>
62
69#include "line/lang/prior.h"
71#include "line/util/error.h"
72#include "line/util/matrix.h"
73
74namespace line {
75namespace env {
76
77/**
78 * The reset policy of a transition, `resetFun` in the reference.
79 *
80 * It maps the mean queue lengths at the moment of the switch onto the mean
81 * queue lengths the next stage starts from. Identity means the jobs are simply
82 * carried over; zero means the buffer is flushed on the switch.
83 */
84using ResetMarginal = std::function<Matrix<double>(const Matrix<double>&)>;
85
86/**
87 * The two NAMED reset policies of `Environment.resolveResetPolicy`, which are
88 * the only ones the JSON interchange can carry (a function handle is written as
89 * `custom` and warned about by both writers, never reloaded).
90 *
91 * `keep` resolves to the EMPTY function rather than to an explicit identity,
92 * because empty is how this port spells identity everywhere a reset is read:
93 * `SolverEnv::post` skips the call, and the compression's macro-arc fold
94 * compares resets only by whether one is PRESENT, so an explicit identity on
95 * one arc and nothing on another would be refused as a disagreement although
96 * the two mean the same thing.
97 */
98inline ResetMarginal env_reset_policy(const std::string& name) {
99 std::string low;
100 low.reserve(name.size());
101 for (std::size_t i = 0; i < name.size(); ++i)
102 low.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(name[i]))));
103 if (low == "keep") return ResetMarginal();
104 if (low == "clear")
105 return [](const Matrix<double>& q) { return Matrix<double>(q.rows(), q.cols(), 0.0); };
106 throw InputError("Environment: unknown reset policy '" + name +
107 "'. The serializable policies are 'keep' (carry the queue lengths across "
108 "the switch) and 'clear' (empty the queues); a custom policy is a function "
109 "and is installed through add_transition, not by name");
110}
111
112/**
113 * `Environment.nodeFailures{k}`: the declarative record of one node breakdown.
114 *
115 * The expanded stages and transitions already carry the STRUCTURE of a
116 * breakdown; what only this record carries is the pair of reset policies, which
117 * decide what the next stage starts from and therefore change the numbers. It
118 * is kept on the environment for the same reason the reference keeps it: so an
119 * environment read from `model.json` in its expanded form can be given back its
120 * policies, and so it serializes out again as the same model.
121 */
122template <class T>
124 std::string node; ///< the node that breaks down
125 lang::Distrib<T> breakdown; ///< time to failure, the UP -> DOWN transition
126 lang::Distrib<T> down_service; ///< the node's service while it is down
127 lang::Distrib<T> repair; ///< time to repair, the DOWN -> UP transition
128 bool has_repair = false; ///< false for a breakdown with no repair arc
129 std::string breakdown_reset = "keep";
130 std::string repair_reset; ///< empty when there is no repair
131};
132
133/**
134 * One stage: a name, a category, and the model in force while it lasts.
135 *
136 * THE MODEL IS ONE OF TWO KINDS, and which one is the stage's own property
137 * rather than the environment's: a flat `qn::NetworkStruct`, or a
138 * `lqn::LqnStruct` -- a LayeredNetwork, whose stations and classes are those of
139 * the LAYERS SolverLN builds out of it. The reference draws no distinction at
140 * this level either (`Environment.addStage` stores whatever model it is handed
141 * and `SolverENV` branches on its class), and the two fields are kept apart
142 * rather than unified because nothing about them is shared below the name: a
143 * flat stage is integrated directly, a layered one through a whole fixed point
144 * over its layers.
145 *
146 * `has_model` and `has_lqn` are mutually exclusive; `set_stage` and
147 * `set_lqn_stage` each clear the other, so a stage re-declared as the other
148 * kind cannot leave a stale twin behind for a later consumer to read.
149 */
150template <class T>
151struct EnvStage {
152 std::string name;
153 std::string type; ///< the stage category, informational only
155 bool has_model = false;
156 /** The layered model, when this stage holds a LayeredNetwork. */
158 bool has_lqn = false;
159 /** True when the stage carries a model of either kind. */
160 bool declared() const { return has_model || has_lqn; }
161};
162
163/**
164 * `resetEnvRatesFun` in the reference: the state-dependent environment rate.
165 *
166 * It is given the arc's CURRENT transition distribution and the exit metrics of
167 * the stage the arc leaves -- mean queue lengths, utilizations and throughputs,
168 * averaged over when that arc fires -- and returns the distribution the arc
169 * should carry next. It is what makes the environment process depend on the
170 * network it modulates, and `method = "statedep"` is what applies it.
171 */
172template <class T>
174 std::function<lang::Distrib<T>(const lang::Distrib<T>&, const Matrix<double>&,
175 const Matrix<double>&, const Matrix<double>&)>;
176
177/** One arc of the environment process. */
178template <class T>
179struct EnvArc {
180 bool enabled = false;
181 lang::Distrib<T> dist; ///< the e -> h transition time
182 ResetMarginal reset; ///< empty means the identity
183 ResetEnvRates<T> reset_rates; ///< empty means the rate does not depend on the state
184};
185
186/**
187 * The DOWN stage network of `addNodeBreakdown`: the base network with ONE
188 * node's service replaced by its degraded distribution, for EVERY class.
189 *
190 * Every class, and not only the ones that were enabled there, is what the
191 * reference does (`for c = 1:length(classes), nodes{nodeIdx}.setService(...)`),
192 * so a class that was disabled at the node while it was up is served at the
193 * degraded rate while it is down. The whole refresh chain is rerun afterwards
194 * because `rates`, `scv` and the chain-derived tables are all read off the
195 * service table; editing the table alone would leave the struct describing the
196 * UP stage and the solver reading the DOWN one.
197 */
198template <class T>
200 const std::string& node_name,
201 const lang::Distrib<T>& down_service) {
203 std::size_t node = 0;
204 for (std::size_t i = 0; i < sn.nodes.size(); ++i)
205 if (sn.nodes[i].name == node_name) {
206 node = i + 1;
207 break;
208 }
209 if (node == 0)
210 throw InputError("Environment: node '" + node_name +
211 "' is not in the base model, so it has no service to degrade");
212 const std::size_t ist = sn.nodes[node - 1].station;
213 if (ist == 0)
214 throw InputError("Environment: node '" + node_name +
215 "' is not a station and carries no service distribution, so it cannot "
216 "break down into a degraded service");
217 lang::Distrib<T> d = down_service;
218 if (d.is_prior())
220 else
222 for (std::size_t r = 1; r <= sn.classes.size(); ++r) sn.set_service(ist, r, d);
223 sn.refresh_struct();
224 return sn;
225}
226
227template <class T>
229public:
230 Environment(const std::string& nm, std::size_t nstages)
231 : name_(nm), stages_(nstages), arcs_(nstages, std::vector<EnvArc<T>>(nstages)) {
232 if (nstages == 0) throw InputError("Environment: a random environment needs a stage");
233 }
234
235 std::size_t nstages() const { return stages_.size(); }
236 const std::string& name() const { return name_; }
237
238 /** `addStage`: name the stage and give it its network. */
239 void set_stage(std::size_t e, const std::string& nm, const std::string& type,
240 const qn::NetworkStruct<T>& model) {
241 check(e);
242 stages_[e].name = nm;
243 stages_[e].type = type;
244 stages_[e].model = model;
245 stages_[e].has_model = true;
246 stages_[e].has_lqn = false;
247 stages_[e].lqn_model = lqn::LqnStruct<T>();
248 }
249
250 /**
251 * `addStage` with a LayeredNetwork: the stage holds a LAYERED model.
252 *
253 * The environment itself does nothing with the difference -- probEnv,
254 * probOrig and the holding times are read off the ARCS and never off a
255 * stage model -- so the whole content of this overload is that the stage
256 * records which kind of model it carries and `SolverEnv` runs the matching
257 * stage solver. What the two kinds must still agree on is the (station,
258 * class) SHAPE of the metrics being blended, and for a layered stage that
259 * shape is the block-diagonal union of its layers; `SolverEnv::init` is
260 * where the shapes are compared, because only there is a SolverLN built and
261 * the layer blocks known.
262 */
263 void set_lqn_stage(std::size_t e, const std::string& nm, const std::string& type,
264 const lqn::LqnStruct<T>& model) {
265 check(e);
266 stages_[e].name = nm;
267 stages_[e].type = type;
268 stages_[e].lqn_model = model;
269 stages_[e].has_lqn = true;
270 stages_[e].has_model = false;
271 stages_[e].model = qn::NetworkStruct<T>();
272 }
273
274 /** True when stage `e` holds a LayeredNetwork rather than a flat network. */
275 bool is_lqn(std::size_t e) const {
276 check(e);
277 return stages_[e].has_lqn;
278 }
279
280 /** True when ANY stage holds a LayeredNetwork. */
281 bool has_lqn_stages() const {
282 for (std::size_t e = 0; e < stages_.size(); ++e)
283 if (stages_[e].has_lqn) return true;
284 return false;
285 }
286
287 /**
288 * Refuse an environment carrying a LayeredNetwork stage, by name.
289 *
290 * Shared by every consumer that reads `stage(e).model` directly -- the
291 * state-vector coupling, the closed-form limits, the compression -- so that
292 * each says the same thing about the same gap rather than reading an EMPTY
293 * NetworkStruct and reporting a confident answer about a model that is not
294 * there. `who` names the caller and `why` says what about it needs a flat
295 * stage.
296 */
297 void reject_lqn_stages(const std::string& who, const std::string& why) const {
298 for (std::size_t e = 0; e < stages_.size(); ++e)
299 if (stages_[e].has_lqn)
300 throw UnsupportedError(
301 who + ": stage " + std::to_string(e + 1) + " ('" + stages_[e].name +
302 "') holds a LayeredNetwork, and " + why +
303 ". The mean-field coupling (method 'default' / 'meanfield') is the one that "
304 "solves a layered stage, through SolverLN");
305 }
306
307 /** `addTransition`: enable e -> h with a distribution and a reset policy. */
308 void add_transition(std::size_t e, std::size_t h, const lang::Distrib<T>& d,
309 const ResetMarginal& reset = ResetMarginal()) {
310 check(e);
311 check(h);
312 arcs_[e][h].enabled = true;
313 arcs_[e][h].dist = d;
314 arcs_[e][h].reset = reset;
315 }
316
317 const EnvStage<T>& stage(std::size_t e) const {
318 check(e);
319 return stages_[e];
320 }
321 const EnvArc<T>& arc(std::size_t e, std::size_t h) const {
322 check(e);
323 check(h);
324 return arcs_[e][h];
325 }
326
327 /**
328 * `resetEnvRatesFun{e,h}`: make the e -> h transition depend on the state
329 * the stage is left in. Only `method = "statedep"` reads it.
330 */
331 void set_env_rate_reset(std::size_t e, std::size_t h, const ResetEnvRates<T>& f) {
332 check(e);
333 check(h);
334 if (!arcs_[e][h].enabled)
335 throw InputError("Environment: no transition " + stages_[e].name + " -> " +
336 stages_[h].name +
337 " is declared, so its rate cannot be made state dependent");
338 arcs_[e][h].reset_rates = f;
339 }
340
341 /**
342 * Replace the distribution of an arc that is already declared.
343 *
344 * This is what the state-dependent method writes back each iteration; every
345 * other caller declares the arc once through `add_transition`. `init()` must
346 * be rerun afterwards, because the superposed holding times and the stage
347 * probabilities are all derived from these distributions.
348 */
349 void set_transition_dist(std::size_t e, std::size_t h, const lang::Distrib<T>& d) {
350 check(e);
351 check(h);
352 if (!arcs_[e][h].enabled)
353 throw InputError("Environment: no transition " + stages_[e].name + " -> " +
354 stages_[h].name + " is declared, so its distribution cannot be set");
355 arcs_[e][h].dist = d;
356 }
357
358 /** The index of the stage called `nm`, or `nstages()` when there is none. */
359 std::size_t find_stage(const std::string& nm) const {
360 for (std::size_t e = 0; e < stages_.size(); ++e)
361 if (stages_[e].has_model && stages_[e].name == nm) return e;
362 return stages_.size();
363 }
364
365 /**
366 * Install a reset policy on an arc that is already declared.
367 *
368 * The arc must exist: a reset on a disabled arc is a policy for a switch
369 * the environment cannot make, and it would sit there reporting nothing.
370 * The reference reaches this through `setBreakdownResetPolicy`, which
371 * likewise errors when the stages it names are absent.
372 */
373 void set_reset(std::size_t e, std::size_t h, const ResetMarginal& reset) {
374 check(e);
375 check(h);
376 if (!arcs_[e][h].enabled)
377 throw InputError("Environment: no transition " + stages_[e].name + " -> " +
378 stages_[h].name +
379 " is declared, so it cannot be given a reset policy");
380 arcs_[e][h].reset = reset;
381 }
382
383 // ---- node breakdown and repair ---------------------------------------
384
385 /** `nodeFailures`, the declarative record of the breakdowns declared here. */
386 const std::vector<NodeFailure<T>>& node_failures() const { return node_failures_; }
387
388 /** `findNodeFailure`: the descriptor for `nm`, or `node_failures().size()`. */
389 std::size_t find_node_failure(const std::string& nm) const {
390 for (std::size_t i = 0; i < node_failures_.size(); ++i)
391 if (node_failures_[i].node == nm) return i;
392 return node_failures_.size();
393 }
394
395 /** The name `addNodeBreakdown` gives the stage in which `nm` is down. */
396 static std::string down_stage_name(const std::string& nm) { return "DOWN_" + nm; }
397
398 /**
399 * Port of `addNodeBreakdown`, on a FIXED stage count.
400 *
401 * The reference grows its stage graph as breakdowns are declared; this
402 * environment is sized at construction, exactly as `set_stage` is, so the
403 * caller says which slot is UP and which is the DOWN stage of this node.
404 * Everything else is the reference's: the UP stage holds the base model and
405 * is named `UP`, the DOWN stage holds the degraded copy and is named
406 * `DOWN_<node>`, and the UP -> DOWN arc carries the breakdown time and the
407 * breakdown reset policy.
408 */
409 void add_node_breakdown(std::size_t up, std::size_t down, const qn::NetworkStruct<T>& base,
410 const std::string& node_name, const lang::Distrib<T>& breakdown,
411 const lang::Distrib<T>& down_service,
412 const std::string& reset_policy = "keep") {
413 check(up);
414 check(down);
415 if (up == down)
416 throw InputError("Environment: the UP and DOWN stages of node '" + node_name +
417 "' must be different stages");
418 if (!stages_[up].has_model) set_stage(up, "UP", "operational", base);
419 set_stage(down, down_stage_name(node_name), "failed",
420 env_degraded_model(base, node_name, down_service));
421 add_transition(up, down, breakdown, env_reset_policy(reset_policy));
422
424 nf.node = node_name;
425 nf.breakdown = breakdown;
426 nf.down_service = down_service;
427 nf.breakdown_reset = reset_policy;
428 record_node_failure(nf);
429 }
430
431 /** Port of `addNodeRepair`: the DOWN_<node> -> UP arc and its policy. */
432 void add_node_repair(const std::string& node_name, const lang::Distrib<T>& repair,
433 const std::string& reset_policy = "keep") {
434 const std::size_t down = find_stage(down_stage_name(node_name));
435 const std::size_t up = find_stage("UP");
436 if (down == stages_.size())
437 throw InputError("Environment: stage '" + down_stage_name(node_name) +
438 "' is not defined; declare the breakdown of node '" + node_name +
439 "' before its repair");
440 if (up == stages_.size())
441 throw InputError("Environment: no UP stage is defined, so node '" + node_name +
442 "' has nothing to be repaired into");
443 add_transition(down, up, repair, env_reset_policy(reset_policy));
444
445 const std::size_t idx = find_node_failure(node_name);
446 if (idx == node_failures_.size())
447 throw InputError("Environment: no breakdown is recorded for node '" + node_name +
448 "', so its repair would describe a failure that was never declared");
449 node_failures_[idx].repair = repair;
450 node_failures_[idx].has_repair = true;
451 node_failures_[idx].repair_reset = reset_policy;
452 }
453
454 /** `addNodeFailureRepair`: the two calls above, in order. */
455 void add_node_failure_repair(std::size_t up, std::size_t down,
456 const qn::NetworkStruct<T>& base, const std::string& node_name,
457 const lang::Distrib<T>& breakdown, const lang::Distrib<T>& repair,
458 const lang::Distrib<T>& down_service,
459 const std::string& breakdown_reset = "keep",
460 const std::string& repair_reset = "keep") {
461 add_node_breakdown(up, down, base, node_name, breakdown, down_service, breakdown_reset);
462 add_node_repair(node_name, repair, repair_reset);
463 }
464
465 /**
466 * Port of `registerNodeFailure`: attach a breakdown descriptor, and its
467 * reset policies, to stages that ALREADY exist.
468 *
469 * This is the read path of an environment saved in its expanded form: the
470 * `UP` and `DOWN_<node>` stages and their two arcs came off the wire, and
471 * the only thing the wire could not carry is the pair of policies, which is
472 * what this installs.
473 */
474 void register_node_failure(const std::string& node_name, const lang::Distrib<T>& breakdown,
475 const lang::Distrib<T>& down_service, bool has_repair,
476 const lang::Distrib<T>& repair,
477 const std::string& breakdown_reset,
478 const std::string& repair_reset) {
479 const std::size_t up = find_stage("UP");
480 const std::size_t down = find_stage(down_stage_name(node_name));
481 if (up == stages_.size())
482 throw InputError("Environment: cannot register a node failure on '" + node_name +
483 "': no UP stage is defined in this environment");
484 if (down == stages_.size())
485 throw InputError("Environment: cannot register a node failure on '" + node_name +
486 "': no '" + down_stage_name(node_name) +
487 "' stage is defined in this environment");
488 set_reset(up, down, env_reset_policy(breakdown_reset));
489
491 nf.node = node_name;
492 nf.breakdown = breakdown;
493 nf.down_service = down_service;
494 nf.breakdown_reset = breakdown_reset;
495 if (has_repair) {
496 set_reset(down, up, env_reset_policy(repair_reset));
497 nf.repair = repair;
498 nf.has_repair = true;
499 nf.repair_reset = repair_reset;
500 }
501 record_node_failure(nf);
502 }
503
504 /**
505 * Port of `Environment.init()`.
506 *
507 * Superpose the outgoing transitions of each stage, read the embedded jump
508 * chain off the per-destination rates, and solve the resulting semi-Markov
509 * process for its stationary stage probabilities.
510 */
511 void init() {
512 const std::size_t E = stages_.size();
513 for (std::size_t e = 0; e < E; ++e)
514 if (!stages_[e].declared())
515 throw InputError("Environment: stage " + std::to_string(e + 1) + " has no model");
516
517 // proc[e][h], the transition process, as an MMAP marked by destination.
518 proc.assign(E, std::vector<mam::Mmap<double>>(E));
519 for (std::size_t e = 0; e < E; ++e)
520 for (std::size_t h = 0; h < E; ++h) proc[e][h] = arc_mmap(e, h, E);
521
522 hold_time.assign(E, mam::Mmap<double>());
523 Matrix<double> Pemb(E, E, 0.0);
524 std::vector<double> lambda(E, 0.0);
525 for (std::size_t e = 0; e < E; ++e) {
526 // The reference seeds the superposition with the SELF transition
527 // and folds in every other destination.
528 mam::Mmap<double> ht = proc[e][e];
529 for (std::size_t h = 0; h < E; ++h) {
530 if (h == e) continue;
531 ht = superpose_collapsed(ht, proc[e][h]);
532 }
533 hold_time[e] = ht;
534 const std::vector<double> cl = mam::mmap_count_lambda(ht);
535 double tot = 0.0;
536 for (double v : cl) tot += v;
537 if (tot > 0.0)
538 for (std::size_t h = 0; h < E; ++h) Pemb(e, h) = cl[h] / tot;
539 const double m = mam::map_mean(ht.map());
540 lambda[e] = (m > 0.0) ? 1.0 / m : 0.0;
541 }
542
543 bool all_positive = true;
544 for (double v : lambda)
545 if (!(v > 0.0)) all_positive = false;
546 if (!all_positive)
547 throw UnsupportedError(
548 "Environment: a stage has no finite holding time, so the environment is "
549 "absorbing; the reference leaves that case unimplemented (Environment.init has "
550 "no branch for it)");
551
552 // A = -lambda_e (I - Pemb): the generator of the semi-Markov process
553 // observed at its jumps, whose stationary law is the stage probability.
554 Matrix<double> A(E, E, 0.0);
555 for (std::size_t e = 0; e < E; ++e)
556 for (std::size_t h = 0; h < E; ++h)
557 A(e, h) = -lambda[e] * ((e == h ? 1.0 : 0.0) - Pemb(e, h));
559
560 prob_orig = Matrix<double>(E, E, 0.0);
561 for (std::size_t e = 0; e < E; ++e) {
562 for (std::size_t h = 0; h < E; ++h)
563 prob_orig(h, e) = prob_env[h] * lambda[h] * Pemb(h, e);
564 if (prob_env[e] > 0.0) {
565 double s = 0.0;
566 for (std::size_t h = 0; h < E; ++h) s += prob_orig(h, e);
567 if (s > 0.0)
568 for (std::size_t h = 0; h < E; ++h) prob_orig(h, e) /= s;
569 }
570 }
571 pemb = Pemb;
572 rate = lambda;
573 }
574
575 /**
576 * `getReliabilityTable`: MTTF, MTTR, MTBF and availability of an
577 * environment built out of node breakdowns.
578 *
579 * It reads the UP and DOWN_* stages BY NAME, as the reference does, so it
580 * applies to a breakdown environment and to nothing else; a stage set that
581 * carries no such names is refused rather than answered about.
582 *
583 * The four are not independent readings of the same thing. MTTF is the
584 * COMPETING-RISK mean of the arcs out of UP, i.e. one over the SUM of the
585 * breakdown rates, so a second failing node shortens it. MTTR is the mean
586 * repair time weighted by the CONDITIONAL probability of being in each DOWN
587 * stage given that the system is down. Availability is read off probEnv and
588 * not from MTTF/(MTTF+MTTR): the two agree for a Markovian environment and
589 * the stationary law is the one that stays right when the arcs are not.
590 */
591 struct Reliability {
592 double mttf = 0.0; ///< mean time to failure, UP -> any DOWN
593 double mttr = 0.0; ///< mean time to repair, DOWN -> UP
594 double mtbf = 0.0; ///< MTTF + MTTR
595 double availability = 0.0; ///< stationary probability of being UP
596 };
597
599 if (prob_env.empty())
600 throw InputError(
601 "Environment: reliability metrics are read from probEnv; call init() first");
602 const std::size_t E = stages_.size();
603 const std::size_t up = find_stage("UP");
604 if (up == E)
605 throw InputError(
606 "Environment: no UP stage, so there is nothing for a breakdown to leave; "
607 "reliability metrics apply to an environment built with add_node_breakdown");
608 std::vector<std::size_t> down;
609 for (std::size_t e = 0; e < E; ++e)
610 if (stages_[e].name.compare(0, 5, "DOWN_") == 0) down.push_back(e);
611 if (down.empty())
612 throw InputError(
613 "Environment: no DOWN_<node> stage, so no node breaks down; reliability metrics "
614 "apply to an environment built with add_node_breakdown");
615
616 double lambda_total = 0.0;
617 for (std::size_t h : down)
618 if (arcs_[up][h].enabled) lambda_total += arc_rate(up, h);
619 if (!(lambda_total > 0.0))
620 throw InputError(
621 "Environment: no UP -> DOWN transition, so the system never fails and its mean "
622 "time to failure is not a number");
623
624 std::vector<double> mu, p;
625 for (std::size_t e : down)
626 if (arcs_[e][up].enabled) {
627 mu.push_back(arc_rate(e, up));
628 p.push_back(prob_env[e]);
629 }
630 if (mu.empty())
631 throw InputError(
632 "Environment: no DOWN -> UP transition, so the system is never repaired and its "
633 "mean time to repair is not a number");
634
635 Reliability r;
636 r.mttf = 1.0 / lambda_total;
637 double ptot = 0.0;
638 for (double v : p) ptot += v;
639 r.mttr = 0.0;
640 for (std::size_t i = 0; i < mu.size(); ++i)
641 r.mttr += (ptot > 0.0 ? p[i] / ptot : 1.0 / static_cast<double>(mu.size())) / mu[i];
642 r.mtbf = r.mttf + r.mttr;
643 double pup = prob_env[up], pdown = 0.0;
644 for (std::size_t e : down) pdown += prob_env[e];
645 r.availability = (pup + pdown > 0.0) ? pup / (pup + pdown) : 0.0;
646 return r;
647 }
648
649 // ---- what init() produced --------------------------------------------
650 std::vector<std::vector<mam::Mmap<double>>> proc; ///< proc[e][h]
651 std::vector<mam::Mmap<double>> hold_time; ///< holdTime[e]
652 std::vector<double> prob_env; ///< probEnv
653 Matrix<double> prob_orig; ///< probOrig(h, e)
654 Matrix<double> pemb; ///< the embedded jump chain
655 std::vector<double> rate; ///< 1/E[holding time]
656
657private:
658 void check(std::size_t e) const {
659 if (e >= stages_.size()) throw InputError("Environment: stage index out of range");
660 }
661
662 /** `1 / env{e,h}.getMean()`: the arc rate the reliability reading uses. */
663 double arc_rate(std::size_t e, std::size_t h) const {
664 const double m = num_traits<T>::to_double(arcs_[e][h].dist.mean);
665 if (!(m > 0.0))
666 throw InputError("Environment: the transition " + stages_[e].name + " -> " +
667 stages_[h].name +
668 " has no positive mean, so it carries no rate to report");
669 return 1.0 / m;
670 }
671
672 /** Record a descriptor, replacing the one this node already had. */
673 void record_node_failure(const NodeFailure<T>& nf) {
674 const std::size_t idx = find_node_failure(nf.node);
675 if (idx < node_failures_.size())
676 node_failures_[idx] = nf;
677 else
678 node_failures_.push_back(nf);
679 }
680
681 /**
682 * `emmap{e}{h}`: the arc's process marked by destination -- its D1 in slot
683 * h and zero in every other slot. A disabled arc is the 1 x 1 zero pair.
684 */
685 mam::Mmap<double> arc_mmap(std::size_t e, std::size_t h, std::size_t E) const {
686 mam::Mmap<double> m;
687 if (!arcs_[e][h].enabled) {
688 m.D0 = Matrix<double>(1, 1, 0.0);
689 m.D1 = Matrix<double>(1, 1, 0.0);
690 m.Dc.assign(E, Matrix<double>(1, 1, 0.0));
691 return m;
692 }
693 const lang::Distrib<T>& d = arcs_[e][h].dist;
694 const std::size_t n = d.D0.rows();
695 if (n == 0)
696 throw InputError("Environment: the transition distribution has no representation");
697 m.D0 = Matrix<double>(n, n, 0.0);
698 m.D1 = Matrix<double>(n, n, 0.0);
699 for (std::size_t a = 0; a < n; ++a)
700 for (std::size_t b = 0; b < n; ++b) {
701 m.D0(a, b) = num_traits<T>::to_double(d.D0(a, b));
702 m.D1(a, b) = num_traits<T>::to_double(d.D1(a, b));
703 }
704 m.Dc.assign(E, Matrix<double>(n, n, 0.0));
705 m.Dc[h] = m.D1;
706 return m;
707 }
708
709 /**
710 * One fold of the reference's superposition loop: Kronecker-sum the blocks,
711 * then move each block's row sums into its FIRST column.
712 *
713 * The collapse is what makes the superposed holding time RENEW at every
714 * jump: without it the phase surviving from the losing risks would carry
715 * into the next stage, which is not the semi-Markov model the solver then
716 * solves.
717 */
718 static mam::Mmap<double> superpose_collapsed(const mam::Mmap<double>& a,
719 const mam::Mmap<double>& b) {
720 mam::Mmap<double> s;
721 s.D0 = mam::krons(a.D0, b.D0);
722 s.D1 = collapse(mam::krons(a.D1, b.D1));
723 s.Dc.reserve(a.Dc.size());
724 for (std::size_t c = 0; c < a.Dc.size(); ++c)
725 s.Dc.push_back(collapse(mam::krons(a.Dc[c], b.Dc[c])));
726 return mam::mmap_normalize(s);
727 }
728
729 /** Row sums into column one, the rest zeroed. */
730 static Matrix<double> collapse(const Matrix<double>& X) {
731 Matrix<double> Y(X.rows(), X.cols(), 0.0);
732 for (std::size_t i = 0; i < X.rows(); ++i) {
733 double s = 0.0;
734 for (std::size_t j = 0; j < X.cols(); ++j) s += X(i, j);
735 Y(i, 0) = s;
736 }
737 return Y;
738 }
739
740 std::string name_;
741 std::vector<EnvStage<T>> stages_;
742 std::vector<std::vector<EnvArc<T>>> arcs_;
743 std::vector<NodeFailure<T>> node_failures_;
744};
745
746} // namespace env
747} // namespace line
748
749#endif // LINE_LANG_QN_ENVIRONMENT_H
InputError(const std::string &what)
Definition error.h:39
UnsupportedError(const std::string &what)
Definition error.h:51
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]
Environment(const std::string &nm, std::size_t nstages)
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.
const EnvStage< T > & stage(std::size_t e) const
Matrix< double > prob_orig
probOrig(h, e)
std::vector< std::vector< mam::Mmap< double > > > proc
proc[e][h]
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 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.
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]
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::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.
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
the embedded jump chain
const EnvArc< T > & arc(std::size_t e, std::size_t h) const
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.
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:84
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...
Definition environment.h:98
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
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 > 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.
A queueing network and its refreshed NetworkStruct.
Prior: parameter uncertainty as a weighted set of alternative models.
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
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
Matrix< T > D0
The (D0,D1) pair when the type carries one directly.
Definition lang_types.h:759
bool is_prior() const
Definition lang_types.h:776
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