visualizer.cpp
1 /*
2  * This file is part of CasADi.
3  *
4  * CasADi -- A symbolic framework for dynamic optimization.
5  * Copyright (C) 2010-2023 Joel Andersson, Joris Gillis, Moritz Diehl,
6  * KU Leuven. All rights reserved.
7  * Copyright (C) 2011-2014 Greg Horn
8  *
9  * CasADi is free software; you can redistribute it and/or
10  * modify it under the terms of the GNU Lesser General Public
11  * License as published by the Free Software Foundation; either
12  * version 3 of the License, or (at your option) any later version.
13  *
14  * CasADi is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17  * Lesser General Public License for more details.
18  *
19  * You should have received a copy of the GNU Lesser General Public
20  * License along with CasADi; if not, write to the Free Software
21  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
22  *
23  */
24 
25 
26 #include "function.hpp"
27 #include "casadi_meta.hpp"
28 #include "function_internal.hpp"
29 #include "sx_function.hpp"
30 #include "sx.hpp"
31 #include "serializer.hpp"
32 #include "mx_function.hpp"
33 #include "filesystem_impl.hpp"
34 #include <casadi/core/resource_casadi_viz.hpp>
35 
36 #include <algorithm>
37 #include <iomanip>
38 #include <map>
39 #include <sstream>
40 
41 namespace casadi {
42 namespace {
43 
44 // Escape JSON and HTML script delimiters without interpreting expression labels.
45 std::string graph_string(const std::string& value) {
46  std::ostringstream s;
47  s << '"';
48  for (unsigned char c : value) {
49  if (c == '"' || c == '\\') {
50  s << '\\' << 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;
54  } else {
55  s << c;
56  }
57  }
58  s << '"';
59  return s.str();
60 }
61 
62 void graph_sparsity(std::ostream& s, const Sparsity& sp) {
63  s << "{\"shape\":[" << sp.size1() << "," << sp.size2() << "],\"colind\":[";
64  for (casadi_int i = 0; i <= sp.size2(); ++i) {
65  if (i) s << ",";
66  s << sp.colind()[i];
67  }
68  s << "],\"row\":[";
69  for (casadi_int i = 0; i < sp.nnz(); ++i) {
70  if (i) s << ",";
71  s << sp.row()[i];
72  }
73  s << "]}";
74 }
75 
76 struct GraphNode {
77  casadi_int id, op, io = -1, offset = 0;
78  bool binary, ordered;
79  std::string kind, display, symbol;
80  std::vector<std::string> input_names, output_names, constants;
81  std::vector<Sparsity> inputs, outputs;
82 };
83 struct GraphEdge { casadi_int from, output, to, input; };
84 struct GraphModel {
85  std::vector<GraphNode> nodes;
86  std::vector<GraphEdge> edges;
87 };
88 
89 std::string graph_model(Function function, const std::string& direction,
90  std::vector<Function>& functions, std::map<const FunctionInternal*, casadi_int>& indices,
91  bool include_functions, GraphModel& model) {
92  auto sx = dynamic_cast<const SXFunction*>(function.get());
93  auto mx = dynamic_cast<const MXFunction*>(function.get());
94  const auto sx_inputs = sx ? function.sx_in() : std::vector<SX>{};
95  const auto mx_inputs = mx ? function.mx_in() : std::vector<MX>{};
96  std::ostringstream data, edges;
97  data << "{\"version\":1,\"name\":" << graph_string(function.name())
98  << ",\"type\":" << graph_string(function.class_name())
99  << ",\"direction\":" << graph_string(direction) << ",\"inputs\":[";
100  for (casadi_int i = 0; i < function.n_in(); ++i) {
101  if (i) data << ",";
102  data << "{\"name\":" << graph_string(function.name_in(i)) << ",\"sparsity\":";
103  graph_sparsity(data, function.sparsity_in(i));
104  data << "}";
105  }
106  data << "],\"outputs\":[";
107  for (casadi_int i = 0; i < function.n_out(); ++i) {
108  if (i) data << ",";
109  data << "{\"name\":" << graph_string(function.name_out(i)) << ",\"sparsity\":";
110  graph_sparsity(data, function.sparsity_out(i));
111  data << "}";
112  }
113  data << "],\"nodes\":[";
114 
115  // Work slots are reused: resolve dependencies before replacing their producers.
116  std::map<casadi_int, std::pair<casadi_int, casadi_int>> producer;
117  bool first_edge = true;
118  for (casadi_int k = 0; k < function.n_instructions(); ++k) {
119  casadi_int op = function.instruction_id(k);
120  auto arg = function.instruction_input(k), res = function.instruction_output(k);
121  std::string label = casadi_math<double>::name(op);
122  std::string expression = sx ? sx->print(sx->algorithm_.at(k))
123  : mx->print(mx->algorithm_.at(k));
124  GraphNode node;
125  node.id = k; node.op = op;
126  std::string kind = "operation", io_metadata, call_metadata;
127  if (op == OP_INPUT || op == OP_OUTPUT) {
128  bool input = op == OP_INPUT;
129  casadi_int io = (input ? arg : res).at(0);
130  casadi_int offset = sx ? (input ? arg : res).at(1)
131  : mx->algorithm_.at(k).data->offset();
132  node.io = io; node.offset = offset;
133  io_metadata = ",\"io_index\":" + str(io) + ",\"io_offset\":" + str(offset);
134  if (input) {
135  std::string symbol = sx ? str(sx_inputs.at(io).nonzeros().at(offset))
136  : str(mx_inputs.at(io));
137  node.symbol = symbol;
138  io_metadata += ",\"symbol\":" + graph_string(symbol);
139  }
140  label = (input ? function.name_in(io) : function.name_out(io));
141  if ((sx && (input ? function.nnz_in(io) : function.nnz_out(io)) != 1) || offset != 0) {
142  label += "[" + str(offset) + "]";
143  }
144  kind = input ? "input" : "output";
145  } else if (op == OP_PARAMETER) {
146  kind = "symbol";
147  label = sx ? str(sx->free_vars_.at(sx->algorithm_.at(k).i1))
148  : mx->algorithm_.at(k).data.name();
149  } else if (op == OP_CONST) {
150  kind = "constant";
151  } else if (op == OP_CALL) {
152  kind = "call";
153  label = (sx ? sx->call_.el.at(sx->algorithm_.at(k).i1).f.name()
154  : mx->algorithm_.at(k).data.which_function().name());
155  }
156  if (op == OP_INPUT) arg.clear();
157  if (op == OP_OUTPUT) res.clear();
158  std::vector<std::string> input_names, output_names, constants;
159  for (casadi_int i = 0; i < arg.size(); ++i) input_names.push_back("arg" + str(i));
160  for (casadi_int i = 0; i < res.size(); ++i) output_names.push_back("out" + str(i));
161  if (op == OP_CALL) {
162  const Function& f = sx ? sx->call_.el.at(sx->algorithm_.at(k).i1).f
163  : mx->algorithm_.at(k).data.which_function();
164  call_metadata = ",\"callee_type\":" + graph_string(f.class_name());
165  Dict info = f.info();
166  auto source = info.find("model_path");
167  if (source != info.end()) {
168  std::string path = source->second.to_string();
169  if (!path.empty()) {
170  call_metadata += ",\"model_path\":" + graph_string(path);
171  label += " [" + path.substr(path.find_last_of("/\\") + 1) + "]";
172  }
173  }
174  if (include_functions && (f.is_a("SXFunction") || f.is_a("MXFunction"))) {
175  auto found = indices.find(f.get());
176  if (found == indices.end()) {
177  found = indices.emplace(f.get(), functions.size()).first;
178  functions.push_back(f);
179  }
180  call_metadata += ",\"callee\":" + str(found->second);
181  }
182  input_names.clear(); output_names.clear();
183  for (casadi_int side = 0; side < 2; ++side) {
184  auto& names = side == 0 ? input_names : output_names;
185  for (casadi_int i = 0; i < (side == 0 ? f.n_in() : f.n_out()); ++i) {
186  std::string name = side == 0 ? f.name_in(i) : f.name_out(i);
187  casadi_int count = side == 0 ? f.nnz_in(i) : f.nnz_out(i);
188  if (sx) {
189  for (casadi_int j = 0; j < count; ++j) {
190  names.push_back(name + (count == 1 ? "" : "[" + str(j) + "]"));
191  }
192  } else {
193  names.push_back(name);
194  }
195  }
196  }
197  } else if (op == OP_MTIMES) {
198  input_names = {"accumulator", "left", "right"};
199  } else if (op == OP_GETNONZEROS) {
200  input_names = {"source"};
201  } else if (op == OP_SETNONZEROS || op == OP_ADDNONZEROS) {
202  input_names = {"base", "values"};
203  }
204  std::string display = label, formula = label;
205  if (kind == "operation") {
206  if (mx) {
207  formula = MX::print_operator(mx->algorithm_.at(k).data, input_names);
208  } else {
209  formula = casadi_math<double>::pre(op);
210  for (casadi_int i = 0; i < input_names.size(); ++i) {
211  if (i) formula += casadi_math<double>::sep(op);
212  formula += input_names[i];
213  }
214  formula += casadi_math<double>::post(op);
215  }
216  switch (op) {
217  case OP_ADD: display = "+"; break;
218  case OP_SUB: display = "-"; break;
219  case OP_MUL: display = "*"; break;
220  case OP_DIV: display = "/"; break;
221  case OP_NEG: display = "-"; break;
222  case OP_SQ: display = "(.)^2"; break;
223  case OP_POW: case OP_CONSTPOW: display = "pow"; break;
224  case OP_TWICE: display = "2*(.)"; break;
225  case OP_INV: display = "1/(.)"; break;
226  default: break;
227  }
228  } else if (op == OP_CONST) {
229  std::vector<double> values = sx ? std::vector<double> {sx->algorithm_.at(k).d}
230  : static_cast<DM>(mx->algorithm_.at(k).data).nonzeros();
231  for (double value : values) {
232  std::ostringstream number;
233  number << std::setprecision(17) << value;
234  constants.push_back(number.str());
235  }
236  display = constants.size() == 1 ? constants.front() : "constant";
237  formula = display;
238  }
239  node.kind = kind; node.display = display;
240  node.binary = casadi_math<double>::is_binary(op);
241  node.ordered = arg.size() > 1 && !operation_checker<CommChecker>(op);
242  node.input_names = input_names; node.output_names = output_names;
243  node.constants = constants;
244  if (k) data << ",";
245  data << "{\"id\":" << k << ",\"op\":" << op << ",\"label\":"
246  << graph_string(label) << ",\"kind\":" << graph_string(kind)
247  << ",\"expression\":" << graph_string(expression)
248  << ",\"display\":" << graph_string(display)
249  << ",\"formula\":" << graph_string(formula)
250  << ",\"binary\":" << (casadi_math<double>::is_binary(op) ? "true" : "false")
251  << ",\"ordered\":" << (arg.size() > 1 && !operation_checker<CommChecker>(op)
252  ? "true" : "false");
253  for (const auto& entry : std::map<std::string, std::vector<std::string>>{
254  {"input_names", input_names}, {"output_names", output_names}, {"constants", constants}}) {
255  data << "," << graph_string(entry.first) << ":[";
256  for (casadi_int i = 0; i < entry.second.size(); ++i) {
257  if (i) data << ",";
258  data << graph_string(entry.second[i]);
259  }
260  data << "]";
261  }
262  if (mx && (op == OP_GETNONZEROS || op == OP_SETNONZEROS || op == OP_ADDNONZEROS)) {
263  data << ",\"mapping_kind\":" << graph_string(op == OP_GETNONZEROS ? "extract"
264  : op == OP_SETNONZEROS ? "assign" : "add") << ",\"mapping\":[";
265  const auto mapping = mx->algorithm_.at(k).data.mapping().nonzeros();
266  for (casadi_int i = 0; i < mapping.size(); ++i) {
267  if (i) data << ",";
268  data << mapping[i];
269  }
270  data << "]";
271  }
272  data << io_metadata << call_metadata << ",\"inputs\":[";
273  for (casadi_int i = 0; i < arg.size(); ++i) {
274  if (i) data << ",";
275  node.inputs.push_back(sx ? Sparsity::scalar() : mx->algorithm_.at(k).data->dep(i).sparsity());
276  graph_sparsity(data, node.inputs.back());
277  if (arg[i] < 0) continue;
278  auto p = producer.find(arg[i]);
279  casadi_assert(p != producer.end(), "Missing graph producer for instruction " + str(k));
280  model.edges.push_back({p->second.first, p->second.second, k, i});
281  if (!first_edge) edges << ",";
282  first_edge = false;
283  edges << "{\"from\":" << p->second.first << ",\"output\":" << p->second.second
284  << ",\"to\":" << k << ",\"input\":" << i << "}";
285  }
286  data << "],\"outputs\":[";
287  for (casadi_int i = 0; i < res.size(); ++i) {
288  if (i) data << ",";
289  node.outputs.push_back(sx ? Sparsity::scalar() : mx->algorithm_.at(k).data->sparsity(i));
290  graph_sparsity(data, node.outputs.back());
291  if (res[i] >= 0) producer[res[i]] = {k, i};
292  }
293  data << "]}";
294  model.nodes.push_back(std::move(node));
295  }
296  data << "],\"edges\":[" << edges.str() << "]}";
297 
298  return data.str();
299 }
300 
301 // DOT quoted strings and HTML labels have different escaping rules from JSON.
302 std::string dot_quote(const std::string& value) {
303  std::string out = "\"";
304  for (char c : value) {
305  if (c == '\n') {
306  out += "\\n";
307  } else if (c == '\r') {
308  out += "\\r";
309  } else {
310  if (c == '\\' || c == '"') out += '\\';
311  out += c;
312  }
313  }
314  return out + "\"";
315 }
316 std::string dot_html(const std::string& value) {
317  std::string out;
318  for (char c : value) {
319  switch (c) {
320  case '&': out += "&amp;"; break;
321  case '<': out += "&lt;"; break;
322  case '>': out += "&gt;"; break;
323  case '"': out += "&quot;"; break;
324  default: out += c;
325  }
326  }
327  return out;
328 }
329 std::string graph_size(const Sparsity& sp) {
330  return str(sp.size1()) + "-by-" + str(sp.size2());
331 }
332 void dot_matrix(std::ostream& s, const std::string& id, const Sparsity& sp,
333  const std::string& title, const std::vector<std::string>* constants = nullptr,
334  const std::string& color = "#666666", bool show_sizes = true) {
335  s << id << " [shape=plain, fontcolor=\"#666666\", fontsize=10, label=<"
336  << "<TABLE BORDER=\"0\" CELLBORDER=\"0\" CELLSPACING=\"0\" CELLPADDING=\"3\">";
337  if (show_sizes || !title.empty()) s << "<TR><TD COLSPAN=\""
338  << std::max<casadi_int>(1, std::min<casadi_int>(8, sp.size2()))
339  << "\"><FONT COLOR=\"" << color << "\">"
340  << dot_html(show_sizes ? (title.empty() ? graph_size(sp) : title + " : " + graph_size(sp))
341  : title)
342  << "</FONT></TD></TR>";
343  std::map<std::pair<casadi_int, casadi_int>, casadi_int> lookup;
344  for (casadi_int c = 0; c < std::min<casadi_int>(8, sp.size2()); ++c) {
345  for (casadi_int k = sp.colind()[c]; k < sp.colind()[c+1]; ++k) {
346  if (sp.row()[k] < 8) lookup[{sp.row()[k], c}] = k;
347  }
348  }
349  for (casadi_int r = 0; r < std::min<casadi_int>(8, sp.size1()); ++r) {
350  s << "<TR>";
351  for (casadi_int c = 0; c < std::min<casadi_int>(8, sp.size2()); ++c) {
352  auto nz = lookup.find({r, c});
353  s << "<TD BGCOLOR=\"white\"";
354  if (nz != lookup.end()) s << " PORT=\"nz" << nz->second << "\"";
355  s << ">";
356  if (constants) s << (nz == lookup.end() ? "." : dot_html(constants->at(nz->second)));
357  else
358  s << "<TABLE BORDER=\"0\" CELLBORDER=\"0\" CELLSPACING=\"0\" CELLPADDING=\"0\">"
359  << "<TR><TD FIXEDSIZE=\"TRUE\" WIDTH=\"9\" HEIGHT=\"9\" BGCOLOR=\""
360  << (nz == lookup.end() ? "white" : "#111111") << "\"></TD></TR></TABLE>";
361  s << "</TD>";
362  }
363  if (!sp.size2()) s << "<TD>empty</TD>";
364  s << "</TR>";
365  }
366  if (!sp.size1()) s << "<TR><TD>empty</TD></TR>";
367  if (sp.size1() > 8 || sp.size2() > 8) s << "<TR><TD>...</TD></TR>";
368  s << "</TABLE>>];\n";
369 }
370 bool dot_record(const GraphNode& n) {
371  return (n.ordered && !n.binary) || n.kind == "call";
372 }
373 bool dot_has_table(const GraphNode& n, const Sparsity& sp, bool show_contents) {
374  if (!show_contents || sp.numel() == 1) return false;
375  if (n.kind == "call" || n.inputs.empty() || n.op == OP_GETNONZEROS
376  || n.op == OP_SETNONZEROS || n.op == OP_ADDNONZEROS) return true;
377  for (const auto& input : n.inputs) if (input != sp) return true;
378  return false;
379 }
380 void dot_ports(std::ostream& s, const GraphNode& n,
381  const std::vector<std::string>& names, const std::string& prefix, bool show_sizes) {
382  if (names.empty()) return;
383  s << "<TR><TD><TABLE BORDER=\"0\" CELLBORDER=\"1\" COLOR=\"#d26666\"><TR>";
384  for (casadi_int i = 0; i < names.size(); ++i) {
385  s << "<TD PORT=\"" << prefix << i << "\"><FONT POINT-SIZE=\"10\">"
386  << dot_html(names[i]);
387  const auto& sp = prefix == "in" ? n.inputs.at(i) : n.outputs.at(i);
388  if (show_sizes && sp.numel() != 1) s << " " << graph_size(sp);
389  s << "</FONT></TD>";
390  }
391  s << "</TR></TABLE></TD></TR>";
392 }
393 void graph_dot(std::ostream& s, const Function& f, const GraphModel& model,
394  const std::string& direction, const std::string& view, bool show_contents, bool show_sizes) {
395  bool function_view = view == "function";
396  std::map<std::pair<casadi_int, casadi_int>, std::string> sources;
397  std::map<casadi_int, std::string> targets;
398  s << "digraph G {\ngraph [rankdir=" << (function_view ? "TB" : direction)
399  << ", bgcolor=\"white\", pad=0.4, nodesep=0.55, ranksep=0.65];\n"
400  << "node [shape=ellipse, style=filled, color=\"#b00000\", fillcolor=\"#b00000\", "
401  << "fontcolor=white, fontname=Helvetica, fontsize=14, margin=\"0.15,0.08\", "
402  << "width=0.5, height=0.5];\nedge [color=\"#34658b\", penwidth=1.5, arrowsize=0.65];\n";
403  for (const auto& n : model.nodes) {
404  if (n.kind == "output" || (function_view && n.kind == "input")) continue;
405  const std::string id = "n" + str(n.id);
406  std::string display = !function_view && n.kind == "input" ? n.symbol : n.display;
407  std::string color = n.kind == "input" || n.kind == "symbol" ? "#34658b"
408  : n.kind == "constant" ? "#38754d" : "#b00000";
409  s << id << " [color=" << dot_quote(color) << ", fillcolor=" << dot_quote(color);
410  if (dot_record(n)) {
411  s << ", shape=plain, label=<<TABLE BORDER=\"0\" CELLBORDER=\"0\" "
412  << "CELLSPACING=\"0\" CELLPADDING=\"6\" BGCOLOR=\"#b00000\">";
413  dot_ports(s, n, n.input_names, "in", show_sizes);
414  s << "<TR><TD>" << dot_html(display) << "</TD></TR>";
415  if (n.kind == "call") dot_ports(s, n, n.output_names, "out", show_sizes);
416  s << "</TABLE>>";
417  } else {
418  const auto& ports = n.outputs.empty() ? n.inputs : n.outputs;
419  bool table = false;
420  for (const auto& sp : n.outputs) table = table || dot_has_table(n, sp, show_contents);
421  if (show_sizes && !table && !ports.empty() && ports.front().numel() != 1) {
422  display += "\n" + graph_size(ports.front());
423  }
424  s << ", label=" << dot_quote(display);
425  }
426  s << "];\n";
427  for (casadi_int i = 0; i < n.outputs.size(); ++i) {
428  const auto& sp = n.outputs[i];
429  if (!dot_has_table(n, sp, show_contents)) continue;
430  std::string value = "v" + str(n.id) + "_" + str(i);
431  dot_matrix(s, value, sp, n.kind == "call" ? n.output_names.at(i) : "",
432  n.kind == "constant" ? &n.constants : nullptr, "#666666", show_sizes);
433  s << id << (n.kind == "call" ? ":out" + str(i) : "") << " -> " << value << ";\n";
434  sources[{n.id, i}] = value;
435  }
436  }
437  for (bool input : {true, false}) {
438  std::vector<std::string> row;
439  for (casadi_int i = 0; i < (input ? f.n_in() : f.n_out()); ++i) {
440  const auto& sp = input ? f.sparsity_in(i) : f.sparsity_out(i);
441  if (!function_view && (input || !show_contents || !f.is_a("SXFunction")
442  || sp.numel() == 1)) continue;
443  std::string kind = input ? "input" : "output", id = "b" + kind + "_" + str(i);
444  std::string name = function_view ? (input ? f.name_in(i) : f.name_out(i)) : "";
445  if (show_contents && sp.numel() != 1) dot_matrix(s, id, sp, name, nullptr,
446  function_view ? (input ? "#34658b" : "#b00000") : "#666666", show_sizes);
447  else
448  s << id << " [label=" << dot_quote(name
449  + (show_sizes && sp.numel() != 1 ? "\n" + graph_size(sp) : "")) << ", color=\""
450  << (input ? "#34658b" : "#b00000") << "\", fillcolor=\""
451  << (input ? "#34658b" : "#b00000") << "\"];\n";
452  for (const auto& n : model.nodes) {
453  if (n.kind != kind || n.io != i) continue;
454  std::string endpoint = id;
455  if (show_contents && f.is_a("SXFunction") && sp.numel() != 1 && n.offset < sp.nnz()) {
456  const auto col = std::upper_bound(sp.colind(), sp.colind()+sp.size2()+1, n.offset)
457  - sp.colind() - 1;
458  if (col < 8 && sp.row()[n.offset] < 8) endpoint += ":nz" + str(n.offset);
459  }
460  if (input) sources[{n.id, 0}] = endpoint;
461  else
462  targets[n.id] = endpoint;
463  }
464  row.push_back(id);
465  }
466  if (function_view && !row.empty()) {
467  s << "{rank=" << (input ? "source" : "sink") << ";";
468  for (const auto& id : row) s << id << ";";
469  s << "}\n";
470  for (casadi_int i = 1; i < row.size(); ++i) {
471  s << row[i-1] << " -> " << row[i] << " [style=invis, weight=100];\n";
472  }
473  }
474  }
475  for (const auto& e : model.edges) {
476  const auto& from = model.nodes.at(e.from);
477  const auto& to = model.nodes.at(e.to);
478  auto source = sources.find({e.from, e.output});
479  auto target = targets.find(e.to);
480  if (to.kind == "output" && target == targets.end()) continue;
481  s << (source != sources.end() ? source->second : "n" + str(e.from)
482  + (from.kind == "call" ? ":out" + str(e.output) : "")) << " -> ";
483  if (target != targets.end()) s << target->second;
484  else
485  s << "n" << e.to << (to.binary ? (e.input == 0 ? ":w" : ":e")
486  : dot_record(to) ? ":in" + str(e.input) : "");
487  s << ";\n";
488  }
489  s << "}\n";
490 }
491 
492 struct GraphOptions {
493  std::string direction = "TB", viz_js, view = "function";
494  std::string viewer_url = "https://unpkg.com/@casadi/casadi-viz@"
495  + std::string(CasadiMeta::version()).substr(
496  0, std::string(CasadiMeta::version()).find_last_of('.'))
497  + "/dist/index.js";
498  bool include_functions = true, show_matrix_contents = true, show_matrix_sizes = true;
499  explicit GraphOptions(const Dict& opts) {
500  for (const auto& opt : opts) {
501  if (opt.first == "direction") {
502  direction = opt.second.to_string();
503  } else if (opt.first == "view") {
504  view = opt.second.to_string();
505  } else if (opt.first == "include_functions") {
506  include_functions = opt.second.to_bool();
507  } else if (opt.first == "show_matrix_contents") {
508  show_matrix_contents = opt.second.to_bool();
509  } else if (opt.first == "show_matrix_sizes") {
510  show_matrix_sizes = opt.second.to_bool();
511  } else if (opt.first == "viewer_url") {
512  viewer_url = opt.second.to_string();
513  } else if (opt.first == "viz_js") {
514  viz_js = opt.second.to_string();
515  } else {
516  casadi_error("Unknown export_graph option: " + opt.first);
517  }
518  }
519  casadi_assert(direction == "LR" || direction == "RL" || direction == "TB"
520  || direction == "BT", "Invalid export_graph direction: " + direction);
521 
522  casadi_assert(view == "function" || view == "expression",
523  "Invalid export_graph view: " + view);
524 
525  }
526 };
527 
528 std::string graph_bundle(const Function& f, const GraphOptions& opts, bool dot = false) {
529  casadi_assert(f.is_a("SXFunction") || f.is_a("MXFunction"),
530  "export_graph requires an SXFunction or MXFunction");
531  std::vector<Function> functions{f};
532  std::map<const FunctionInternal*, casadi_int> indices{{f.get(), 0}};
533  std::vector<std::string> models;
534  for (casadi_int i = 0; i < functions.size(); ++i) {
535  GraphModel model;
536  models.push_back(graph_model(functions[i], opts.direction, functions, indices,
537  opts.include_functions, model));
538  if (dot) {
539  std::ostringstream output;
540  graph_dot(output, f, model, opts.direction, opts.view,
541  opts.show_matrix_contents, opts.show_matrix_sizes);
542  return output.str();
543  }
544  }
545  std::ostringstream data;
546  data << models.front().substr(0, models.front().size()-1)
547  << ",\"format\":\"casadi_viz\",\"view\":"
548  << graph_string(opts.view) << ",\"casadi_version\":"
549  << graph_string(CasadiMeta::version()) << ",\"functions\":[";
550  for (casadi_int i = 1; i < models.size(); ++i) {
551  if (i > 1) data << ",";
552  data << models[i];
553  }
554  data << "]}";
555 
556  return data.str();
557 }
558 
559 void write_graph(const std::string& fname, const std::string& data,
560  const GraphOptions& options) {
561  if (fname.size() < 5 || fname.substr(fname.size()-5) != ".html") {
562  auto output = Filesystem::ofstream_ptr(fname);
563  *output << data << "\n";
564  output->flush();
565  casadi_assert(output->good(), "Failed to write graph to '" + fname + "'");
566  return;
567  }
568 
569  std::ostringstream config;
570  config << "{\"viewer_url\":" << graph_string(options.viewer_url) << ",\"runtime\":";
571  if (options.viz_js.empty()) {
572  config << "null";
573  } else {
574  auto input = Filesystem::ifstream_ptr(options.viz_js);
575  std::ostringstream contents;
576  contents << input->rdbuf();
577  config << "{\"source\":" << graph_string(contents.str()) << "}";
578  }
579 
580  config << "}";
581 
582  const std::string html = resource_casadi_viz;
583  const std::string graph_marker = "@@GRAPH_DATA@@", viz_marker = "@@VIZ_CONFIG@@";
584  size_t graph_pos = html.find(graph_marker), viz_pos = html.find(viz_marker);
585  casadi_assert_dev(graph_pos != std::string::npos && viz_pos > graph_pos);
586  auto output = Filesystem::ofstream_ptr(fname);
587  *output << html.substr(0, graph_pos) << data
588  << html.substr(graph_pos + graph_marker.size(), viz_pos-graph_pos-graph_marker.size())
589  << config.str() << html.substr(viz_pos + viz_marker.size());
590  output->flush();
591  casadi_assert(output->good(), "Failed to write graph to '" + fname + "'");
592 }
593 
594 std::string graph_extension(const std::string& fname) {
595  const auto dot = fname.find_last_of('.');
596  const std::string extension = dot == std::string::npos ? "" : fname.substr(dot);
597  casadi_assert(extension == ".html" || extension == ".dot" || extension == ".casadi_viz",
598  "Unsupported export_graph extension '" + extension
599  + "'. Supported extensions: .html, .dot, .casadi_viz");
600  return extension;
601 }
602 
603 template<typename MatType>
604 std::string expression_bundle(const std::vector<MatType>& expressions, const Dict& opts) {
605  Dict defaults = opts;
606  if (!defaults.count("view")) defaults["view"] = "expression";
607  const GraphOptions options(defaults);
608  StringSerializer serializer;
609  serializer.pack(expressions);
610  std::ostringstream data;
611  data << "{\"format\":\"casadi_viz\",\"version\":1,\"source\":"
612  << graph_string(serializer.encode()) << ",\"view\":" << graph_string(options.view)
613  << ",\"direction\":" << graph_string(options.direction)
614  << ",\"casadi_version\":" << graph_string(CasadiMeta::version())
615  << ",\"include_functions\":" << (options.include_functions ? "true" : "false")
616  << ",\"show_matrix_contents\":" << (options.show_matrix_contents ? "true" : "false")
617  << ",\"show_matrix_sizes\":" << (options.show_matrix_sizes ? "true" : "false") << "}";
618  return data.str();
619 }
620 
621 template<typename MatType>
622 void export_expressions(const std::vector<MatType>& expressions, const std::string& fname,
623  const Dict& opts) {
624  if (graph_extension(fname) == ".dot") {
625  Dict defaults = opts;
626  if (!defaults.count("view")) defaults["view"] = "expression";
627  Function("expression", symvar(veccat(expressions)), expressions).export_graph(fname, defaults);
628  } else {
629  write_graph(fname, expression_bundle(expressions, opts), GraphOptions(opts));
630  }
631 }
632 
633 } // namespace
634 
635 std::string Function::export_graph(const Dict& opts) const {
636  return graph_bundle(*this, GraphOptions(opts));
637 }
638 
639 void Function::export_graph(const std::string& fname, const Dict& opts) const {
640  const std::string extension = graph_extension(fname);
641  const GraphOptions options(opts);
642  write_graph(fname, graph_bundle(*this, options, extension == ".dot"), options);
643 }
644 
645 void export_graph(const SX& expression, const std::string& fname, const Dict& opts) {
646  export_expressions(std::vector<SX>{expression}, fname, opts);
647 }
648 
649 void export_graph(const std::vector<SX>& expressions, const std::string& fname, const Dict& opts) {
650  export_expressions(expressions, fname, opts);
651 }
652 
653 void export_graph(const MX& expression, const std::string& fname, const Dict& opts) {
654  export_expressions(std::vector<MX>{expression}, fname, opts);
655 }
656 
657 void export_graph(const std::vector<MX>& expressions, const std::string& fname, const Dict& opts) {
658  export_expressions(expressions, fname, opts);
659 }
660 
661 std::string export_graph(const std::vector<SX>& expressions, const Dict& opts) {
662  return expression_bundle(expressions, opts);
663 }
664 
665 std::string export_graph(const SX& expression, const Dict& opts) {
666  return export_graph(std::vector<SX>{expression}, opts);
667 }
668 
669 std::string export_graph(const std::vector<MX>& expressions, const Dict& opts) {
670  return expression_bundle(expressions, opts);
671 }
672 
673 std::string export_graph(const MX& expression, const Dict& opts) {
674  return export_graph(std::vector<MX>{expression}, opts);
675 }
676 
677 } // namespace casadi
static const char * version()
Obtain the version number of CasADi.
Definition: casadi_meta.cpp:30
static std::unique_ptr< std::ostream > ofstream_ptr(const std::string &path, std::ios_base::openmode mode=std::ios_base::out)
Definition: filesystem.cpp:115
static std::unique_ptr< std::istream > ifstream_ptr(const std::string &path, std::ios_base::openmode mode=std::ios_base::in, bool fail=true)
Definition: filesystem.cpp:135
void export_graph(const std::string &fname, const Dict &opts=Dict()) const
Export an SX/MX instruction graph as .html, .dot, or .casadi_viz.
Definition: visualizer.cpp:639
MX - Matrix expression.
Definition: mx.hpp:92
static std::string print_operator(const MX &x, const std::vector< std::string > &args)
Definition: mx.cpp:1499
Sparse matrix class. SX and DM are specializations.
Definition: matrix_decl.hpp:99
static Sparsity scalar(bool dense_scalar=true)
Create a scalar sparsity pattern *.
Definition: sparsity.hpp:153
The casadi namespace.
Definition: archiver.cpp:28
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.
Definition: visualizer.cpp:673
T dot(const std::vector< T > &a, const std::vector< T > &b)
Matrix< double > DM
Definition: dm_fwd.hpp:33
std::vector< casadi_int > path(const std::vector< casadi_int > &map, casadi_int i_start)
@ OP_INV
Definition: calculus.hpp:73
@ OP_OUTPUT
Definition: calculus.hpp:82
@ OP_SETNONZEROS
Definition: calculus.hpp:163
@ OP_CONST
Definition: calculus.hpp:79
@ OP_TWICE
Definition: calculus.hpp:67
@ OP_INPUT
Definition: calculus.hpp:82
@ OP_SUB
Definition: calculus.hpp:65
@ OP_POW
Definition: calculus.hpp:66
@ OP_ADDNONZEROS
Definition: calculus.hpp:157
@ OP_PARAMETER
Definition: calculus.hpp:85
@ OP_MTIMES
Definition: calculus.hpp:100
@ OP_CALL
Definition: calculus.hpp:88
@ OP_ADD
Definition: calculus.hpp:65
@ OP_DIV
Definition: calculus.hpp:65
@ OP_NEG
Definition: calculus.hpp:66
@ OP_CONSTPOW
Definition: calculus.hpp:66
@ OP_MUL
Definition: calculus.hpp:65
@ OP_SQ
Definition: calculus.hpp:67
@ OP_GETNONZEROS
Definition: calculus.hpp:151
static bool is_binary(unsigned char op)
Is binary operation?
Definition: calculus.hpp:1612
static std::string post(unsigned char op)
Definition: calculus.hpp:1803
static std::string pre(unsigned char op)
Definition: calculus.hpp:1762
static std::string name(unsigned char op)
Definition: calculus.hpp:1665
static std::string sep(unsigned char op)
Definition: calculus.hpp:1785