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]);
150 std::string u = baseUrl;
151 const std::size_t b = u.find_first_not_of(
" \t\r\n");
152 const std::size_t e = u.find_last_not_of(
" \t\r\n");
153 u = b == std::string::npos ? std::string() : u.substr(b, e - b + 1);
154 while (!u.empty() && u[u.size() - 1] ==
'/') u.erase(u.size() - 1);
155 if (u.empty())
throw InputError(
"SageRestEngine: baseUrl must not be empty");
166 timeoutSeconds_ = seconds;
176 std::string
name()
const override {
return "sage"; }
180 const detail::Json health = get(
"/api/v1/health", 5000);
181 return detail::opt_string(health,
"status",
"") ==
"ok";
182 }
catch (
const Error&) {
205 std::lock_guard<std::mutex> guard(detail::usable_mutex());
206 std::map<std::string, bool>&
cache = detail::usable_cache();
207 const std::map<std::string, bool>::const_iterator it =
cache.find(baseUrl_);
208 if (it !=
cache.end())
return it->second;
212 detail::Json request;
213 request[
"exprs"] = detail::Json::array({std::string(detail::canary_expr())});
214 detail::Json values = detail::Json::object();
215 values[
"x"] = std::string(detail::canary_arg());
216 request[
"values"] = values;
217 request[
"timeout_s"] = 30;
218 const detail::Json response =
219 parse(
http::post_json(baseUrl_ +
"/api/v1/eval", request.dump(), 60000));
220 checkStatus(
"/api/v1/eval", response);
221 if (response.contains(
"values") && response[
"values"].is_array() &&
222 response[
"values"].size() == 1 && !response[
"values"][0].is_null()) {
223 const double v = response[
"values"][0].get<
double>();
224 ok = std::fabs(v - detail::CANARY_VALUE) < 1e-9;
226 }
catch (
const Error&) {
233 std::cerr <<
"[LINE] Ignoring symbolic backend at " << baseUrl_
234 <<
": it did not return the usability canary. On a CPU without "
235 <<
"BMI2/ADX the image's FLINT raises SIGILL mid-request." << std::endl;
238 std::lock_guard<std::mutex> guard(detail::usable_mutex());
239 detail::usable_cache()[baseUrl_] = ok;
250 detail::Json
info()
const {
return get(
"/api/v1/info", 5000); }
253 const std::vector<std::string>& symbols)
override {
254 detail::Json request;
255 request[
"Q"] = detail::to_json_matrix(Q);
256 request[
"symbols"] = detail::to_json_array(symbols);
257 request[
"normalize"] =
true;
258 const detail::Json response = post(
"/api/v1/ctmc/solve", request);
261 sol.
pi = detail::to_string_list(response,
"pi");
262 sol.
num = detail::to_string_list(response,
"num");
263 sol.
den = detail::opt_string(response,
"den",
"1");
264 sol.
nConnComp = response.contains(
"nConnComp") ? response[
"nConnComp"].get<
int>() : 1;
265 if (response.contains(
"connComp") && !response[
"connComp"].is_null())
266 for (
const detail::Json& el : response[
"connComp"]) sol.
connComp.push_back(el.get<
int>());
271 const std::vector<std::string>& symbols,
272 const std::string& theta,
273 const std::vector<std::string>& reward)
override {
274 detail::Json request;
275 request[
"Q"] = detail::to_json_matrix(Q);
276 request[
"symbols"] = detail::to_json_array(symbols);
277 request[
"theta"] = theta;
278 if (!reward.empty()) request[
"reward"] = detail::to_json_array(reward);
279 const detail::Json response = post(
"/api/v1/ctmc/sensitivity", request);
282 s.
pi = detail::to_string_list(response,
"pi");
283 s.
dpi = detail::to_string_list(response,
"dpi");
284 s.
Er = detail::opt_string(response,
"Er",
"");
285 s.
S = detail::opt_string(response,
"S",
"");
286 s.
SS = detail::opt_string(response,
"SS",
"");
291 std::vector<std::string>
simplify(
const std::vector<std::string>& exprs,
292 const std::string& form)
override {
293 detail::Json request;
294 request[
"exprs"] = detail::to_json_array(exprs);
295 request[
"form"] = form.empty() ? std::string(
"cancel") : form;
296 return detail::to_string_list(post(
"/api/v1/simplify", request),
"results");
299 std::vector<std::string>
diff(
const std::vector<std::string>& exprs,
300 const std::string& variable,
int order)
override {
301 detail::Json request;
302 request[
"exprs"] = detail::to_json_array(exprs);
303 request[
"var"] = variable;
304 request[
"order"] = order;
305 return detail::to_string_list(post(
"/api/v1/diff", request),
"results");
308 std::vector<double>
eval(
const std::vector<std::string>& exprs,
309 const std::map<std::string, double>& assignment)
override {
310 detail::Json request;
311 request[
"exprs"] = detail::to_json_array(exprs);
312 detail::Json values = detail::Json::object();
313 for (std::map<std::string, double>::const_iterator it = assignment.begin();
314 it != assignment.end(); ++it) {
317 values[it->first] = detail::decimal_string(it->second);
319 request[
"values"] = values;
320 const detail::Json response = post(
"/api/v1/eval", request);
322 std::vector<double> out;
323 if (!response.contains(
"values") || response[
"values"].is_null())
return out;
324 for (
const detail::Json& el : response[
"values"])
325 out.push_back(el.is_null() ? std::nan(
"") : el.get<
double>());
330 const std::vector<std::string>& want)
override {
331 detail::Json request;
332 request[
"rhs"] = detail::to_json_array(rhs);
333 request[
"vars"] = detail::to_json_array(vars);
334 request[
"want"] = detail::to_json_array(want);
335 const detail::Json response = post(
"/api/v1/fluid/odes", request);
338 if (response.contains(
"jacobian") && !response[
"jacobian"].is_null()) {
340 for (
const detail::Json& row : response[
"jacobian"]) {
341 std::vector<std::string> r;
342 for (
const detail::Json& el : row) r.push_back(el.get<std::string>());
346 if (response.contains(
"latex") && !response[
"latex"].is_null()) {
348 odes.
latex = detail::to_string_list(response,
"latex");
350 if (response.contains(
"equilibria") && !response[
"equilibria"].is_null()) {
352 for (
const detail::Json& sol : response[
"equilibria"]) {
353 std::map<std::string, std::string> m;
354 for (detail::Json::const_iterator it = sol.begin(); it != sol.end(); ++it)
355 m[it.key()] = it.value().get<std::string>();
363 detail::Json post(
const std::string& path, detail::Json request)
const {
364 const int millis = timeoutSeconds_ > 0 ? timeoutSeconds_ * 1000 : 0;
365 if (timeoutSeconds_ > 0) request[
"timeout_s"] = timeoutSeconds_;
366 const detail::Json response = parse(
368 checkStatus(path, response);
372 detail::Json get(
const std::string& path,
int millis)
const {
373 return parse(
http::get(baseUrl_ + path, millis));
376 static detail::Json parse(
const http::Response& response) {
377 if (response.body.empty())
379 std::to_string(response.status) +
" with no body");
380 detail::Json parsed = detail::Json::parse(response.body,
nullptr,
false);
381 if (parsed.is_discarded() || !parsed.is_object())
382 throw SymEngineError(
"line-sage-rest returned a non-JSON body: " + response.body);
386 static void checkStatus(
const std::string& path,
const detail::Json& response) {
387 const std::string status = detail::opt_string(response,
"status",
"");
388 if (status ==
"ok")
return;
390 detail::opt_string(response,
"code",
"error") +
391 "]: " + detail::opt_string(response,
"message",
"unspecified error"));
394 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...
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.
std::vector< std::string > simplify(const std::vector< std::string > &exprs, const std::string &form) 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.
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
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.