26 #include "function.hpp"
27 #include "casadi_meta.hpp"
28 #include "function_internal.hpp"
29 #include "sx_function.hpp"
31 #include "serializer.hpp"
32 #include "mx_function.hpp"
33 #include "filesystem_impl.hpp"
34 #include <casadi/core/resource_casadi_viz.hpp>
45 std::string graph_string(
const std::string& value) {
48 for (
unsigned char c : value) {
49 if (c ==
'"' || c ==
'\\') {
51 }
else if (c < 32 || c ==
'<' || c ==
'>' || c ==
'&') {
52 s <<
"\\u" << std::hex << std::setfill(
'0') << std::setw(4)
53 <<
static_cast<unsigned int>(c) << std::dec;
63 casadi_int id, op, io = -1, offset = 0;
65 std::string kind, display, symbol;
66 std::vector<std::string> input_names, output_names, constants;
67 std::vector<Sparsity> inputs, outputs;
69 struct GraphEdge { casadi_int from, output, to, input; };
71 std::vector<GraphNode> nodes;
72 std::vector<GraphEdge> edges;
76 void graph_model(
const Function&
function, GraphModel& model) {
77 auto sx =
dynamic_cast<const SXFunction*
>(
function.get());
78 auto mx =
dynamic_cast<const MXFunction*
>(
function.get());
79 const auto sx_inputs = sx ?
function.sx_in() : std::vector<SX>{};
80 const auto mx_inputs = mx ?
function.mx_in() : std::vector<MX>{};
83 std::map<casadi_int, std::pair<casadi_int, casadi_int>> producer;
84 for (casadi_int k = 0; k <
function.n_instructions(); ++k) {
85 casadi_int op =
function.instruction_id(k);
86 auto arg =
function.instruction_input(k), res =
function.instruction_output(k);
89 node.id = k; node.op = op;
90 std::string kind =
"operation";
93 casadi_int io = (input ? arg : res).at(0);
94 casadi_int offset = sx ? (input ? arg : res).at(1)
95 : mx->algorithm_.at(k).data->offset();
96 node.io = io; node.offset = offset;
98 node.symbol = sx ?
str(sx_inputs.at(io).nonzeros().at(offset))
99 :
str(mx_inputs.at(io));
101 label = (input ?
function.name_in(io) :
function.name_out(io));
102 if ((sx && (input ?
function.nnz_in(io) :
function.nnz_out(io)) != 1) || offset != 0) {
103 label +=
"[" +
str(offset) +
"]";
105 kind = input ?
"input" :
"output";
108 label = sx ?
str(sx->free_vars_.at(sx->algorithm_.at(k).i1))
109 : mx->algorithm_.at(k).data.name();
114 label = (sx ? sx->call_.el.at(sx->algorithm_.at(k).i1).f.name()
115 : mx->algorithm_.at(k).data.which_function().name());
119 std::vector<std::string> input_names, output_names, constants;
120 for (casadi_int i = 0; i < arg.size(); ++i) input_names.push_back(
"arg" +
str(i));
121 for (casadi_int i = 0; i < res.size(); ++i) output_names.push_back(
"out" +
str(i));
123 const Function& f = sx ? sx->call_.el.at(sx->algorithm_.at(k).i1).f
124 : mx->algorithm_.at(k).data.which_function();
125 Dict info = f.info();
126 auto source = info.find(
"model_path");
127 if (source != info.end()) {
128 std::string
path = source->second.to_string();
129 if (!
path.empty()) label +=
" [" +
path.substr(
path.find_last_of(
"/\\") + 1) +
"]";
131 input_names.clear(); output_names.clear();
132 for (casadi_int side = 0; side < 2; ++side) {
133 auto& names = side == 0 ? input_names : output_names;
134 for (casadi_int i = 0; i < (side == 0 ? f.n_in() : f.n_out()); ++i) {
135 std::string name = side == 0 ? f.name_in(i) : f.name_out(i);
136 casadi_int count = side == 0 ? f.nnz_in(i) : f.nnz_out(i);
138 for (casadi_int j = 0; j < count; ++j) {
139 names.push_back(name + (count == 1 ?
"" :
"[" +
str(j) +
"]"));
142 names.push_back(name);
147 std::string display = label;
148 if (kind ==
"operation") {
150 case OP_ADD: display =
"+";
break;
151 case OP_SUB: display =
"-";
break;
152 case OP_MUL: display =
"*";
break;
153 case OP_DIV: display =
"/";
break;
154 case OP_NEG: display =
"-";
break;
155 case OP_SQ: display =
"(.)^2";
break;
157 case OP_TWICE: display =
"2*(.)";
break;
158 case OP_INV: display =
"1/(.)";
break;
162 std::vector<double> values = sx ? std::vector<double> {sx->algorithm_.at(k).d}
163 :
static_cast<DM>(mx->algorithm_.at(k).data).nonzeros();
164 for (
double value : values) {
165 std::ostringstream number;
166 number << std::setprecision(17) << value;
167 constants.push_back(number.str());
169 display = constants.size() == 1 ? constants.front() :
"constant";
171 node.kind = kind; node.display = display;
173 node.ordered = arg.size() > 1 && !operation_checker<CommChecker>(op);
174 node.input_names = input_names; node.output_names = output_names;
175 node.constants = constants;
176 for (casadi_int i = 0; i < arg.size(); ++i) {
177 node.inputs.push_back(sx ?
Sparsity::scalar() : mx->algorithm_.at(k).data->dep(i).sparsity());
178 if (arg[i] < 0)
continue;
179 auto p = producer.find(arg[i]);
180 casadi_assert(p != producer.end(),
"Missing graph producer for instruction " +
str(k));
181 model.edges.push_back({p->second.first, p->second.second, k, i});
183 for (casadi_int i = 0; i < res.size(); ++i) {
184 node.outputs.push_back(sx ?
Sparsity::scalar() : mx->algorithm_.at(k).data->sparsity(i));
185 if (res[i] >= 0) producer[res[i]] = {k, i};
187 model.nodes.push_back(std::move(node));
192 std::string dot_quote(
const std::string& value) {
193 std::string out =
"\"";
194 for (
char c : value) {
197 }
else if (c ==
'\r') {
200 if (c ==
'\\' || c ==
'"') out +=
'\\';
206 std::string dot_html(
const std::string& value) {
208 for (
char c : value) {
210 case '&': out +=
"&";
break;
211 case '<': out +=
"<";
break;
212 case '>': out +=
">";
break;
213 case '"': out +=
""";
break;
219 std::string graph_size(
const Sparsity& sp) {
220 return str(sp.size1()) +
"-by-" +
str(sp.size2());
222 void dot_matrix(std::ostream& s,
const std::string&
id,
const Sparsity& sp,
223 const std::string& title,
const std::vector<std::string>* constants =
nullptr,
224 const std::string& color =
"#666666",
bool show_sizes =
true) {
225 s <<
id <<
" [shape=plain, fontcolor=\"#666666\", fontsize=10, label=<"
226 <<
"<TABLE BORDER=\"0\" CELLBORDER=\"0\" CELLSPACING=\"0\" CELLPADDING=\"3\">";
227 if (show_sizes || !title.empty()) s <<
"<TR><TD COLSPAN=\""
228 << std::max<casadi_int>(1, std::min<casadi_int>(8, sp.size2()))
229 <<
"\"><FONT COLOR=\"" << color <<
"\">"
230 << dot_html(show_sizes ? (title.empty() ? graph_size(sp) : title +
" : " + graph_size(sp))
232 <<
"</FONT></TD></TR>";
233 std::map<std::pair<casadi_int, casadi_int>, casadi_int> lookup;
234 for (casadi_int c = 0; c < std::min<casadi_int>(8, sp.size2()); ++c) {
235 for (casadi_int k = sp.colind()[c]; k < sp.colind()[c+1]; ++k) {
236 if (sp.row()[k] < 8) lookup[{sp.row()[k], c}] = k;
239 for (casadi_int r = 0; r < std::min<casadi_int>(8, sp.size1()); ++r) {
241 for (casadi_int c = 0; c < std::min<casadi_int>(8, sp.size2()); ++c) {
242 auto nz = lookup.find({r, c});
243 s <<
"<TD BGCOLOR=\"white\"";
244 if (nz != lookup.end()) s <<
" PORT=\"nz" << nz->second <<
"\"";
246 if (constants) s << (nz == lookup.end() ?
"." : dot_html(constants->at(nz->second)));
248 s <<
"<TABLE BORDER=\"0\" CELLBORDER=\"0\" CELLSPACING=\"0\" CELLPADDING=\"0\">"
249 <<
"<TR><TD FIXEDSIZE=\"TRUE\" WIDTH=\"9\" HEIGHT=\"9\" BGCOLOR=\""
250 << (nz == lookup.end() ?
"white" :
"#111111") <<
"\"></TD></TR></TABLE>";
253 if (!sp.size2()) s <<
"<TD>empty</TD>";
256 if (!sp.size1()) s <<
"<TR><TD>empty</TD></TR>";
257 if (sp.size1() > 8 || sp.size2() > 8) s <<
"<TR><TD>...</TD></TR>";
258 s <<
"</TABLE>>];\n";
260 bool dot_record(
const GraphNode& n) {
261 return (n.ordered && !n.binary) || n.kind ==
"call";
263 bool dot_has_table(
const GraphNode& n,
const Sparsity& sp,
bool show_contents) {
264 if (!show_contents || sp.numel() == 1)
return false;
265 if (n.kind ==
"call" || n.inputs.empty() || n.op ==
OP_GETNONZEROS
267 for (
const auto& input : n.inputs)
if (input != sp)
return true;
270 void dot_ports(std::ostream& s,
const GraphNode& n,
271 const std::vector<std::string>& names,
const std::string& prefix,
bool show_sizes) {
272 if (names.empty())
return;
273 s <<
"<TR><TD><TABLE BORDER=\"0\" CELLBORDER=\"1\" COLOR=\"#d26666\"><TR>";
274 for (casadi_int i = 0; i < names.size(); ++i) {
275 s <<
"<TD PORT=\"" << prefix << i <<
"\"><FONT POINT-SIZE=\"10\">"
276 << dot_html(names[i]);
277 const auto& sp = prefix ==
"in" ? n.inputs.at(i) : n.outputs.at(i);
278 if (show_sizes && sp.numel() != 1) s <<
" " << graph_size(sp);
281 s <<
"</TR></TABLE></TD></TR>";
283 void graph_dot(std::ostream& s,
const Function& f,
const GraphModel& model,
284 const std::string& direction,
const std::string& view,
bool show_contents,
bool show_sizes) {
285 bool function_view = view ==
"function";
286 std::map<std::pair<casadi_int, casadi_int>, std::string> sources;
287 std::map<casadi_int, std::string> targets;
288 s <<
"digraph G {\ngraph [rankdir=" << (function_view ?
"TB" : direction)
289 <<
", bgcolor=\"white\", pad=0.4, nodesep=0.55, ranksep=0.65];\n"
290 <<
"node [shape=ellipse, style=filled, color=\"#b00000\", fillcolor=\"#b00000\", "
291 <<
"fontcolor=white, fontname=Helvetica, fontsize=14, margin=\"0.15,0.08\", "
292 <<
"width=0.5, height=0.5];\nedge [color=\"#34658b\", penwidth=1.5, arrowsize=0.65];\n";
293 for (
const auto& n : model.nodes) {
294 if (n.kind ==
"output" || (function_view && n.kind ==
"input"))
continue;
295 const std::string
id =
"n" +
str(n.id);
296 std::string display = !function_view && n.kind ==
"input" ? n.symbol : n.display;
297 std::string color = n.kind ==
"input" || n.kind ==
"symbol" ?
"#34658b"
298 : n.kind ==
"constant" ?
"#38754d" :
"#b00000";
299 s <<
id <<
" [color=" << dot_quote(color) <<
", fillcolor=" << dot_quote(color);
301 s <<
", shape=plain, label=<<TABLE BORDER=\"0\" CELLBORDER=\"0\" "
302 <<
"CELLSPACING=\"0\" CELLPADDING=\"6\" BGCOLOR=\"#b00000\">";
303 dot_ports(s, n, n.input_names,
"in", show_sizes);
304 s <<
"<TR><TD>" << dot_html(display) <<
"</TD></TR>";
305 if (n.kind ==
"call") dot_ports(s, n, n.output_names,
"out", show_sizes);
308 const auto& ports = n.outputs.empty() ? n.inputs : n.outputs;
310 for (
const auto& sp : n.outputs) table = table || dot_has_table(n, sp, show_contents);
311 if (show_sizes && !table && !ports.empty() && ports.front().numel() != 1) {
312 display +=
"\n" + graph_size(ports.front());
314 s <<
", label=" << dot_quote(display);
317 for (casadi_int i = 0; i < n.outputs.size(); ++i) {
318 const auto& sp = n.outputs[i];
319 if (!dot_has_table(n, sp, show_contents))
continue;
320 std::string value =
"v" +
str(n.id) +
"_" +
str(i);
321 dot_matrix(s, value, sp, n.kind ==
"call" ? n.output_names.at(i) :
"",
322 n.kind ==
"constant" ? &n.constants :
nullptr,
"#666666", show_sizes);
323 s <<
id << (n.kind ==
"call" ?
":out" +
str(i) :
"") <<
" -> " << value <<
";\n";
324 sources[{n.id, i}] = value;
327 for (
bool input : {
true,
false}) {
328 std::vector<std::string> row;
329 for (casadi_int i = 0; i < (input ? f.n_in() : f.n_out()); ++i) {
330 const auto& sp = input ? f.sparsity_in(i) : f.sparsity_out(i);
331 if (!function_view && (input || !show_contents || !f.is_a(
"SXFunction")
332 || sp.numel() == 1))
continue;
333 std::string kind = input ?
"input" :
"output",
id =
"b" + kind +
"_" +
str(i);
334 std::string name = function_view ? (input ? f.name_in(i) : f.name_out(i)) :
"";
335 if (show_contents && sp.numel() != 1) dot_matrix(s,
id, sp, name,
nullptr,
336 function_view ? (input ?
"#34658b" :
"#b00000") :
"#666666", show_sizes);
338 s <<
id <<
" [label=" << dot_quote(name
339 + (show_sizes && sp.numel() != 1 ?
"\n" + graph_size(sp) :
"")) <<
", color=\""
340 << (input ?
"#34658b" :
"#b00000") <<
"\", fillcolor=\""
341 << (input ?
"#34658b" :
"#b00000") <<
"\"];\n";
342 for (
const auto& n : model.nodes) {
343 if (n.kind != kind || n.io != i)
continue;
344 std::string endpoint = id;
345 if (show_contents && f.is_a(
"SXFunction") && sp.numel() != 1 && n.offset < sp.nnz()) {
346 const auto col = std::upper_bound(sp.colind(), sp.colind()+sp.size2()+1, n.offset)
348 if (col < 8 && sp.row()[n.offset] < 8) endpoint +=
":nz" +
str(n.offset);
350 if (input) sources[{n.id, 0}] = endpoint;
352 targets[n.id] = endpoint;
356 if (function_view && !row.empty()) {
357 s <<
"{rank=" << (input ?
"source" :
"sink") <<
";";
358 for (
const auto&
id : row) s <<
id <<
";";
360 for (casadi_int i = 1; i < row.size(); ++i) {
361 s << row[i-1] <<
" -> " << row[i] <<
" [style=invis, weight=100];\n";
365 for (
const auto& e : model.edges) {
366 const auto& from = model.nodes.at(e.from);
367 const auto& to = model.nodes.at(e.to);
368 auto source = sources.find({e.from, e.output});
369 auto target = targets.find(e.to);
370 if (to.kind ==
"output" && target == targets.end())
continue;
371 s << (source != sources.end() ? source->second :
"n" +
str(e.from)
372 + (from.kind ==
"call" ?
":out" +
str(e.output) :
"")) <<
" -> ";
373 if (target != targets.end()) s << target->second;
375 s <<
"n" << e.to << (to.binary ? (e.input == 0 ?
":w" :
":e")
376 : dot_record(to) ?
":in" +
str(e.input) :
"");
382 struct GraphOptions {
383 std::string direction =
"TB", viz_js, view =
"function";
384 std::string viewer_url =
"https://unpkg.com/@casadi/casadi-viz@"
388 bool include_functions =
true, show_matrix_contents =
true, show_matrix_sizes =
true;
389 explicit GraphOptions(
const Dict& opts) {
390 for (
const auto& opt : opts) {
391 if (opt.first ==
"direction") {
392 direction = opt.second.to_string();
393 }
else if (opt.first ==
"view") {
394 view = opt.second.to_string();
395 }
else if (opt.first ==
"include_functions") {
396 include_functions = opt.second.to_bool();
397 }
else if (opt.first ==
"show_matrix_contents") {
398 show_matrix_contents = opt.second.to_bool();
399 }
else if (opt.first ==
"show_matrix_sizes") {
400 show_matrix_sizes = opt.second.to_bool();
401 }
else if (opt.first ==
"viewer_url") {
402 viewer_url = opt.second.to_string();
403 }
else if (opt.first ==
"viz_js") {
404 viz_js = opt.second.to_string();
406 casadi_error(
"Unknown export_graph option: " + opt.first);
409 casadi_assert(direction ==
"LR" || direction ==
"RL" || direction ==
"TB"
410 || direction ==
"BT",
"Invalid export_graph direction: " + direction);
412 casadi_assert(view ==
"function" || view ==
"expression",
413 "Invalid export_graph view: " + view);
418 std::string graph_dot(
const Function& f,
const GraphOptions& opts) {
419 casadi_assert(f.is_a(
"SXFunction") || f.is_a(
"MXFunction"),
420 "DOT export requires an SXFunction or MXFunction; use .html or .casadi_viz for "
423 graph_model(f, model);
424 std::ostringstream output;
425 graph_dot(output, f, model, opts.direction, opts.view,
426 opts.show_matrix_contents, opts.show_matrix_sizes);
430 void write_graph(
const std::string& fname,
const std::string& data,
431 const GraphOptions& options) {
432 if (fname.size() < 5 || fname.substr(fname.size()-5) !=
".html") {
434 *output << data <<
"\n";
436 casadi_assert(output->good(),
"Failed to write graph to '" + fname +
"'");
440 std::ostringstream config;
441 config <<
"{\"viewer_url\":" << graph_string(options.viewer_url) <<
",\"runtime\":";
442 if (options.viz_js.empty()) {
446 std::ostringstream contents;
447 contents << input->rdbuf();
448 config <<
"{\"source\":" << graph_string(contents.str()) <<
"}";
453 const std::string html = resource_casadi_viz;
454 const std::string graph_marker =
"@@GRAPH_DATA@@", viz_marker =
"@@VIZ_CONFIG@@";
455 size_t graph_pos = html.find(graph_marker), viz_pos = html.find(viz_marker);
456 casadi_assert_dev(graph_pos != std::string::npos && viz_pos > graph_pos);
458 *output << html.substr(0, graph_pos) << data
459 << html.substr(graph_pos + graph_marker.size(), viz_pos-graph_pos-graph_marker.size())
460 << config.str() << html.substr(viz_pos + viz_marker.size());
462 casadi_assert(output->good(),
"Failed to write graph to '" + fname +
"'");
465 std::string graph_extension(
const std::string& fname) {
466 const auto dot = fname.find_last_of(
'.');
467 const std::string extension =
dot == std::string::npos ?
"" : fname.substr(
dot);
468 casadi_assert(extension ==
".html" || extension ==
".dot" || extension ==
".casadi_viz",
469 "Unsupported export_graph extension '" + extension
470 +
"'. Supported extensions: .html, .dot, .casadi_viz");
476 std::string serialized_bundle(
const T& data,
const Dict& opts,
const std::string& view) {
477 Dict defaults = opts;
478 if (!defaults.count(
"view")) defaults[
"view"] = view;
479 const GraphOptions options(defaults);
480 StringSerializer serializer;
481 serializer.pack(data);
482 std::ostringstream s;
483 s <<
"{\"format\":\"casadi_viz\",\"version\":1,\"source\":"
484 << graph_string(serializer.encode()) <<
",\"view\":" << graph_string(options.view)
485 <<
",\"direction\":" << graph_string(options.direction)
487 <<
",\"include_functions\":" << (options.include_functions ?
"true" :
"false")
488 <<
",\"show_matrix_contents\":" << (options.show_matrix_contents ?
"true" :
"false")
489 <<
",\"show_matrix_sizes\":" << (options.show_matrix_sizes ?
"true" :
"false") <<
"}";
493 template<
typename MatType>
494 void export_expressions(
const std::vector<MatType>& expressions,
const std::string& fname,
496 if (graph_extension(fname) ==
".dot") {
497 Dict defaults = opts;
498 if (!defaults.count(
"view")) defaults[
"view"] =
"expression";
499 Function(
"expression", symvar(veccat(expressions)), expressions).export_graph(fname, defaults);
501 write_graph(fname, serialized_bundle(expressions, opts,
"expression"), GraphOptions(opts));
508 return serialized_bundle(*
this, opts,
"function");
512 const GraphOptions options(opts);
513 write_graph(fname, graph_extension(fname) ==
".dot" ? graph_dot(*
this, options)
514 : serialized_bundle(*
this, opts,
"function"), options);
518 export_expressions(std::vector<SX>{expression}, fname, opts);
521 void export_graph(
const std::vector<SX>& expressions,
const std::string& fname,
const Dict& opts) {
522 export_expressions(expressions, fname, opts);
526 export_expressions(std::vector<MX>{expression}, fname, opts);
529 void export_graph(
const std::vector<MX>& expressions,
const std::string& fname,
const Dict& opts) {
530 export_expressions(expressions, fname, opts);
534 return serialized_bundle(expressions, opts,
"expression");
542 return serialized_bundle(expressions, opts,
"expression");
static std::unique_ptr< std::ostream > ofstream_ptr(const std::string &path, std::ios_base::openmode mode=std::ios_base::out)
static std::unique_ptr< std::istream > ifstream_ptr(const std::string &path, std::ios_base::openmode mode=std::ios_base::in, bool fail=true)
void export_graph(const std::string &fname, const Dict &opts=Dict()) const
Export a Function graph as .html, .dot, or .casadi_viz.
Sparse matrix class. SX and DM are specializations.
static Sparsity scalar(bool dense_scalar=true)
Create a 1-by-1 sparsity pattern.
std::string str(const T &v)
String representation, any type.
GenericType::Dict Dict
C++ equivalent of Python's dict or MATLAB's struct.
CASADI_EXPORT std::string export_graph(const MX &expression, const Dict &opts=Dict())
Export expressions as .html, .dot, or .casadi_viz, defaulting to expression view.
T dot(const std::vector< T > &a, const std::vector< T > &b)
std::vector< casadi_int > path(const std::vector< casadi_int > &map, casadi_int i_start)
static bool is_binary(unsigned char op)
Is binary operation?
static std::string name(unsigned char op)