5#ifndef LINE_API_SYM_SAGE_REST_ENGINE_H
6#define LINE_API_SYM_SAGE_REST_ENGINE_H
49using Json = nlohmann::json;
57inline const char* canary_expr() {
58 return "(x*exp(-x) + exp(-1))/(exp(-x) + exp(-1))";
62inline const char* canary_arg() {
return "0.68999999999999995"; }
65const double CANARY_VALUE = 0.82116556904906557;
68inline std::map<std::string, bool>& usable_cache() {
69 static std::map<std::string, bool>
cache;
74inline std::mutex& usable_mutex() {
84inline std::string decimal_string(
double v) {
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);
90 std::snprintf(buf,
sizeof(buf),
"%.17g", v);
91 return std::string(buf);
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>());
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>();
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]);
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]);
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]);
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]);
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]) {
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",
"");
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>());
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");
228 timeoutSeconds_ = seconds;
238 std::string
name()
const override {
return "sage"; }
242 const detail::Json health = get(
"/api/v1/health", 5000);
243 return detail::opt_string(health,
"status",
"") ==
"ok";
244 }
catch (
const Error&) {
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;
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;
288 }
catch (
const Error&) {
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;
300 std::lock_guard<std::mutex> guard(detail::usable_mutex());
301 detail::usable_cache()[baseUrl_] = ok;
312 detail::Json
info()
const {
return get(
"/api/v1/info", 5000); }
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();
346 if (it->is_string() && it->get<std::string>() == feature)
return true;
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) {
361 for (detail::Json::const_iterator it = served.begin(); it != served.end(); ++it) {
362 if (it->is_string() && it->get<std::string>() == kRequired[i]) {
367 if (!found) missing.push_back(kRequired[i]);
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);
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>());
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;
410 request[
"ratios"] = arr;
412 const detail::Json response = post(
"/api/v1/ctmc/measures", request);
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");
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);
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>());
453 out.
unreachable = detail::to_int_list(response,
"unreachable");
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);
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",
"");
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");
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");
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) {
504 values[it->first] = detail::decimal_string(it->second);
506 request[
"values"] = values;
507 const detail::Json response = post(
"/api/v1/eval", request);
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>());
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() &&
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);
543 if (!domain.empty()) request[
"domain"] = domain;
544 const detail::Json response = post(
"/api/v1/fluid/odes", request);
547 if (response.contains(
"jacobian") && !response[
"jacobian"].is_null()) {
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>());
555 if (response.contains(
"latex") && !response[
"latex"].is_null()) {
557 odes.
latex = detail::to_string_list(response,
"latex");
559 if (response.contains(
"equilibria") && !response[
"equilibria"].is_null()) {
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>();
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(
577 checkStatus(path, response);
581 detail::Json get(
const std::string& path,
int millis)
const {
582 return parse(
http::get(baseUrl_ + path, millis));
585 static detail::Json parse(
const http::Response& response) {
586 if (response.body.empty())
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);
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;
599 detail::opt_string(response,
"code",
"error") +
600 "]: " + detail::opt_string(response,
"message",
"unspecified error"));
603 std::string baseUrl_;
Base error for the multiprecision C++ port.
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
int getTimeoutSeconds() 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)
A computer algebra backend.
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.
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.
Response get(const std::string &url, int timeoutMillis)
GET a URL.
Conservation laws of a layered queueing network, enumerated from its structure.
One weighted sum of the stationary distribution, or a ratio of two.
std::string name
Caller supplied name.
std::string num
Numerator of expr, empty when undefined.
std::string reason
Why the value is undefined, empty otherwise.
std::string expr
The value as a rational function, or "undefined".
std::string den
Denominator of expr, empty when undefined.
Stationary distribution together with the measures taken from it.
std::vector< CtmcMeasure > ratios
One per requested ratio, in order.
std::string den
Common denominator of the whole vector.
std::vector< CtmcMeasure > measures
One per weight vector, in order.
int nConnComp
Weakly connected components.
std::vector< std::string > num
Numerator of each entry over den.
std::vector< int > connComp
Component index of each state, one based.
std::vector< std::string > pi
Stationary probability of each state.
Symbolic stationary distribution of a CTMC.
std::vector< std::string > pi
Stationary probability of each state, as an expression.
std::vector< int > connComp
Component index of each state, one based.
std::vector< std::string > num
Numerator of each entry over the common denominator.
int nConnComp
Weakly connected components of the generator.
std::string den
Common denominator of the whole vector.
Symbolic analysis of a fluid vector field.
std::vector< std::string > latex
LaTeX form of each right hand side.
std::vector< std::map< std::string, std::string > > equilibria
variable -> expression
std::vector< std::vector< std::string > > jacobian
d f_i / d x_j
First passage time transform and moments.
std::vector< std::vector< std::string > > momAll
Per start state moments.
std::vector< std::string > lstAll
Per start state transform.
std::string lstNum
Numerator of lst.
std::string lst
The transform L(s), empty if not requested.
std::vector< std::string > moments
Moments 1..nmax for the initial law.
std::vector< int > unreachable
States that cannot reach the target.
std::string lstDen
Denominator of lst.
Exact parametric sensitivity, following Trivedi and Bobbio (2017), Sec.
bool hasReward
Whether Er, S and SS were computed.
std::vector< std::string > dpi
Derivative of the distribution with respect to theta.
std::vector< std::string > pi
Stationary distribution.
std::string SS
Scaled sensitivity (theta/E[r]) d(E[r])/dtheta, Eq. (9.80).
std::string Er
Mean reward, empty if no reward was given.
std::string S
Unscaled sensitivity d(E[r])/dtheta, Eq. (9.79).
Computer algebra operations LINE needs, as seen by this port.