LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
environment_reader.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_IO_ENVIRONMENT_READER_H
6#define LINE_IO_ENVIRONMENT_READER_H
7
8/**
9 * @file
10 * @ingroup line_io
11 * Reader for the LINE `model.json` interchange of an ENVIRONMENT model into an
12 * `env::Environment<T>`.
13 *
14 * The wire format is the one `environment2json` in `linemodel_save.m` and
15 * `_environment_to_json` in `python/line_solver/io/linemodel_io.py` emit:
16 * `{type: "Environment", name, numStages, stages: [{name, type, model}],
17 * transitions: [{from, to, distribution}]}`, with `from`/`to` ZERO-BASED on
18 * the wire in both writers (MATLAB converts from its 1-based stage index when
19 * writing and back when reading, so a C++ reader that treated them as 1-based
20 * would silently rotate the environment process).
21 *
22 * Each stage's `model` is an ordinary Network envelope, so it is handed to
23 * `build_network_from_json` unchanged: a stage network that MVA or the fluid
24 * solver can read on its own is exactly the stage network ENV runs, and no
25 * second, weaker parser exists for it.
26 *
27 * `nodeFailures` HAS TWO ROLES, and which one applies is decided exactly as
28 * both readers decide it: by whether a `DOWN_<node>` stage is already declared.
29 *
30 * MACRO FORM (no DOWN stage declared): the block IS the environment. The one
31 * declared stage holds the base model, and each entry expands into a
32 * `DOWN_<node>` stage carrying the degraded service plus the breakdown and
33 * repair arcs. `transitions` must be absent, because the block implies them.
34 *
35 * EXPANDED FORM (the writers' own output): the stages and arcs came off the
36 * wire and the block adds only what the wire cannot express, the QUEUE-LENGTH
37 * RESET POLICIES `breakdownResetPolicy` and `repairResetPolicy`. They decide
38 * what the next stage starts from at a switch, so dropping them would run the
39 * identity reset and report a confident answer for a different model -- which
40 * is why this reader refused the whole block before the policies were ported.
41 *
42 * A `custom` policy is refused by `env_reset_policy`, and correctly: both
43 * writers warn and OMIT the key when the policy is a function handle, so the
44 * only way `custom` reaches here is a hand-written file claiming a policy that
45 * no file can carry.
46 */
47
48#include <fstream>
49#include <string>
50#include <vector>
51
52#include "json.hpp"
55#include "line/num/number.h"
56#include "line/util/error.h"
57
58namespace line {
59namespace io {
60
61namespace detail {
62
63/** The keys an Environment envelope may carry; anything else is refused. */
64inline void reject_unconsumed_env_keys(const json& model) {
65 static const char* kEnvKeys[] = {"name", "type", "numStages", "stages",
66 "transitions", "format", "version", "nodeFailures"};
67 static const char* kStageKeys[] = {"name", "type", "model"};
68 static const char* kArcKeys[] = {"from", "to", "distribution"};
69 static const char* kFailKeys[] = {"node", "breakdownRate", "repairRate",
70 "downService", "breakdownResetPolicy", "repairResetPolicy"};
71 auto known = [](const char* const* tab, std::size_t n, const std::string& k) {
72 for (std::size_t i = 0; i < n; ++i)
73 if (k == tab[i]) return true;
74 return false;
75 };
76 const std::string why =
77 "', which this reader does not implement. Refusing rather than dropping it: a "
78 "constraint silently discarded here would make the solver return a confident answer "
79 "for a different environment";
80 for (json::const_iterator it = model.begin(); it != model.end(); ++it)
81 if (!known(kEnvKeys, sizeof(kEnvKeys) / sizeof(*kEnvKeys), it.key()))
82 throw UnsupportedError("environment_reader: the model carries '" + it.key() + why);
83 if (model.contains("nodeFailures"))
84 for (const json& nf : model.at("nodeFailures"))
85 for (json::const_iterator it = nf.begin(); it != nf.end(); ++it)
86 if (!known(kFailKeys, sizeof(kFailKeys) / sizeof(*kFailKeys), it.key()))
87 throw UnsupportedError("environment_reader: the node failure on '" +
88 nf.value("node", std::string("?")) + "' carries '" +
89 it.key() + why);
90 if (model.contains("stages"))
91 for (const json& st : model.at("stages"))
92 for (json::const_iterator it = st.begin(); it != st.end(); ++it)
93 if (!known(kStageKeys, sizeof(kStageKeys) / sizeof(*kStageKeys), it.key()))
94 throw UnsupportedError("environment_reader: stage '" +
95 st.value("name", std::string("?")) + "' carries '" +
96 it.key() + why);
97 if (model.contains("transitions"))
98 for (const json& tr : model.at("transitions"))
99 for (json::const_iterator it = tr.begin(); it != tr.end(); ++it)
100 if (!known(kArcKeys, sizeof(kArcKeys) / sizeof(*kArcKeys), it.key()))
101 throw UnsupportedError("environment_reader: an environment transition carries '" +
102 it.key() + why);
103}
104
105/** One decoded `nodeFailures` entry: `nodefailure_fields` in both readers. */
106template <class T>
107struct NodeFailureSpec {
108 std::string node;
109 lang::Distrib<T> breakdown, down_service, repair;
110 bool has_repair = false;
111 std::string breakdown_reset = "keep";
112 std::string repair_reset = "keep";
113};
114
115/**
116 * Decode one entry.
117 *
118 * `breakdownRate` and `repairRate` carry FULL DISTRIBUTIONS and not scalar
119 * rates, despite the names; both writers note it and both readers do the same.
120 */
121template <class T>
122NodeFailureSpec<T> node_failure_fields(const json& nf) {
123 NodeFailureSpec<T> s;
124 if (!nf.contains("node"))
125 throw InputError(
126 "environment_reader: a 'nodeFailures' entry is missing the required 'node' field, "
127 "which names the node that breaks down");
128 s.node = nf.at("node").get<std::string>();
129 if (!nf.contains("breakdownRate") || nf.at("breakdownRate").is_null())
130 throw InputError("environment_reader: the node failure on '" + s.node +
131 "' is missing the required 'breakdownRate' field, the time to failure");
132 if (!nf.contains("downService") || nf.at("downService").is_null())
133 throw InputError("environment_reader: the node failure on '" + s.node +
134 "' is missing the required 'downService' field, the service the node "
135 "gives while it is down");
136 s.breakdown = dist_from_json<T>(nf.at("breakdownRate"));
137 s.down_service = dist_from_json<T>(nf.at("downService"));
138 if (nf.contains("repairRate") && !nf.at("repairRate").is_null()) {
139 s.repair = dist_from_json<T>(nf.at("repairRate"));
140 s.has_repair = true;
141 }
142 if (nf.contains("breakdownResetPolicy") && !nf.at("breakdownResetPolicy").is_null()) {
143 const std::string p = nf.at("breakdownResetPolicy").get<std::string>();
144 if (!p.empty()) s.breakdown_reset = p;
145 }
146 if (nf.contains("repairResetPolicy") && !nf.at("repairResetPolicy").is_null()) {
147 const std::string p = nf.at("repairResetPolicy").get<std::string>();
148 if (!p.empty()) s.repair_reset = p;
149 }
150 return s;
151}
152
153/**
154 * The MACRO form: one base stage plus a `nodeFailures` block that expands into
155 * the UP and DOWN_<node> stages and the arcs between them.
156 *
157 * THE DECLARED STAGE NAME IS DROPPED, and deliberately: the reference's
158 * `addNodeBreakdown` names the stage it creates `UP` whatever the base model was
159 * called, and the repair arcs are then found by that name. Keeping the declared
160 * name would leave `addNodeRepair`'s lookup with nothing to find, on both sides.
161 */
162template <class T>
163env::Environment<T> expand_node_failures(const json& model, const json& stage0,
164 const std::vector<NodeFailureSpec<T> >& fails) {
165 if (model.at("stages").size() != 1)
166 throw InputError(
167 "environment_reader: 'nodeFailures' expands the base model into the UP and "
168 "DOWN_<node> stages, so 'stages' must declare exactly one stage, holding the base "
169 "(UP) model");
170 if (model.contains("transitions") && !model.at("transitions").empty())
171 throw InputError(
172 "environment_reader: 'nodeFailures' implies the breakdown and repair transitions; "
173 "'transitions' must not be declared alongside it");
174 if (!stage0.contains("model") || stage0.at("model").is_null())
175 throw InputError(
176 "environment_reader: 'nodeFailures' requires the base stage to carry a 'model'");
177
178 qn::Network<T> base_net = build_network_from_json<T>(stage0.at("model"));
179 const qn::NetworkStruct<T> base = base_net.get_struct();
180 const std::size_t E = 1 + fails.size();
181 // Either count is a consistent statement of the same file: `numStages` may
182 // count the stages as DECLARED (one) or as EXPANDED, and both writers emit
183 // the expanded form, where the question does not arise. Anything else means
184 // the file counts stages this expansion would not produce.
185 if (model.contains("numStages")) {
186 const std::size_t declared = model.at("numStages").get<std::size_t>();
187 if (declared != E && declared != 1)
188 throw InputError("environment_reader: 'numStages' is " + std::to_string(declared) +
189 " but the 'nodeFailures' block expands one base stage into " +
190 std::to_string(E) + " stages");
191 }
192
193 env::Environment<T> e(model.value("name", std::string("env")), E);
194 for (std::size_t k = 0; k < fails.size(); ++k) {
195 const NodeFailureSpec<T>& nf = fails[k];
196 e.add_node_breakdown(0, k + 1, base, nf.node, nf.breakdown, nf.down_service,
197 nf.breakdown_reset);
198 if (nf.has_repair) e.add_node_repair(nf.node, nf.repair, nf.repair_reset);
199 }
200 return e;
201}
202
203} // namespace detail
204
205/**
206 * Build an `env::Environment<T>` from a parsed model.json envelope.
207 *
208 * `init()` is NOT called here. It superposes the arcs into the marked processes
209 * the analyzer integrates against, and it throws when a stage has no finite
210 * holding time; that diagnostic belongs to the solve, next to the options that
211 * chose it, and not to the parse. Every caller in this port calls `init()`
212 * itself, exactly as the reference's `env.init()` is a separate step from
213 * building the object.
214 */
215template <class T>
217 using detail::json;
218 const json& model = root.contains("model") ? root.at("model") : root;
219 const std::string mtype = model.value("type", std::string(""));
220 if (mtype != "Environment")
221 throw UnsupportedError("environment_reader: model type '" + mtype +
222 "' is not an Environment; -s env solves a random-environment "
223 "model and the Network path solves the rest");
224 detail::reject_unconsumed_env_keys(model);
225
226 if (!model.contains("stages"))
227 throw InputError("environment_reader: the environment declares no 'stages'");
228 const json& stages = model.at("stages");
229 const std::size_t D = stages.size();
230 if (D == 0) throw InputError("environment_reader: the environment declares no stage");
231 std::vector<std::string> declared_names(D);
232 for (std::size_t s = 0; s < D; ++s)
233 declared_names[s] = stages[s].value("name", std::string("Stage") + std::to_string(s + 1));
234
235 // The node-failure block, and which of its two roles applies. The rule is
236 // both readers': the block is a MACRO to expand only while no DOWN_<node>
237 // stage has been declared for it; once one has, the stages already carry
238 // the structure and the block carries only the reset policies.
239 std::vector<detail::NodeFailureSpec<T> > fails;
240 if (model.contains("nodeFailures"))
241 for (const json& nf : model.at("nodeFailures"))
242 fails.push_back(detail::node_failure_fields<T>(nf));
243 bool macro = !fails.empty();
244 for (std::size_t k = 0; k < fails.size() && macro; ++k) {
245 const std::string down = env::Environment<T>::down_stage_name(fails[k].node);
246 for (std::size_t s = 0; s < D; ++s)
247 if (declared_names[s] == down) macro = false;
248 }
249 for (std::size_t k = 0; k < fails.size(); ++k)
250 for (std::size_t j = k + 1; j < fails.size(); ++j)
251 if (fails[k].node == fails[j].node)
252 throw InputError("environment_reader: 'nodeFailures' declares node '" +
253 fails[k].node +
254 "' twice, and a node has ONE down stage; merge the two entries");
255
256 if (macro) return detail::expand_node_failures<T>(model, stages[0], fails);
257
258 const std::size_t E = D;
259 // `numStages` is written by both writers and is the count the transitions
260 // are indexed against, so a disagreement with the array length means the
261 // file is inconsistent and the arcs cannot be trusted to point where they
262 // say. Checking it costs nothing and turns a wrong answer into a message.
263 if (model.contains("numStages")) {
264 const std::size_t declared = model.at("numStages").get<std::size_t>();
265 if (declared != E)
266 throw InputError("environment_reader: 'numStages' is " + std::to_string(declared) +
267 " but 'stages' holds " + std::to_string(E) + " entries");
268 }
269
270 env::Environment<T> e(model.value("name", std::string("env")), E);
271 for (std::size_t s = 0; s < E; ++s) {
272 const json& st = stages[s];
273 const std::string nm = st.value("name", std::string("Stage") + std::to_string(s + 1));
274 // A stage with no network is not a stage ENV can solve: the analyzer
275 // runs a transient solve per stage per iteration, so the missing model
276 // would surface as an empty drift rather than as a missing input.
277 if (!st.contains("model"))
278 throw InputError("environment_reader: stage '" + nm +
279 "' carries no 'model'; every stage of a random environment holds "
280 "the network in force while it lasts");
281 qn::Network<T> net = build_network_from_json<T>(st.at("model"));
282 e.set_stage(s, nm, st.value("type", std::string("")), net.get_struct());
283 }
284
285 if (model.contains("transitions")) {
286 for (const json& tr : model.at("transitions")) {
287 if (!tr.contains("from") || !tr.contains("to"))
288 throw InputError(
289 "environment_reader: an environment transition is missing 'from' or 'to'");
290 const long long from = tr.at("from").get<long long>();
291 const long long to = tr.at("to").get<long long>();
292 if (from < 0 || to < 0 || static_cast<std::size_t>(from) >= E ||
293 static_cast<std::size_t>(to) >= E)
294 throw InputError("environment_reader: transition " + std::to_string(from) + " -> " +
295 std::to_string(to) +
296 " names a stage outside 0.." + std::to_string(E - 1) +
297 " (the wire index is zero-based in both writers)");
298 if (!tr.contains("distribution"))
299 throw InputError("environment_reader: the transition " + std::to_string(from) +
300 " -> " + std::to_string(to) +
301 " carries no 'distribution'; the arc holding time is what the "
302 "environment process is made of");
303 e.add_transition(static_cast<std::size_t>(from), static_cast<std::size_t>(to),
304 detail::dist_from_json<T>(tr.at("distribution")));
305 }
306 }
307
308 // Re-attach the descriptors and their reset policies to the stages just
309 // built, so the environment is the one it was written from: the arcs came
310 // off the wire, the policies could not, and only these entries carry them.
311 for (std::size_t k = 0; k < fails.size(); ++k) {
312 const detail::NodeFailureSpec<T>& nf = fails[k];
313 e.register_node_failure(nf.node, nf.breakdown, nf.down_service, nf.has_repair, nf.repair,
314 nf.breakdown_reset, nf.repair_reset);
315 }
316 return e;
317}
318
319/** Parse a model.json file into an `env::Environment<T>`. */
320template <class T>
322 std::ifstream in(path.c_str());
323 if (!in) throw InputError("environment_reader: cannot open " + path);
324 detail::json root;
325 try {
326 in >> root;
327 } catch (const detail::json::parse_error& err) {
328 throw InputError("environment_reader: malformed JSON in " + path + ": " + err.what());
329 }
331}
332
333} // namespace io
334} // namespace line
335
336#endif // LINE_IO_ENVIRONMENT_READER_H
InputError(const std::string &what)
Definition error.h:39
UnsupportedError(const std::string &what)
Definition error.h:51
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,...
static std::string down_stage_name(const std::string &nm)
The name addNodeBreakdown gives the stage in which nm is down.
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.
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.
A queueing network under construction.
const NetworkStruct< T > & get_struct()
The refreshed struct, MATLAB's model.getStruct().
A random environment: a port of matlab/src/lang/Environment.m, restricted to what SolverENV reads out...
The exception types the port throws.
env::Environment< T > build_environment_from_json(const detail::json &root)
Build an env::Environment<T> from a parsed model.json envelope.
env::Environment< T > read_environment_json(const std::string &path)
Parse a model.json file into an env::Environment<T>.
qn::Network< T > build_network_from_json(const detail::json &root)
Build a qn::Network<T> from a parsed model.json envelope.
Reader for the LINE model.json interchange (a Network model) into a qn::Network<T> built through the ...
Number-type abstraction for the templated API port.