LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
fluid_jacobian.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_SOLVERS_FLUID_FLUID_JACOBIAN_H
6#define LINE_SOLVERS_FLUID_FLUID_JACOBIAN_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `@@SolverFLD/getJacobian`, all four of its outputs.
12 *
13 * The reference builds the drift locally with `getSymbolicDrift` and then posts
14 * it to the line-sage-rest backend, asking for the Jacobian and, when a fourth
15 * output is requested, the solutions of f(x) = 0. This header does the same:
16 * `fluid_symbolic_drift` supplies rhs and vars, `sym::sym_resolve` supplies the
17 * backend, and `SymEngine::fluidODEs` answers.
18 *
19 * WHY THE BACKEND IS NOT OPTIONAL FOR THE EQUILIBRIA. Differentiating the drift
20 * is structural -- `fluid_symbolic_jacobian` does it exactly by the chain rule
21 * over the typed factors of `FluidSymSystem`, with no algebra system in sight --
22 * but SOLVING f(x) = 0 in closed form is not. There is no local answer to fall
23 * back to, so a request for equilibria without a backend is refused by name,
24 * with the same guidance the reference's `SAGE.require()` prints. Returning an
25 * empty list instead would read as "this system has no equilibria", which is a
26 * different and false statement.
27 *
28 * DIVERGENCE, DELIBERATE: when equilibria are NOT asked for and no backend
29 * resolves, this returns the locally differentiated Jacobian rather than
30 * erroring as the reference does. The entries are the same derivative, obtained
31 * without a 3 GB container; refusing to answer a question we can answer exactly
32 * would be a worse port than answering it. `FluidJacobian::engine` names which
33 * path produced them, so a caller comparing text against MATLAB knows whether it
34 * is comparing against SAGE's normal form or this port's.
35 *
36 * The smoothness gate is upstream, in `fluid_symbolic_drift`: a drift that
37 * scales rates by min(n_i, S_i) has no Jacobian at n_i = S_i and is refused
38 * before any backend is contacted.
39 */
40
41#include <map>
42#include <memory>
43#include <string>
44#include <vector>
45
50#include "line/util/error.h"
51
52namespace line {
53namespace fluid {
54
55/** Backend selection, mirroring `options.config.symbolic` and its timeout. */
57 /** `auto` to search, a URL, an image name, or `none` to stay local. */
58 std::string backend = "auto";
59 /** `options.config.symbolic_timeout`, seconds; the reference defaults to 300. */
60 int timeout_s = 300;
61 /** The reference's `nargout >= 4`: ask the backend to solve f(x) = 0. */
62 bool equilibria = false;
63};
64
65/** The four outputs of `@@SolverFLD/getJacobian`. */
67 std::vector<std::string> vars; ///< state variable names
68 std::vector<std::string> rhs; ///< the drift, one expression per variable
69 std::vector<std::vector<std::string> > J; ///< J[i][j] = d f_i / d x_j
70 /**
71 * Solutions of f(x) = 0, each a variable -> expression map.
72 *
73 * EMPTY IS NOT AN ASSERTION THAT NONE EXIST. The backend returns what it
74 * solves in closed form, and a system beyond it answers with nothing; that
75 * is a limit of the solve. `has_equilibria` separates "asked and answered
76 * with none" from "never asked".
77 */
78 std::vector<std::map<std::string, std::string> > equilibria;
79 bool has_equilibria = false; ///< the backend answered the equilibria request
80 std::string engine; ///< `sage` or `local`, whichever produced J
81};
82
83namespace detail {
84
85/** The reference's `SAGE.require()` message, verbatim in substance. */
86inline std::string fluid_sym_backend_missing() {
87 return std::string(
88 "fluid_jacobian: solving f(x) = 0 in closed form needs a symbolic backend, and none is "
89 "available. Start one with\n docker run -d -p 8080:8080 ") +
91 "\npoint the " + sym::SYM_URL_ENV +
92 " environment variable at a running service, or set the backend to its URL. The Jacobian "
93 "alone needs no backend: ask for it without the equilibria";
94}
95
96} // namespace detail
97
98/**
99 * Jacobian, drift and equilibria of the mean-field vector field.
100 *
101 * @param sys the symbolic system, from `fluid_symodes`
102 * @param opt backend selection and whether equilibria are wanted
103 * @return the four outputs; `engine` names what produced the Jacobian
104 */
107 // The drift comes first and locally, exactly as the reference's getJacobian
108 // calls getSymbolicDrift before touching the backend: it is what carries the
109 // smoothness gate, so a non-smooth method is refused without a round trip.
111
112 FluidJacobian out;
113 out.vars = d.vars;
114 out.rhs = d.rhs;
115
116 std::shared_ptr<sym::SymEngine> engine = sym::sym_resolve(opt.backend);
117 if (!engine) {
118 if (opt.equilibria) throw sym::SymEngineError(detail::fluid_sym_backend_missing());
120 out.J = local.J;
121 out.engine = "local";
122 return out;
123 }
124
125 // The timeout travels in the request body, so it must be set on the engine
126 // before the call rather than passed to it; only the REST engine has one.
127 if (opt.timeout_s > 0) {
128 sym::SageRestEngine* rest = dynamic_cast<sym::SageRestEngine*>(engine.get());
129 if (rest != nullptr) rest->setTimeoutSeconds(opt.timeout_s);
130 }
131
132 std::vector<std::string> want;
133 want.push_back("jacobian");
134 if (opt.equilibria) want.push_back("equilibria");
135 const sym::FluidODEs odes = engine->fluidODEs(d.rhs, d.vars, want);
136
137 if (!odes.hasJacobian)
138 throw sym::SymEngineError("fluid_jacobian: backend '" + engine->name() +
139 "' answered without the jacobian it was asked for");
140 if (odes.jacobian.size() != sys.nstates)
141 throw sym::SymEngineError("fluid_jacobian: backend returned " +
142 detail::sym_state_index(odes.jacobian.size()) + " rows for a " +
143 detail::sym_state_index(sys.nstates) + " state system");
144 for (std::size_t i = 0; i < odes.jacobian.size(); ++i)
145 if (odes.jacobian[i].size() != sys.nstates)
146 throw sym::SymEngineError("fluid_jacobian: backend row " +
147 detail::sym_state_index(i + 1) + " has " +
148 detail::sym_state_index(odes.jacobian[i].size()) +
149 " columns, expected " + detail::sym_state_index(sys.nstates));
150 out.J = odes.jacobian;
151 out.engine = engine->name();
152 if (opt.equilibria) {
153 out.equilibria = odes.equilibria;
154 out.has_equilibria = odes.hasEquilibria;
155 }
156 return out;
157}
158
159} // namespace fluid
160} // namespace line
161
162#endif // LINE_SOLVERS_FLUID_FLUID_JACOBIAN_H
Client of the line-sage-rest service.
SageRestEngine & setTimeoutSeconds(int seconds)
Sets the per-request timeout.
The symbolic backend is unreachable, or rejected the request.
Definition sym_engine.h:41
The exception types the port throws.
A symbolic description of the fluid ODE system: a port of solver_fluid_symodes.m, which is what @@Sol...
FluidSymbolicDrift fluid_symbolic_drift(const FluidSymSystem &sys)
Port of @@SolverFLD/getSymbolicDrift: the right-hand side of the mean-field ODE system as expression ...
FluidSymbolicJacobian fluid_symbolic_jacobian(const FluidSymSystem &sys)
Port of @@SolverFLD/getJacobian: d f_i / d x_j of the mean-field drift, as expression strings.
FluidJacobian fluid_jacobian(const FluidSymSystem &sys, const FluidSymbolicOptions &opt=FluidSymbolicOptions())
Jacobian, drift and equilibria of the mean-field vector field.
const char *const SYM_URL_ENV
Environment variable naming a service to use.
Definition sym_engines.h:74
std::shared_ptr< SymEngine > sym_resolve(const std::string &requested="auto")
Resolves an engine.
const char *const SYM_DOCKER_IMAGE
Image serving the symbolic REST API.
Definition sym_engines.h:72
SymEngine backed by the line-sage-rest service.
The four outputs of @@SolverFLD/getJacobian.
std::string engine
sage or local, whichever produced J
std::vector< std::string > rhs
the drift, one expression per variable
std::vector< std::vector< std::string > > J
J[i][j] = d f_i / d x_j.
bool has_equilibria
the backend answered the equilibria request
std::vector< std::map< std::string, std::string > > equilibria
Solutions of f(x) = 0, each a variable -> expression map.
std::vector< std::string > vars
state variable names
The symbolic system, in whichever of the two forms the method implies.
The drift as expression strings, one per state variable, plus their names.
std::vector< std::string > vars
std::vector< std::string > rhs
What @@SolverFLD/getJacobian returns: d f_i / d x_j as expression strings.
std::vector< std::vector< std::string > > J
J[i][j] = d f_i / d x_j.
Backend selection, mirroring options.config.symbolic and its timeout.
bool equilibria
The reference's nargout >= 4: ask the backend to solve f(x) = 0.
int timeout_s
options.config.symbolic_timeout, seconds; the reference defaults to 300.
std::string backend
auto to search, a URL, an image name, or none to stay local.
Symbolic analysis of a fluid vector field.
Definition sym_engine.h:72
std::vector< std::map< std::string, std::string > > equilibria
variable -> expression
Definition sym_engine.h:75
std::vector< std::vector< std::string > > jacobian
d f_i / d x_j
Definition sym_engine.h:73
Computer algebra operations LINE needs, as seen by this port.
Resolves the symbolic backend to use, and owns the container that serves it.