LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
sage_rest_engine.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_API_SYM_SAGE_REST_ENGINE_H
6#define LINE_API_SYM_SAGE_REST_ENGINE_H
7
8/**
9 * @file
10 * @ingroup api_sym
11 * SymEngine backed by the line-sage-rest service.
12 *
13 * Port of jline.api.sym.SageRestEngine. The service is SageMath behind the JSON
14 * protocol in io/sage/server.py. Every request is a single POST carrying the whole
15 * problem, so nothing is bind-mounted and the client works against a container,
16 * a remote host or a hand-started server alike.
17 *
18 * NUMERIC COEFFICIENTS ARE SENT AS DECIMAL STRINGS and read server side as
19 * exact rationals, which is what keeps the solve exact: a double coerced by the
20 * CAS would carry the binary rational nearest the decimal instead, and the
21 * difference survives all the way into the printed normal form. The same rule
22 * applies to the assignment eval() sends.
23 *
24 * The server enforces the timeout too, so a runaway symbolic solve is killed
25 * there rather than merely abandoned here.
26 */
27
28#include <algorithm>
29#include <cmath>
30#include <cstddef>
31#include <cstdio>
32#include <cstdlib>
33#include <iostream>
34#include <map>
35#include <mutex>
36#include <string>
37#include <vector>
38
39#include "json.hpp"
41#include "line/util/error.h"
42#include "line/util/http.h"
43
44namespace line {
45namespace sym {
46
47namespace detail {
48
49using Json = nlohmann::json;
50
51/**
52 * Canary for SageRestEngine::isUsable: the weighted-average softmin form, the
53 * smallest expression observed to kill a worker whose FLINT wants BMI2/ADX on
54 * a CPU that has neither. Its argument is a 17-digit decimal so the exact
55 * rational is multi-limb, which is what reaches the offending routine.
56 */
57inline const char* canary_expr() {
58 return "(x*exp(-x) + exp(-1))/(exp(-x) + exp(-1))";
59}
60
61/** @return the canary's argument */
62inline const char* canary_arg() { return "0.68999999999999995"; }
63
64/** The canary's value. */
65const double CANARY_VALUE = 0.82116556904906557;
66
67/** @return the usability verdicts, keyed by base URL */
68inline std::map<std::string, bool>& usable_cache() {
69 static std::map<std::string, bool> cache;
70 return cache;
71}
72
73/** @return the mutex guarding usable_cache */
74inline std::mutex& usable_mutex() {
75 static std::mutex m;
76 return m;
77}
78
79/**
80 * The shortest decimal literal that round-trips to v, i.e. what Java's
81 * Double.toString sends. Twin of line::reg::shortest_decimal, repeated here so
82 * that api/sym stays independent of the numeric core it never otherwise uses.
83 */
84inline std::string decimal_string(double v) {
85 char buf[64];
86 for (int prec = 1; prec <= 17; ++prec) {
87 std::snprintf(buf, sizeof(buf), "%.*g", prec, v);
88 if (std::strtod(buf, nullptr) == v) return std::string(buf);
89 }
90 std::snprintf(buf, sizeof(buf), "%.17g", v);
91 return std::string(buf);
92}
93
94/** Reads a string array field, empty when the field is absent or null. */
95inline std::vector<std::string> to_string_list(const Json& obj, const std::string& field) {
96 std::vector<std::string> out;
97 if (!obj.contains(field) || obj[field].is_null()) return out;
98 for (const Json& el : obj[field]) out.push_back(el.get<std::string>());
99 return out;
100}
101
102/** Reads a string field, or the fallback when it is absent or null. */
103inline std::string opt_string(const Json& obj, const std::string& field,
104 const std::string& fallback) {
105 if (obj.contains(field) && !obj[field].is_null()) return obj[field].get<std::string>();
106 return fallback;
107}
108
109/** Encodes a square expression matrix, refusing a ragged one. */
110inline Json to_json_matrix(const std::vector<std::vector<std::string>>& Q) {
111 if (Q.empty()) throw InputError("SageRestEngine: Q must not be empty");
112 Json rows = Json::array();
113 for (std::size_t i = 0; i < Q.size(); ++i) {
114 if (Q[i].size() != Q.size())
115 throw InputError("SageRestEngine: Q must be square, row " + std::to_string(i) +
116 " has " + std::to_string(Q[i].size()) + " entries but Q has " +
117 std::to_string(Q.size()) + " rows");
118 Json row = Json::array();
119 for (std::size_t j = 0; j < Q[i].size(); ++j)
120 row.push_back(Q[i][j].empty() ? std::string("0") : Q[i][j]);
121 rows.push_back(row);
122 }
123 return rows;
124}
125
126/**
127 * Encodes a string list, dropping empty entries. An empty symbol marks an event
128 * with no positive rate, as symbolicGeneratorResult.symbols does in the JAR; it
129 * contributes nothing and must not reach the server as "".
130 */
131inline Json to_json_array(const std::vector<std::string>& items) {
132 Json arr = Json::array();
133 for (std::size_t i = 0; i < items.size(); ++i)
134 if (!items[i].empty()) arr.push_back(items[i]);
135 return arr;
136}
137
138/**
139 * A possibly NON-SQUARE block, e.g. a weight block.
140 *
141 * to_json_matrix is for the generator and insists on a square matrix; a weight
142 * block is (number of measures) by (number of states) and is rarely square. The
143 * column count is still checked, because a short row would otherwise be a
144 * silently wrong measure rather than an error.
145 */
146inline Json to_json_rows(const std::vector<std::vector<std::string>>& rows, std::size_t width) {
147 Json out = Json::array();
148 for (std::size_t i = 0; i < rows.size(); ++i) {
149 if (rows[i].size() != width)
150 throw InputError("SageRestEngine: weight row " + std::to_string(i) + " has " +
151 std::to_string(rows[i].size()) + " entries but the generator has " +
152 std::to_string(width) + " states");
153 Json row = Json::array();
154 for (std::size_t j = 0; j < rows[i].size(); ++j)
155 row.push_back(rows[i][j].empty() ? std::string("0") : rows[i][j]);
156 out.push_back(row);
157 }
158 return out;
159}
160
161/**
162 * A vector whose LENGTH IS PART OF THE CONTRACT, with an empty entry meaning
163 * zero rather than absent.
164 *
165 * Deliberately not to_json_array: that one DROPS empty entries, which is right
166 * for a symbol list (an event with no positive rate contributes no symbol) and
167 * catastrophic for s0 or alpha, where dropping an entry shifts every state
168 * after it and the server would see a shorter vector than the matrix.
169 */
170inline Json to_json_vector(const std::vector<std::string>& items) {
171 Json arr = Json::array();
172 for (std::size_t i = 0; i < items.size(); ++i)
173 arr.push_back(items[i].empty() ? std::string("0") : items[i]);
174 return arr;
175}
176
177inline std::vector<CtmcMeasure> to_measure_list(const Json& obj, const std::string& field) {
178 std::vector<CtmcMeasure> out;
179 if (!obj.contains(field) || obj[field].is_null()) return out;
180 for (const Json& el : obj[field]) {
181 CtmcMeasure m;
182 m.name = opt_string(el, "name", "");
183 m.expr = opt_string(el, "expr", "");
184 m.num = opt_string(el, "num", "");
185 m.den = opt_string(el, "den", "");
186 m.reason = opt_string(el, "reason", "");
187 out.push_back(m);
188 }
189 return out;
190}
191
192inline std::vector<int> to_int_list(const Json& obj, const std::string& field) {
193 std::vector<int> out;
194 if (!obj.contains(field) || obj[field].is_null()) return out;
195 for (const Json& el : obj[field]) out.push_back(el.get<int>());
196 return out;
197}
198
199} // namespace detail
200
201/** Client of the line-sage-rest service. */
202class SageRestEngine : public SymEngine {
203public:
204 /** Default per-request timeout, in seconds. */
205 static constexpr int DEFAULT_TIMEOUT_SECONDS = 300;
206
207 /**
208 * @param baseUrl base URL of the service, e.g. "http://localhost:8080"
209 */
210 explicit SageRestEngine(const std::string& baseUrl)
211 : timeoutSeconds_(DEFAULT_TIMEOUT_SECONDS) {
212 std::string u = baseUrl;
213 const std::size_t b = u.find_first_not_of(" \t\r\n");
214 const std::size_t e = u.find_last_not_of(" \t\r\n");
215 u = b == std::string::npos ? std::string() : u.substr(b, e - b + 1);
216 while (!u.empty() && u[u.size() - 1] == '/') u.erase(u.size() - 1);
217 if (u.empty()) throw InputError("SageRestEngine: baseUrl must not be empty");
218 baseUrl_ = u;
219 }
220
221 /**
222 * Sets the per-request timeout.
223 *
224 * @param seconds timeout in seconds; not positive disables it
225 * @return this engine
226 */
228 timeoutSeconds_ = seconds;
229 return *this;
230 }
231
232 /** @return the per-request timeout in seconds */
233 int getTimeoutSeconds() const { return timeoutSeconds_; }
234
235 /** @return the base URL this engine posts to */
236 const std::string& getBaseUrl() const { return baseUrl_; }
237
238 std::string name() const override { return "sage"; }
239
240 bool isAvailable() const override {
241 try {
242 const detail::Json health = get("/api/v1/health", 5000);
243 return detail::opt_string(health, "status", "") == "ok";
244 } catch (const Error&) {
245 return false;
246 }
247 }
248
249 /**
250 * Checks that the service can actually EVALUATE, not merely that it
251 * answers.
252 *
253 * The line-sage-rest image ships a FLINT built for CPUs that have BMI2 and
254 * ADX. On an older host the first multi-limb exact operation raises
255 * SIGILL, the worker dies mid-request and the call returns no bytes at
256 * all; /api/v1/health is pure Python and keeps answering, so it cannot see
257 * this. The canary is the weighted-average softmin form, which is what the
258 * fluid export actually sends, and is the smallest expression observed to
259 * trigger it. Verdicts are cached per URL, so this costs one small request
260 * the first time a service is considered and nothing after. See
261 * _kb/11-conventions-and-gotchas.md.
262 *
263 * @return true if the service returned the canary's value
264 */
265 bool isUsable() const {
266 {
267 std::lock_guard<std::mutex> guard(detail::usable_mutex());
268 std::map<std::string, bool>& cache = detail::usable_cache();
269 const std::map<std::string, bool>::const_iterator it = cache.find(baseUrl_);
270 if (it != cache.end()) return it->second;
271 }
272 bool ok = false;
273 try {
274 detail::Json request;
275 request["exprs"] = detail::Json::array({std::string(detail::canary_expr())});
276 detail::Json values = detail::Json::object();
277 values["x"] = std::string(detail::canary_arg());
278 request["values"] = values;
279 request["timeout_s"] = 30;
280 const detail::Json response =
281 parse(http::post_json(baseUrl_ + "/api/v1/eval", request.dump(), 60000));
282 checkStatus("/api/v1/eval", response);
283 if (response.contains("values") && response["values"].is_array() &&
284 response["values"].size() == 1 && !response["values"][0].is_null()) {
285 const double v = response["values"][0].get<double>();
286 ok = std::fabs(v - detail::CANARY_VALUE) < 1e-9;
287 }
288 } catch (const Error&) {
289 // A dead worker closes the connection without a reply, which
290 // surfaces as a transport error rather than a service one. Either
291 // way the backend cannot serve us.
292 ok = false;
293 }
294 if (!ok) {
295 std::cerr << "[LINE] Ignoring symbolic backend at " << baseUrl_
296 << ": it did not return the usability canary. On a CPU without "
297 << "BMI2/ADX the image's FLINT raises SIGILL mid-request." << std::endl;
298 }
299 {
300 std::lock_guard<std::mutex> guard(detail::usable_mutex());
301 detail::usable_cache()[baseUrl_] = ok;
302 }
303 return ok;
304 }
305
306 /**
307 * Reads the service identity, used to tell a line-sage-rest server apart
308 * from another line-*-rest service on the same conventional port.
309 *
310 * @return the /api/v1/info document
311 */
312 detail::Json info() const { return get("/api/v1/info", 5000); }
313
314 /**
315 * Names the routes this client calls that the service does not serve.
316 *
317 * A SERVICE CAN BE HEALTHY AND STILL TOO OLD, and that case used to reach
318 * the caller as a request failure rather than as a resolution failure. The
319 * roster ran a line-sage-rest image predating the ctmc/measures and
320 * ctmc/passage routes on 2026-09-11: it answered /api/v1/health, passed the
321 * `isUsable` arithmetic canary, and then failed every call to those routes
322 * with `unknown endpoint /api/v1/ctmc/measures` -- an error that reads as a
323 * defect in the solver rather than as an out-of-date container.
324 * /api/v1/info lists what the running server actually routes, so asking it
325 * turns that into a named refusal at resolution time.
326 *
327 * @return the missing routes; empty when the service serves them all, and
328 * empty when it is too old to report its route list at all, which
329 * leaves the request itself to fail as before
330 */
331 /**
332 * True when /api/v1/info lists `feature` among the service's capabilities.
333 *
334 * The route list cannot say this: a 1.0.0 image serves /api/v1/fluid/odes
335 * and silently drops the `constraints` field, so the capability is
336 * advertised by name in `features`, which an older service does not carry.
337 *
338 * @param feature a capability name, e.g. "fluid_odes_constraints"
339 * @return false as well when the service reports no feature list at all
340 */
341 bool hasFeature(const std::string& feature) const {
342 const detail::Json doc = info();
343 if (!doc.contains("features") || !doc["features"].is_array()) return false;
344 for (detail::Json::const_iterator it = doc["features"].begin(); it != doc["features"].end();
345 ++it)
346 if (it->is_string() && it->get<std::string>() == feature) return true;
347 return false;
348 }
349
350 std::vector<std::string> missingRoutes() const {
351 static const char* kRequired[] = {
352 "/api/v1/ctmc/solve", "/api/v1/ctmc/sensitivity", "/api/v1/ctmc/measures",
353 "/api/v1/ctmc/passage", "/api/v1/simplify", "/api/v1/diff", "/api/v1/eval",
354 "/api/v1/fluid/odes"};
355 std::vector<std::string> missing;
356 const detail::Json doc = info();
357 if (!doc.contains("endpoints") || !doc["endpoints"].is_array()) return missing;
358 const detail::Json& served = doc["endpoints"];
359 for (std::size_t i = 0; i < sizeof(kRequired) / sizeof(kRequired[0]); ++i) {
360 bool found = false;
361 for (detail::Json::const_iterator it = served.begin(); it != served.end(); ++it) {
362 if (it->is_string() && it->get<std::string>() == kRequired[i]) {
363 found = true;
364 break;
365 }
366 }
367 if (!found) missing.push_back(kRequired[i]);
368 }
369 return missing;
370 }
371
372 CtmcSolution solveCTMC(const std::vector<std::vector<std::string>>& Q,
373 const std::vector<std::string>& symbols) override {
374 detail::Json request;
375 request["Q"] = detail::to_json_matrix(Q);
376 request["symbols"] = detail::to_json_array(symbols);
377 request["normalize"] = true;
378 const detail::Json response = post("/api/v1/ctmc/solve", request);
379
380 CtmcSolution sol;
381 sol.pi = detail::to_string_list(response, "pi");
382 sol.num = detail::to_string_list(response, "num");
383 sol.den = detail::opt_string(response, "den", "1");
384 sol.nConnComp = response.contains("nConnComp") ? response["nConnComp"].get<int>() : 1;
385 if (response.contains("connComp") && !response["connComp"].is_null())
386 for (const detail::Json& el : response["connComp"]) sol.connComp.push_back(el.get<int>());
387 return sol;
388 }
389
390 CtmcMeasures ctmcMeasures(const std::vector<std::vector<std::string>>& Q,
391 const std::vector<std::string>& symbols,
392 const std::vector<std::vector<std::string>>& weights,
393 const std::vector<std::string>& names,
394 const std::vector<RatioSpec>& ratios) override {
395 detail::Json request;
396 request["Q"] = detail::to_json_matrix(Q);
397 request["symbols"] = detail::to_json_array(symbols);
398 request["weights"] = detail::to_json_rows(weights, Q.size());
399 request["normalize"] = true;
400 if (!names.empty()) request["names"] = detail::to_json_vector(names);
401 if (!ratios.empty()) {
402 detail::Json arr = detail::Json::array();
403 for (std::size_t i = 0; i < ratios.size(); ++i) {
404 detail::Json o = detail::Json::object();
405 o["name"] = ratios[i].name;
406 o["num"] = ratios[i].num;
407 o["den"] = ratios[i].den;
408 arr.push_back(o);
409 }
410 request["ratios"] = arr;
411 }
412 const detail::Json response = post("/api/v1/ctmc/measures", request);
413
414 CtmcMeasures out;
415 out.pi = detail::to_string_list(response, "pi");
416 out.num = detail::to_string_list(response, "num");
417 out.den = detail::opt_string(response, "den", "1");
418 out.nConnComp = response.contains("nConnComp") ? response["nConnComp"].get<int>() : 1;
419 out.connComp = detail::to_int_list(response, "connComp");
420 out.measures = detail::to_measure_list(response, "measures");
421 out.ratios = detail::to_measure_list(response, "ratios");
422 return out;
423 }
424
425 Passage ctmcPassage(const std::vector<std::vector<std::string>>& S,
426 const std::vector<std::string>& s0,
427 const std::vector<std::string>& alpha, const std::string& atom,
428 const std::vector<std::string>& symbols, const std::string& svar,
429 const std::vector<std::string>& want, int nmax) override {
430 detail::Json request;
431 request["S"] = detail::to_json_matrix(S);
432 request["s0"] = detail::to_json_vector(s0);
433 request["alpha"] = detail::to_json_vector(alpha);
434 request["atom"] = atom.empty() ? std::string("0") : atom;
435 request["symbols"] = detail::to_json_array(symbols);
436 request["svar"] = svar.empty() ? std::string("s") : svar;
437 request["want"] = detail::to_json_vector(want);
438 request["nmax"] = nmax < 1 ? 1 : nmax;
439 const detail::Json response = post("/api/v1/ctmc/passage", request);
440
441 Passage out;
442 out.lst = detail::opt_string(response, "lst", "");
443 out.lstNum = detail::opt_string(response, "lst_num", "");
444 out.lstDen = detail::opt_string(response, "lst_den", "");
445 out.lstAll = detail::to_string_list(response, "lstall");
446 out.moments = detail::to_string_list(response, "moments");
447 if (response.contains("momall") && !response["momall"].is_null())
448 for (const detail::Json& row : response["momall"]) {
449 std::vector<std::string> r;
450 for (const detail::Json& el : row) r.push_back(el.get<std::string>());
451 out.momAll.push_back(r);
452 }
453 out.unreachable = detail::to_int_list(response, "unreachable");
454 return out;
455 }
456
457 SymSensitivity ctmcSensitivity(const std::vector<std::vector<std::string>>& Q,
458 const std::vector<std::string>& symbols,
459 const std::string& theta,
460 const std::vector<std::string>& reward) override {
461 detail::Json request;
462 request["Q"] = detail::to_json_matrix(Q);
463 request["symbols"] = detail::to_json_array(symbols);
464 request["theta"] = theta;
465 if (!reward.empty()) request["reward"] = detail::to_json_array(reward);
466 const detail::Json response = post("/api/v1/ctmc/sensitivity", request);
467
469 s.pi = detail::to_string_list(response, "pi");
470 s.dpi = detail::to_string_list(response, "dpi");
471 s.Er = detail::opt_string(response, "Er", "");
472 s.S = detail::opt_string(response, "S", "");
473 s.SS = detail::opt_string(response, "SS", "");
474 s.hasReward = !reward.empty();
475 return s;
476 }
477
478 std::vector<std::string> simplify(const std::vector<std::string>& exprs,
479 const std::string& form) override {
480 detail::Json request;
481 request["exprs"] = detail::to_json_array(exprs);
482 request["form"] = form.empty() ? std::string("cancel") : form;
483 return detail::to_string_list(post("/api/v1/simplify", request), "results");
484 }
485
486 std::vector<std::string> diff(const std::vector<std::string>& exprs,
487 const std::string& variable, int order) override {
488 detail::Json request;
489 request["exprs"] = detail::to_json_array(exprs);
490 request["var"] = variable;
491 request["order"] = order;
492 return detail::to_string_list(post("/api/v1/diff", request), "results");
493 }
494
495 std::vector<double> eval(const std::vector<std::string>& exprs,
496 const std::map<std::string, double>& assignment) override {
497 detail::Json request;
498 request["exprs"] = detail::to_json_array(exprs);
499 detail::Json values = detail::Json::object();
500 for (std::map<std::string, double>::const_iterator it = assignment.begin();
501 it != assignment.end(); ++it) {
502 // sent as text so the server reads the decimal exactly, see the
503 // header comment
504 values[it->first] = detail::decimal_string(it->second);
505 }
506 request["values"] = values;
507 const detail::Json response = post("/api/v1/eval", request);
508
509 std::vector<double> out;
510 if (!response.contains("values") || response["values"].is_null()) return out;
511 for (const detail::Json& el : response["values"])
512 out.push_back(el.is_null() ? std::nan("") : el.get<double>());
513 return out;
514 }
515
517
518 /**
519 * The `/api/v1/fluid/odes` call, constraints and domain included.
520 *
521 * A SERVICE OLDER THAN 1.1.0 IGNORES `constraints`, and a closed model's
522 * equilibria then form a continuum that it never finishes solving: the call
523 * would reach the caller as a timeout. So constrained equilibria are asked
524 * for only of a service whose /api/v1/info lists `fluid_odes_constraints`,
525 * and an older image is refused by name, as the Python client does.
526 */
527 FluidODEs fluidODEs(const std::vector<std::string>& rhs, const std::vector<std::string>& vars,
528 const std::vector<std::string>& want,
529 const std::vector<std::string>& constraints,
530 const std::string& domain) override {
531 detail::Json request;
532 request["rhs"] = detail::to_json_array(rhs);
533 request["vars"] = detail::to_json_array(vars);
534 request["want"] = detail::to_json_array(want);
535 if (!constraints.empty()) {
536 if (std::find(want.begin(), want.end(), std::string("equilibria")) != want.end() &&
537 !hasFeature("fluid_odes_constraints"))
538 throw SymEngineError("the line-sage-rest service at " + baseUrl_ +
539 " predates constrained fluid equilibria (needs image 1.1.0 "
540 "or newer, built from io/sage); rebuild or update the image");
541 request["constraints"] = detail::to_json_array(constraints);
542 }
543 if (!domain.empty()) request["domain"] = domain;
544 const detail::Json response = post("/api/v1/fluid/odes", request);
545
546 FluidODEs odes;
547 if (response.contains("jacobian") && !response["jacobian"].is_null()) {
548 odes.hasJacobian = true;
549 for (const detail::Json& row : response["jacobian"]) {
550 std::vector<std::string> r;
551 for (const detail::Json& el : row) r.push_back(el.get<std::string>());
552 odes.jacobian.push_back(r);
553 }
554 }
555 if (response.contains("latex") && !response["latex"].is_null()) {
556 odes.hasLatex = true;
557 odes.latex = detail::to_string_list(response, "latex");
558 }
559 if (response.contains("equilibria") && !response["equilibria"].is_null()) {
560 odes.hasEquilibria = true;
561 for (const detail::Json& sol : response["equilibria"]) {
562 std::map<std::string, std::string> m;
563 for (detail::Json::const_iterator it = sol.begin(); it != sol.end(); ++it)
564 m[it.key()] = it.value().get<std::string>();
565 odes.equilibria.push_back(m);
566 }
567 }
568 return odes;
569 }
570
571private:
572 detail::Json post(const std::string& path, detail::Json request) const {
573 const int millis = timeoutSeconds_ > 0 ? timeoutSeconds_ * 1000 : 0;
574 if (timeoutSeconds_ > 0) request["timeout_s"] = timeoutSeconds_;
575 const detail::Json response = parse(
576 http::post_json(baseUrl_ + path, request.dump(), millis));
577 checkStatus(path, response);
578 return response;
579 }
580
581 detail::Json get(const std::string& path, int millis) const {
582 return parse(http::get(baseUrl_ + path, millis));
583 }
584
585 static detail::Json parse(const http::Response& response) {
586 if (response.body.empty())
587 throw SymEngineError("line-sage-rest returned HTTP " +
588 std::to_string(response.status) + " with no body");
589 detail::Json parsed = detail::Json::parse(response.body, nullptr, false);
590 if (parsed.is_discarded() || !parsed.is_object())
591 throw SymEngineError("line-sage-rest returned a non-JSON body: " + response.body);
592 return parsed;
593 }
594
595 static void checkStatus(const std::string& path, const detail::Json& response) {
596 const std::string status = detail::opt_string(response, "status", "");
597 if (status == "ok") return;
598 throw SymEngineError("line-sage-rest " + path + " failed [" +
599 detail::opt_string(response, "code", "error") +
600 "]: " + detail::opt_string(response, "message", "unspecified error"));
601 }
602
603 std::string baseUrl_;
604 int timeoutSeconds_;
605};
606
607} // namespace sym
608} // namespace line
609
610#endif // LINE_API_SYM_SAGE_REST_ENGINE_H
Base error for the multiprecision C++ port.
Definition error.h:73
InputError(const std::string &what)
Definition error.h:105
SageRestEngine(const std::string &baseUrl)
static constexpr int DEFAULT_TIMEOUT_SECONDS
Default per-request timeout, in seconds.
std::vector< double > eval(const std::vector< std::string > &exprs, const std::map< std::string, double > &assignment) override
Substitutes values for symbols and evaluates.
detail::Json info() const
Reads the service identity, used to tell a line-sage-rest server apart from another line-*-rest servi...
std::vector< std::string > missingRoutes() const
bool hasFeature(const std::string &feature) const
Names the routes this client calls that the service does not serve.
CtmcSolution solveCTMC(const std::vector< std::vector< std::string > > &Q, const std::vector< std::string > &symbols) override
Symbolic stationary distribution of a CTMC, pi Q = 0 with sum(pi) = 1.
bool isUsable() const
Checks that the service can actually EVALUATE, not merely that it answers.
std::string name() const override
Name of the backing engine, e.g.
bool isAvailable() const override
True if the engine answers a health probe.
std::vector< std::string > diff(const std::vector< std::string > &exprs, const std::string &variable, int order) override
Differentiates expressions.
SageRestEngine & setTimeoutSeconds(int seconds)
Sets the per-request timeout.
const std::string & getBaseUrl() const
SymSensitivity ctmcSensitivity(const std::vector< std::vector< std::string > > &Q, const std::vector< std::string > &symbols, const std::string &theta, const std::vector< std::string > &reward) override
Exact parametric sensitivity of a steady-state reward.
Passage ctmcPassage(const std::vector< std::vector< std::string > > &S, const std::vector< std::string > &s0, const std::vector< std::string > &alpha, const std::string &atom, const std::vector< std::string > &symbols, const std::string &svar, const std::vector< std::string > &want, int nmax) override
First passage time into a target set: the transform and the moments.
std::vector< std::string > simplify(const std::vector< std::string > &exprs, const std::string &form) override
Rewrites expressions into a normal form.
CtmcMeasures ctmcMeasures(const std::vector< std::vector< std::string > > &Q, const std::vector< std::string > &symbols, const std::vector< std::vector< std::string > > &weights, const std::vector< std::string > &names, const std::vector< RatioSpec > &ratios) override
Stationary distribution and a set of weighted sums of it.
FluidODEs fluidODEs(const std::vector< std::string > &rhs, const std::vector< std::string > &vars, const std::vector< std::string > &want, const std::vector< std::string > &constraints, const std::string &domain) override
The /api/v1/fluid/odes call, constraints and domain included.
SymEngineError(const std::string &what)
Definition sym_engine.h:43
A computer algebra backend.
Definition sym_engine.h:131
FluidODEs fluidODEs(const std::vector< std::string > &rhs, const std::vector< std::string > &vars, const std::vector< std::string > &want)
Jacobian, LaTeX form and equilibria of a fluid vector field.
Definition sym_engine.h:262
The exception types the port throws.
Minimal HTTP/1.1 client, enough to talk to a line-*-rest service.
Response post_json(const std::string &url, const std::string &json, int timeoutMillis)
POST a JSON document.
Definition http.h:360
Response get(const std::string &url, int timeoutMillis)
GET a URL.
Definition http.h:348
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
One weighted sum of the stationary distribution, or a ratio of two.
Definition sym_engine.h:47
std::string name
Caller supplied name.
Definition sym_engine.h:48
std::string num
Numerator of expr, empty when undefined.
Definition sym_engine.h:50
std::string reason
Why the value is undefined, empty otherwise.
Definition sym_engine.h:60
std::string expr
The value as a rational function, or "undefined".
Definition sym_engine.h:49
std::string den
Denominator of expr, empty when undefined.
Definition sym_engine.h:51
Stationary distribution together with the measures taken from it.
Definition sym_engine.h:74
std::vector< CtmcMeasure > ratios
One per requested ratio, in order.
Definition sym_engine.h:81
std::string den
Common denominator of the whole vector.
Definition sym_engine.h:77
std::vector< CtmcMeasure > measures
One per weight vector, in order.
Definition sym_engine.h:80
int nConnComp
Weakly connected components.
Definition sym_engine.h:78
std::vector< std::string > num
Numerator of each entry over den.
Definition sym_engine.h:76
std::vector< int > connComp
Component index of each state, one based.
Definition sym_engine.h:79
std::vector< std::string > pi
Stationary probability of each state.
Definition sym_engine.h:75
Symbolic stationary distribution of a CTMC.
Definition sym_engine.h:96
std::vector< std::string > pi
Stationary probability of each state, as an expression.
Definition sym_engine.h:97
std::vector< int > connComp
Component index of each state, one based.
Definition sym_engine.h:101
std::vector< std::string > num
Numerator of each entry over the common denominator.
Definition sym_engine.h:98
int nConnComp
Weakly connected components of the generator.
Definition sym_engine.h:100
std::string den
Common denominator of the whole vector.
Definition sym_engine.h:99
Symbolic analysis of a fluid vector field.
Definition sym_engine.h:121
std::vector< std::string > latex
LaTeX form of each right hand side.
Definition sym_engine.h:123
std::vector< std::map< std::string, std::string > > equilibria
variable -> expression
Definition sym_engine.h:124
std::vector< std::vector< std::string > > jacobian
d f_i / d x_j
Definition sym_engine.h:122
First passage time transform and moments.
Definition sym_engine.h:85
std::vector< std::vector< std::string > > momAll
Per start state moments.
Definition sym_engine.h:91
std::vector< std::string > lstAll
Per start state transform.
Definition sym_engine.h:89
std::string lstNum
Numerator of lst.
Definition sym_engine.h:87
std::string lst
The transform L(s), empty if not requested.
Definition sym_engine.h:86
std::vector< std::string > moments
Moments 1..nmax for the initial law.
Definition sym_engine.h:90
std::vector< int > unreachable
States that cannot reach the target.
Definition sym_engine.h:92
std::string lstDen
Denominator of lst.
Definition sym_engine.h:88
Exact parametric sensitivity, following Trivedi and Bobbio (2017), Sec.
Definition sym_engine.h:111
bool hasReward
Whether Er, S and SS were computed.
Definition sym_engine.h:117
std::vector< std::string > dpi
Derivative of the distribution with respect to theta.
Definition sym_engine.h:113
std::vector< std::string > pi
Stationary distribution.
Definition sym_engine.h:112
std::string SS
Scaled sensitivity (theta/E[r]) d(E[r])/dtheta, Eq. (9.80).
Definition sym_engine.h:116
std::string Er
Mean reward, empty if no reward was given.
Definition sym_engine.h:114
std::string S
Unscaled sensitivity d(E[r])/dtheta, Eq. (9.79).
Definition sym_engine.h:115
Computer algebra operations LINE needs, as seen by this port.