5#ifndef LINE_API_SYM_SAGE_REST_ENGINE_H
6#define LINE_API_SYM_SAGE_REST_ENGINE_H
48using Json = nlohmann::json;
56inline const char* canary_expr() {
57 return "(x*exp(-x) + exp(-1))/(exp(-x) + exp(-1))";
61inline const char* canary_arg() {
return "0.68999999999999995"; }
64const double CANARY_VALUE = 0.82116556904906557;
67inline std::map<std::string, bool>& usable_cache() {
68 static std::map<std::string, bool>
cache;
73inline std::mutex& usable_mutex() {
83inline std::string decimal_string(
double v) {
85 for (
int prec = 1; prec <= 17; ++prec) {
86 std::snprintf(buf,
sizeof(buf),
"%.*g", prec, v);
87 if (std::strtod(buf,
nullptr) == v)
return std::string(buf);
89 std::snprintf(buf,
sizeof(buf),
"%.17g", v);
90 return std::string(buf);
94inline std::vector<std::string> to_string_list(
const Json& obj,
const std::string& field) {
95 std::vector<std::string> out;
96 if (!obj.contains(field) || obj[field].is_null())
return out;
97 for (
const Json& el : obj[field]) out.push_back(el.get<std::string>());
102inline std::string opt_string(
const Json& obj,
const std::string& field,
103 const std::string& fallback) {
104 if (obj.contains(field) && !obj[field].is_null())
return obj[field].get<std::string>();
109inline Json to_json_matrix(
const std::vector<std::vector<std::string>>& Q) {
110 if (Q.empty())
throw InputError(
"SageRestEngine: Q must not be empty");
111 Json rows = Json::array();
112 for (std::size_t i = 0; i < Q.size(); ++i) {
113 if (Q[i].size() != Q.size())
114 throw InputError(
"SageRestEngine: Q must be square, row " + std::to_string(i) +
115 " has " + std::to_string(Q[i].size()) +
" entries but Q has " +
116 std::to_string(Q.size()) +
" rows");
117 Json row = Json::array();
118 for (std::size_t j = 0; j < Q[i].size(); ++j)
119 row.push_back(Q[i][j].empty() ? std::string(
"0") : Q[i][j]);
130inline Json to_json_array(
const std::vector<std::string>& items) {
131 Json arr = Json::array();
132 for (std::size_t i = 0; i < items.size(); ++i)
133 if (!items[i].empty()) arr.push_back(items[i]);
145inline Json to_json_rows(
const std::vector<std::vector<std::string>>& rows, std::size_t width) {
146 Json out = Json::array();
147 for (std::size_t i = 0; i < rows.size(); ++i) {
148 if (rows[i].size() != width)
149 throw InputError(
"SageRestEngine: weight row " + std::to_string(i) +
" has " +
150 std::to_string(rows[i].size()) +
" entries but the generator has " +
151 std::to_string(width) +
" states");
152 Json row = Json::array();
153 for (std::size_t j = 0; j < rows[i].size(); ++j)
154 row.push_back(rows[i][j].empty() ? std::string(
"0") : rows[i][j]);
169inline Json to_json_vector(
const std::vector<std::string>& items) {
170 Json arr = Json::array();
171 for (std::size_t i = 0; i < items.size(); ++i)
172 arr.push_back(items[i].empty() ? std::string(
"0") : items[i]);
176inline std::vector<CtmcMeasure> to_measure_list(
const Json& obj,
const std::string& field) {
177 std::vector<CtmcMeasure> out;
178 if (!obj.contains(field) || obj[field].is_null())
return out;
179 for (
const Json& el : obj[field]) {
181 m.
name = opt_string(el,
"name",
"");
182 m.
expr = opt_string(el,
"expr",
"");
183 m.
num = opt_string(el,
"num",
"");
184 m.
den = opt_string(el,
"den",
"");
185 m.
reason = opt_string(el,
"reason",
"");
191inline std::vector<int> to_int_list(
const Json& obj,
const std::string& field) {
192 std::vector<int> out;
193 if (!obj.contains(field) || obj[field].is_null())
return out;
194 for (
const Json& el : obj[field]) out.push_back(el.get<
int>());
211 std::string u = baseUrl;
212 const std::size_t b = u.find_first_not_of(
" \t\r\n");
213 const std::size_t e = u.find_last_not_of(
" \t\r\n");
214 u = b == std::string::npos ? std::string() : u.substr(b, e - b + 1);
215 while (!u.empty() && u[u.size() - 1] ==
'/') u.erase(u.size() - 1);
216 if (u.empty())
throw InputError(
"SageRestEngine: baseUrl must not be empty");
227 timeoutSeconds_ = seconds;
237 std::string
name()
const override {
return "sage"; }
241 const detail::Json health = get(
"/api/v1/health", 5000);
242 return detail::opt_string(health,
"status",
"") ==
"ok";
243 }
catch (
const Error&) {
266 std::lock_guard<std::mutex> guard(detail::usable_mutex());
267 std::map<std::string, bool>&
cache = detail::usable_cache();
268 const std::map<std::string, bool>::const_iterator it =
cache.find(baseUrl_);
269 if (it !=
cache.end())
return it->second;
273 detail::Json request;
274 request[
"exprs"] = detail::Json::array({std::string(detail::canary_expr())});
275 detail::Json values = detail::Json::object();
276 values[
"x"] = std::string(detail::canary_arg());
277 request[
"values"] = values;
278 request[
"timeout_s"] = 30;
279 const detail::Json response =
280 parse(
http::post_json(baseUrl_ +
"/api/v1/eval", request.dump(), 60000));
281 checkStatus(
"/api/v1/eval", response);
282 if (response.contains(
"values") && response[
"values"].is_array() &&
283 response[
"values"].size() == 1 && !response[
"values"][0].is_null()) {
284 const double v = response[
"values"][0].get<
double>();
285 ok = std::fabs(v - detail::CANARY_VALUE) < 1e-9;
287 }
catch (
const Error&) {
294 std::cerr <<
"[LINE] Ignoring symbolic backend at " << baseUrl_
295 <<
": it did not return the usability canary. On a CPU without "
296 <<
"BMI2/ADX the image's FLINT raises SIGILL mid-request." << std::endl;
299 std::lock_guard<std::mutex> guard(detail::usable_mutex());
300 detail::usable_cache()[baseUrl_] = ok;
311 detail::Json
info()
const {
return get(
"/api/v1/info", 5000); }
331 static const char* kRequired[] = {
332 "/api/v1/ctmc/solve",
"/api/v1/ctmc/sensitivity",
"/api/v1/ctmc/measures",
333 "/api/v1/ctmc/passage",
"/api/v1/simplify",
"/api/v1/diff",
"/api/v1/eval",
334 "/api/v1/fluid/odes"};
335 std::vector<std::string> missing;
336 const detail::Json doc =
info();
337 if (!doc.contains(
"endpoints") || !doc[
"endpoints"].is_array())
return missing;
338 const detail::Json& served = doc[
"endpoints"];
339 for (std::size_t i = 0; i <
sizeof(kRequired) /
sizeof(kRequired[0]); ++i) {
341 for (detail::Json::const_iterator it = served.begin(); it != served.end(); ++it) {
342 if (it->is_string() && it->get<std::string>() == kRequired[i]) {
347 if (!found) missing.push_back(kRequired[i]);
353 const std::vector<std::string>& symbols)
override {
354 detail::Json request;
355 request[
"Q"] = detail::to_json_matrix(Q);
356 request[
"symbols"] = detail::to_json_array(symbols);
357 request[
"normalize"] =
true;
358 const detail::Json response = post(
"/api/v1/ctmc/solve", request);
361 sol.
pi = detail::to_string_list(response,
"pi");
362 sol.
num = detail::to_string_list(response,
"num");
363 sol.
den = detail::opt_string(response,
"den",
"1");
364 sol.
nConnComp = response.contains(
"nConnComp") ? response[
"nConnComp"].get<
int>() : 1;
365 if (response.contains(
"connComp") && !response[
"connComp"].is_null())
366 for (
const detail::Json& el : response[
"connComp"]) sol.
connComp.push_back(el.get<
int>());
371 const std::vector<std::string>& symbols,
372 const std::vector<std::vector<std::string>>& weights,
373 const std::vector<std::string>& names,
374 const std::vector<RatioSpec>& ratios)
override {
375 detail::Json request;
376 request[
"Q"] = detail::to_json_matrix(Q);
377 request[
"symbols"] = detail::to_json_array(symbols);
378 request[
"weights"] = detail::to_json_rows(weights, Q.size());
379 request[
"normalize"] =
true;
380 if (!names.empty()) request[
"names"] = detail::to_json_vector(names);
381 if (!ratios.empty()) {
382 detail::Json arr = detail::Json::array();
383 for (std::size_t i = 0; i < ratios.size(); ++i) {
384 detail::Json o = detail::Json::object();
385 o[
"name"] = ratios[i].name;
386 o[
"num"] = ratios[i].num;
387 o[
"den"] = ratios[i].den;
390 request[
"ratios"] = arr;
392 const detail::Json response = post(
"/api/v1/ctmc/measures", request);
395 out.
pi = detail::to_string_list(response,
"pi");
396 out.
num = detail::to_string_list(response,
"num");
397 out.
den = detail::opt_string(response,
"den",
"1");
398 out.
nConnComp = response.contains(
"nConnComp") ? response[
"nConnComp"].get<
int>() : 1;
399 out.
connComp = detail::to_int_list(response,
"connComp");
400 out.
measures = detail::to_measure_list(response,
"measures");
401 out.
ratios = detail::to_measure_list(response,
"ratios");
406 const std::vector<std::string>& s0,
407 const std::vector<std::string>& alpha,
const std::string& atom,
408 const std::vector<std::string>& symbols,
const std::string& svar,
409 const std::vector<std::string>& want,
int nmax)
override {
410 detail::Json request;
411 request[
"S"] = detail::to_json_matrix(S);
412 request[
"s0"] = detail::to_json_vector(s0);
413 request[
"alpha"] = detail::to_json_vector(alpha);
414 request[
"atom"] = atom.empty() ? std::string(
"0") : atom;
415 request[
"symbols"] = detail::to_json_array(symbols);
416 request[
"svar"] = svar.empty() ? std::string(
"s") : svar;
417 request[
"want"] = detail::to_json_vector(want);
418 request[
"nmax"] = nmax < 1 ? 1 : nmax;
419 const detail::Json response = post(
"/api/v1/ctmc/passage", request);
422 out.
lst = detail::opt_string(response,
"lst",
"");
423 out.
lstNum = detail::opt_string(response,
"lst_num",
"");
424 out.
lstDen = detail::opt_string(response,
"lst_den",
"");
425 out.
lstAll = detail::to_string_list(response,
"lstall");
426 out.
moments = detail::to_string_list(response,
"moments");
427 if (response.contains(
"momall") && !response[
"momall"].is_null())
428 for (
const detail::Json& row : response[
"momall"]) {
429 std::vector<std::string> r;
430 for (
const detail::Json& el : row) r.push_back(el.get<std::string>());
433 out.
unreachable = detail::to_int_list(response,
"unreachable");
438 const std::vector<std::string>& symbols,
439 const std::string& theta,
440 const std::vector<std::string>& reward)
override {
441 detail::Json request;
442 request[
"Q"] = detail::to_json_matrix(Q);
443 request[
"symbols"] = detail::to_json_array(symbols);
444 request[
"theta"] = theta;
445 if (!reward.empty()) request[
"reward"] = detail::to_json_array(reward);
446 const detail::Json response = post(
"/api/v1/ctmc/sensitivity", request);
449 s.
pi = detail::to_string_list(response,
"pi");
450 s.
dpi = detail::to_string_list(response,
"dpi");
451 s.
Er = detail::opt_string(response,
"Er",
"");
452 s.
S = detail::opt_string(response,
"S",
"");
453 s.
SS = detail::opt_string(response,
"SS",
"");
458 std::vector<std::string>
simplify(
const std::vector<std::string>& exprs,
459 const std::string& form)
override {
460 detail::Json request;
461 request[
"exprs"] = detail::to_json_array(exprs);
462 request[
"form"] = form.empty() ? std::string(
"cancel") : form;
463 return detail::to_string_list(post(
"/api/v1/simplify", request),
"results");
466 std::vector<std::string>
diff(
const std::vector<std::string>& exprs,
467 const std::string& variable,
int order)
override {
468 detail::Json request;
469 request[
"exprs"] = detail::to_json_array(exprs);
470 request[
"var"] = variable;
471 request[
"order"] = order;
472 return detail::to_string_list(post(
"/api/v1/diff", request),
"results");
475 std::vector<double>
eval(
const std::vector<std::string>& exprs,
476 const std::map<std::string, double>& assignment)
override {
477 detail::Json request;
478 request[
"exprs"] = detail::to_json_array(exprs);
479 detail::Json values = detail::Json::object();
480 for (std::map<std::string, double>::const_iterator it = assignment.begin();
481 it != assignment.end(); ++it) {
484 values[it->first] = detail::decimal_string(it->second);
486 request[
"values"] = values;
487 const detail::Json response = post(
"/api/v1/eval", request);
489 std::vector<double> out;
490 if (!response.contains(
"values") || response[
"values"].is_null())
return out;
491 for (
const detail::Json& el : response[
"values"])
492 out.push_back(el.is_null() ? std::nan(
"") : el.get<
double>());
497 const std::vector<std::string>& want)
override {
498 detail::Json request;
499 request[
"rhs"] = detail::to_json_array(rhs);
500 request[
"vars"] = detail::to_json_array(vars);
501 request[
"want"] = detail::to_json_array(want);
502 const detail::Json response = post(
"/api/v1/fluid/odes", request);
505 if (response.contains(
"jacobian") && !response[
"jacobian"].is_null()) {
507 for (
const detail::Json& row : response[
"jacobian"]) {
508 std::vector<std::string> r;
509 for (
const detail::Json& el : row) r.push_back(el.get<std::string>());
513 if (response.contains(
"latex") && !response[
"latex"].is_null()) {
515 odes.
latex = detail::to_string_list(response,
"latex");
517 if (response.contains(
"equilibria") && !response[
"equilibria"].is_null()) {
519 for (
const detail::Json& sol : response[
"equilibria"]) {
520 std::map<std::string, std::string> m;
521 for (detail::Json::const_iterator it = sol.begin(); it != sol.end(); ++it)
522 m[it.key()] = it.value().get<std::string>();
530 detail::Json post(
const std::string& path, detail::Json request)
const {
531 const int millis = timeoutSeconds_ > 0 ? timeoutSeconds_ * 1000 : 0;
532 if (timeoutSeconds_ > 0) request[
"timeout_s"] = timeoutSeconds_;
533 const detail::Json response = parse(
535 checkStatus(path, response);
539 detail::Json get(
const std::string& path,
int millis)
const {
540 return parse(
http::get(baseUrl_ + path, millis));
543 static detail::Json parse(
const http::Response& response) {
544 if (response.body.empty())
546 std::to_string(response.status) +
" with no body");
547 detail::Json parsed = detail::Json::parse(response.body,
nullptr,
false);
548 if (parsed.is_discarded() || !parsed.is_object())
549 throw SymEngineError(
"line-sage-rest returned a non-JSON body: " + response.body);
553 static void checkStatus(
const std::string& path,
const detail::Json& response) {
554 const std::string status = detail::opt_string(response,
"status",
"");
555 if (status ==
"ok")
return;
557 detail::opt_string(response,
"code",
"error") +
558 "]: " + detail::opt_string(response,
"message",
"unspecified error"));
561 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
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.
FluidODEs fluidODEs(const std::vector< std::string > &rhs, const std::vector< std::string > &vars, const std::vector< std::string > &want) override
Jacobian, LaTeX form and equilibria of a fluid vector field.
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
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
Rewrites expressions into a normal form.
SymEngineError(const std::string &what)
A computer algebra backend.
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.
Symbolic stationary distribution of a CTMC.
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.
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.