LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
workflow_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_WORKFLOW_READER_H
6#define LINE_IO_WORKFLOW_READER_H
7
8/**
9 * @file
10 * @ingroup line_io
11 * Reader for the LINE `model.json` interchange (a Workflow model) into a
12 * `workflow::Workflow<T>` built through the programmatic builder.
13 *
14 * The wire format is the one `linemodel_save.m::workflow2json` and the Python
15 * `_workflow_to_json` emit: a `{format, version, model}` envelope whose
16 * `model.type == "Workflow"` carries `activities` (name plus `hostDemand`) and
17 * `precedences` (`preActs`, `postActs`, `preType`, `postType` and the optional
18 * `preParams` / `postParams`). Activities and precedences are fed to the SAME
19 * `add_activity` / `add_precedence` the programmatic API uses, so a workflow
20 * that reaches C++ this way is indistinguishable from one authored in code.
21 *
22 * WHY THIS EXISTS SEPARATELY from `network_reader.h`: a Workflow is not a
23 * queueing network and shares none of its node, class or routing structure. It
24 * reduces to ONE phase-type law, so it has no stations to solve. The two
25 * readers share only `detail::dist_from_json`, which is why this header
26 * includes `network_reader.h` rather than duplicating the distribution table.
27 *
28 * WHAT IT REFUSES, and by name rather than by silent degradation: a precedence
29 * type the wire spells but this port has no composition rule for, an activity
30 * whose `hostDemand` names a family `dist_from_json` cannot reconstruct, and
31 * any unknown key at model / activity / precedence level. The last of these is
32 * the same rule `network_reader.h` states at length: a key carrying model
33 * semantics that no branch consumes would otherwise be dropped in silence, and
34 * `to_ph()` would then return a confident law for a different workflow.
35 *
36 * The SEMANTIC checks (a loop count that is not a positive scalar, a quorum
37 * join, a graph that is not series-parallel) are NOT repeated here. They belong
38 * to `Workflow::validate()` and `to_ph()`, which every construction path runs,
39 * so duplicating them would let the two drift apart.
40 */
41
42#include <string>
43#include <vector>
44
45#include "json.hpp"
49#include "line/util/error.h"
50
51namespace line {
52namespace io {
53
54namespace detail {
55
56/**
57 * Map the wire's precedence spelling to `lang::PrecedenceType`.
58 *
59 * The strings are the JAR-compatible ones `linemodel_save.m::prectype_to_str`
60 * writes; the Python writer emits the same set. `POST_CACHE` is spelled by the
61 * wire and accepted here, because refusing it at the READER would misreport a
62 * layered cache-queueing workflow as unrepresentable when the truth is that
63 * `to_ph` has no rule for it -- which `Workflow::validate()` says in its own
64 * words, at the point where it is actually true.
65 */
66inline lang::PrecedenceType precedence_type_from_str(const std::string& s) {
67 if (s == "pre") return lang::PrecedenceType::PRE_SEQ;
68 if (s == "pre-AND") return lang::PrecedenceType::PRE_AND;
69 if (s == "pre-OR") return lang::PrecedenceType::PRE_OR;
70 if (s == "post") return lang::PrecedenceType::POST_SEQ;
71 if (s == "post-AND") return lang::PrecedenceType::POST_AND;
72 if (s == "post-OR") return lang::PrecedenceType::POST_OR;
73 if (s == "post-LOOP") return lang::PrecedenceType::POST_LOOP;
74 if (s == "post-CACHE") return lang::PrecedenceType::POST_CACHE;
75 throw UnsupportedError("workflow_reader: unsupported precedence type '" + s +
76 "'; the wire spells one of pre, pre-AND, pre-OR, post, post-AND, "
77 "post-OR, post-LOOP, post-CACHE");
78}
79
80/** Read a `preActs` / `postActs` array as a list of activity names. */
81inline std::vector<std::string> activity_names_from_json(const json& arr, const char* key) {
82 if (!arr.is_array())
83 throw InputError(std::string("workflow_reader: '") + key + "' must be an array of "
84 "activity names");
85 std::vector<std::string> out;
86 out.reserve(arr.size());
87 for (const json& v : arr) {
88 if (!v.is_string())
89 throw InputError(std::string("workflow_reader: '") + key +
90 "' must hold activity NAMES, not objects; the wire references an "
91 "activity by the name its 'activities' entry declares");
92 out.push_back(v.get<std::string>());
93 }
94 return out;
95}
96
97/** Read an optional `preParams` / `postParams` array of scalars. */
98template <class T>
99std::vector<T> params_from_json(const json& obj, const char* key) {
100 std::vector<T> out;
101 if (!obj.contains(key)) return out;
102 const json& arr = obj.at(key);
103 // A scalar is accepted as the one-element form: `Loop` writes its count as
104 // a bare number in some writers and as a singleton array in others.
105 if (arr.is_number()) {
106 out.push_back(num_traits<T>::from_double(num_from_json(arr)));
107 return out;
108 }
109 if (!arr.is_array())
110 throw InputError(std::string("workflow_reader: '") + key +
111 "' must be a number or an array of numbers");
112 for (const json& v : arr) out.push_back(num_traits<T>::from_double(num_from_json(v)));
113 return out;
114}
115
116/**
117 * Refuse any key this reader does not consume, at every level.
118 *
119 * Same rule as `network_reader.h::reject_unknown_keys`, and for the same
120 * reason: a dropped key is indistinguishable from a key that was never there.
121 */
122inline void reject_unknown_workflow_keys(const json& model) {
123 static const char* kModelKeys[] = {"type", "name", "activities", "precedences"};
124 static const char* kActKeys[] = {"name", "hostDemand"};
125 static const char* kPrecKeys[] = {"preActs", "postActs", "preType", "postType",
126 "preParams", "postParams"};
127 auto known = [](const char* const* tab, std::size_t n, const std::string& k) {
128 for (std::size_t i = 0; i < n; ++i)
129 if (k == tab[i]) return true;
130 return false;
131 };
132 const std::string why =
133 "', which this reader does not implement. Refusing rather than dropping it: a "
134 "constraint silently discarded here would make to_ph() return a confident law "
135 "for a different workflow";
136 for (auto it = model.begin(); it != model.end(); ++it)
137 if (!known(kModelKeys, sizeof(kModelKeys) / sizeof(*kModelKeys), it.key()))
138 throw UnsupportedError("workflow_reader: the model carries '" + it.key() + why);
139 if (model.contains("activities"))
140 for (const json& act : model.at("activities"))
141 for (auto it = act.begin(); it != act.end(); ++it)
142 if (!known(kActKeys, sizeof(kActKeys) / sizeof(*kActKeys), it.key()))
143 throw UnsupportedError("workflow_reader: activity '" +
144 act.value("name", std::string("?")) + "' carries '" +
145 it.key() + why);
146 if (model.contains("precedences"))
147 for (const json& prec : model.at("precedences"))
148 for (auto it = prec.begin(); it != prec.end(); ++it)
149 if (!known(kPrecKeys, sizeof(kPrecKeys) / sizeof(*kPrecKeys), it.key()))
150 throw UnsupportedError("workflow_reader: a precedence carries '" + it.key() +
151 why);
152}
153
154} // namespace detail
155
156/**
157 * Build a `workflow::Workflow<T>` from a parsed model.json envelope.
158 *
159 * One pass: an activity is declared before any precedence may reference it,
160 * which the wire guarantees by carrying `activities` as its own block. The
161 * result is NOT validated here -- `validate()` and `to_ph()` do that on the
162 * composed model, where the series-parallel and quorum rules actually apply.
163 */
164template <class T>
166 using detail::json;
167 const json& model = root.contains("model") ? root.at("model") : root;
168 const std::string mtype = model.value("type", std::string("Workflow"));
169 if (mtype != "Workflow")
170 throw UnsupportedError("workflow_reader: model type '" + mtype +
171 "' is not a Workflow; a Network is read by network_reader.h and an "
172 "Environment by environment_reader.h");
173
174 detail::reject_unknown_workflow_keys(model);
175
176 workflow::Workflow<T> wf(model.value("name", std::string("workflow")));
177
178 if (model.contains("activities")) {
179 for (const json& act : model.at("activities")) {
180 if (!act.contains("name"))
181 throw InputError("workflow_reader: every activity needs a 'name'");
182 const std::string name = act.at("name").get<std::string>();
183 if (act.contains("hostDemand"))
184 wf.add_activity(name, detail::dist_from_json<T>(act.at("hostDemand")));
185 else
186 // The writers ALWAYS emit a hostDemand (a float mean is written
187 // as the Exp of that mean), so an absent one is a hand-authored
188 // document. Unit mean matches what the Python reader does.
190 }
191 }
192
193 if (model.contains("precedences")) {
194 for (const json& prec : model.at("precedences")) {
195 if (!prec.contains("preActs") || !prec.contains("postActs"))
196 throw InputError("workflow_reader: every precedence needs 'preActs' and "
197 "'postActs'");
199 p.pre_acts = detail::activity_names_from_json(prec.at("preActs"), "preActs");
200 p.post_acts = detail::activity_names_from_json(prec.at("postActs"), "postActs");
201 p.pre_type =
202 detail::precedence_type_from_str(prec.value("preType", std::string("pre")));
203 p.post_type =
204 detail::precedence_type_from_str(prec.value("postType", std::string("post")));
205 p.pre_params = detail::params_from_json<T>(prec, "preParams");
206 p.post_params = detail::params_from_json<T>(prec, "postParams");
207 wf.add_precedence(p);
208 }
209 }
210
211 return wf;
212}
213
214/** Read a Workflow model.json off disk. */
215template <class T>
217 std::ifstream in(path.c_str());
218 if (!in)
219 throw InputError("workflow_reader: cannot open '" + path + "'");
220 detail::json root;
221 in >> root;
222 return build_workflow_from_json<T>(root);
223}
224
225} // namespace io
226} // namespace line
227
228#endif // LINE_IO_WORKFLOW_READER_H
InputError(const std::string &what)
Definition error.h:39
UnsupportedError(const std::string &what)
Definition error.h:51
The exception types the port throws.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
workflow::Workflow< T > read_workflow_json(const std::string &path)
Read a Workflow model.json off disk.
workflow::Workflow< T > build_workflow_from_json(const detail::json &root)
Build a workflow::Workflow<T> from a parsed model.json envelope.
PrecedenceType
Activity precedence kinds, with the values of MATLAB ActivityPrecedenceType.
Definition lang_types.h:470
Reader for the LINE model.json interchange (a Network model) into a qn::Network<T> built through the ...
static Distrib exp_mean(const T &m)
Definition lang_types.h:799
One precedence of the activity graph.
Definition workflow.h:78
std::vector< T > pre_params
Definition workflow.h:83
PrecedenceType pre_type
Definition workflow.h:81
PrecedenceType post_type
Definition workflow.h:82
std::vector< T > post_params
Definition workflow.h:84
std::vector< std::string > post_acts
Definition workflow.h:80
std::vector< std::string > pre_acts
Definition workflow.h:79
An activity workflow reduced to one phase-type law.