onnx_export.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 "onnx_model.hpp"
27 #include <casadi/core/casadi_meta.hpp>
28 #include <memory>
29 #include <set>
30 
32 namespace casadi {
33 
34  std::string onnx_input_name(const Function& f, casadi_int i) {
35  std::string n = f.name_in(i);
36  return n.empty() ? "input_" + std::to_string(i) : n;
37  }
38 
39  std::string onnx_output_name(const Function& f, casadi_int i) {
40  std::string n = f.name_out(i);
41  return n.empty() ? "output_" + std::to_string(i) : n;
42  }
43 
44  void Onnx::set_real_tensor_type(onnx::ValueInfoProto* value, const Sparsity& sp) {
45  // Transpose-representation invariant: an ONNX tensor stores its CasADi value's column-major
46  // bytes with the shape declared REVERSED, so a CasADi (r,c) value is an ONNX (c,r) tensor.
47  onnx::TypeProto::Tensor* tensor_type = value->mutable_type()->mutable_tensor_type();
48  tensor_type->set_elem_type(real_type());
49  onnx::TensorShapeProto* shape = tensor_type->mutable_shape();
50  shape->add_dim()->set_dim_value(sp.size2());
51  shape->add_dim()->set_dim_value(sp.size1());
52  }
53 
54  // Add graph inputs. Subgraphs use a prefixed name for uniqueness; the main graph (empty
55  // prefix) uses the function's own input names.
56  void Onnx::add_graph_inputs(onnx::GraphProto* graph, const Function& f,
57  const std::string& name_prefix) {
58  for (casadi_int i = 0; i < f.n_in(); ++i) {
59  onnx::ValueInfoProto* input = graph->add_input();
60  input->set_name(name_prefix.empty() ? onnx_input_name(f, i)
61  : name_prefix + "_i" + std::to_string(i));
62  set_real_tensor_type(input, f.sparsity_in(i));
63  }
64  }
65 
66  // Add graph outputs (see add_graph_inputs for the naming convention)
67  void Onnx::add_graph_outputs(onnx::GraphProto* graph, const Function& f,
68  const std::string& name_prefix) {
69  for (casadi_int i = 0; i < f.n_out(); ++i) {
70  onnx::ValueInfoProto* output = graph->add_output();
71  output->set_name(name_prefix.empty() ? onnx_output_name(f, i)
72  : name_prefix + "_o" + std::to_string(i));
73  set_real_tensor_type(output, f.sparsity_out(i));
74  }
75  }
76 
77  void Onnx::load(const Function& f) {
78  model_.Clear();
79  exported_functions_.clear();
80  model_.set_ir_version(8);
81  model_.set_producer_name("CasADi");
82  model_.set_producer_version(CasadiMeta::version());
83 
84  onnx::OperatorSetIdProto* opset = model_.add_opset_import();
85  opset->set_domain(""); // default ONNX domain
86  opset->set_version(16); // opset 16: ScatterND reduction
87 
88  onnx::GraphProto* graph = model_.mutable_graph();
89  graph->set_name(f.name());
90  add_graph_inputs(graph, f);
91 
92  // work vector index -> the ONNX tensor name that produced it
93  std::map<casadi_int, std::string> work_to_onnx;
94 
95  casadi_int n_instr = f.n_instructions();
96 
97  // Pre-scan: count segments per output (multi-segment = horz/vert/diagcat output)
98  std::map<casadi_int, casadi_int> output_segment_count;
99  for (casadi_int k = 0; k < n_instr; ++k) {
100  if (f.instruction_id(k) == OP_OUTPUT) {
101  MX mx = f.instruction_MX(k);
102  Dict info = mx.info();
103  casadi_int output_idx = info["ind"];
104  output_segment_count[output_idx]++;
105  }
106  }
107 
108  // Track segment values for multi-segment outputs: output_idx -> (offset -> onnx_name)
109  std::map<casadi_int, std::map<casadi_int, std::string>> output_segment_values;
110  // and each segment's source sparsity, to extract its nonzero array on reassembly
111  std::map<casadi_int, std::map<casadi_int, Sparsity>> output_segment_sparsity;
112 
113  for (casadi_int k = 0; k < n_instr; ++k) {
114  casadi_int op = f.instruction_id(k);
115  std::vector<casadi_int> o = f.instruction_output(k);
116  std::vector<casadi_int> i = f.instruction_input(k);
117 
118  // Unique result name keyed on the instruction index
119  std::string node_output = "n" + std::to_string(k);
120 
121  // OP_OUTPUT is special: it supports multi-segment outputs (horz/vert/diagcat)
122  if (op == OP_OUTPUT) {
123  MX mx = f.instruction_MX(k);
124  Dict info = mx.info();
125  casadi_int output_idx = info["ind"];
126  casadi_int offset = info["offset"];
127 
128  std::string output_name = onnx_output_name(f, output_idx);
129  std::string input_onnx_name = work_to_onnx[i[0]];
130 
131  if (output_segment_count[output_idx] > 1) {
132  // Multi-segment output (horzcat-like): each segment provides a CONTIGUOUS run of the
133  // output's column-major nonzero array (offset = nnz offset). Record the source value and
134  // its sparsity; reassemble by concatenating the segments' nonzero arrays below.
135  output_segment_values[output_idx][offset] = input_onnx_name;
136  output_segment_sparsity[output_idx][offset] = mx.dep(0).sparsity();
137  } else {
138  // Single-segment output. A reshape can fold into the output (no OP_RESHAPE
139  // instruction): the source shape then differs from the declared output shape. A
140  // folded reshape densifies on import, so when the output pattern is non-dense, route
141  // the assembly through a temp and restore the exact pattern natively from the seed
142  // (no overlay).
143  MX dep = mx.dep(0);
144  Sparsity out_sp = f.sparsity_out(output_idx);
145  auto add_node = [graph]() -> onnx::NodeProto* { return graph->add_node(); };
146  bool folded = (dep.size1() != out_sp.size1() || dep.size2() != out_sp.size2());
147  if (folded && !out_sp.is_dense()) {
148  std::string tmp = "out_pre_" + std::to_string(k);
149  emit_output_node(graph, input_onnx_name, dep.size1(), dep.size2(), out_sp,
150  tmp, "out_rs_" + std::to_string(k));
151  emit_sparsity_restore(add_node, tmp,
152  Sparsity::dense(out_sp.size1(), out_sp.size2()), out_sp,
153  "outr_" + std::to_string(k), output_name);
154  } else {
155  emit_output_node(graph, input_onnx_name, dep.size1(), dep.size2(), out_sp,
156  output_name, "out_rs_" + std::to_string(k));
157  }
158  }
159  continue;
160  }
161 
162  if (process_operation(graph, f, op, k, i, o, work_to_onnx, node_output)) continue;
163 
164  // Operations not handled by process_operation
165  if (op == OP_CALL) {
166  Function called_func = f.instruction_MX(k).which_function();
167  if (is_map_function(called_func)) {
168  export_map(graph, called_func, i, o, work_to_onnx, node_output + "_out");
169  } else if (is_reduce_map_function(called_func)) {
170  export_reduce_map(graph, called_func, i, o, work_to_onnx, node_output + "_out");
171  } else if (is_if_else_function(called_func)) {
172  export_if(graph, called_func, i, o, work_to_onnx, node_output + "_out");
173  } else {
174  assert_not_control_flow(called_func);
175  export_call(graph, called_func, i, o, work_to_onnx, node_output + "_out");
176  }
177  continue;
178  }
179 
180  // Unknown/unsupported operation
181  casadi_error("ONNX export: unsupported operation code " +
182  std::to_string(op) + " at instruction " + std::to_string(k) +
183  ". The CasADi Function contains operations that cannot be exported to ONNX.");
184  }
185 
186  // Reassemble multi-segment outputs pseudo-dense. The block structure is one of:
187  // - horzcat (all blocks full-height, size1==R): columns concatenated -> a single ONNX Concat.
188  // Transpose-rep: CasADi horzcat (axis 1) is ONNX axis 0 (matches OP_HORZCAT dispatch).
189  // - vertcat (all blocks full-width, size2==C): rows concatenated -> a single ONNX Concat on
190  // the other axis. Transpose-rep: CasADi vertcat (axis 0) is ONNX axis 1
191  // (matches OP_VERTCAT).
192  // - diagcat (neither, genuine block-diagonal): Pad each DENSE block to (R,C) at its
193  // offset, Sum.
194  // The assembly imports as a DENSE block; when the output pattern is non-dense, restore it
195  // natively from the seed (no overlay) by routing the assembly through a temp.
196  auto seg_add_node = [graph]() -> onnx::NodeProto* { return graph->add_node(); };
197  for (const auto& kv : output_segment_values) {
198  casadi_int output_idx = kv.first;
199  const auto& segments = kv.second; // offset -> onnx_name, sorted by offset
200  const auto& seg_sp = output_segment_sparsity[output_idx];
201  casadi_int R = f.size1_out(output_idx), C = f.size2_out(output_idx);
202 
203  std::vector<std::string> names;
204  std::vector<casadi_int> brs, bcs;
205  bool all_full_height = true, all_full_width = true;
206  for (const auto& seg : segments) {
207  const Sparsity& sp = seg_sp.at(seg.first);
208  names.push_back(seg.second);
209  brs.push_back(sp.size1()); bcs.push_back(sp.size2());
210  if (sp.size1() != R) all_full_height = false; // not horzcat
211  if (sp.size2() != C) all_full_width = false; // not vertcat
212  }
213  Sparsity out_sp = f.sparsity_out(output_idx);
214  std::string oname = onnx_output_name(f, output_idx);
215  std::string uniq = "oseg" + std::to_string(output_idx);
216 
217  // Helper: write the assembly directly to oname when dense, else to a temp + restore.
218  auto finish = [&](const std::function<void(const std::string&)>& emit) -> void {
219  if (out_sp.is_dense()) {
220  emit(oname);
221  } else {
222  std::string tmp = oname + "_pre";
223  emit(tmp);
224  emit_sparsity_restore(seg_add_node, tmp, Sparsity::dense(R, C), out_sp,
225  uniq + "r", oname);
226  }
227  };
228 
229  if (all_full_height) {
230  // horzcat -> ONNX Concat axis 0
231  finish([&](const std::string& dst) {
232  onnx::NodeProto* cc = seg_add_node();
233  cc->set_op_type("Concat");
234  for (const auto& n : names) cc->add_input(n);
235  cc->add_output(dst);
236  add_int_attribute(cc, "axis", 0);
237  });
238  } else if (all_full_width) {
239  // vertcat -> ONNX Concat axis 1
240  finish([&](const std::string& dst) {
241  onnx::NodeProto* cc = seg_add_node();
242  cc->set_op_type("Concat");
243  for (const auto& n : names) cc->add_input(n);
244  cc->add_output(dst);
245  add_int_attribute(cc, "axis", 1);
246  });
247  } else {
248  // genuine block-diagonal (diagcat) -> Pad each block to (R,C) at its offset, then Sum
249  std::vector<casadi_int> row_off, col_off;
250  casadi_int ro = 0, co = 0;
251  for (casadi_int b = 0; b < static_cast<casadi_int>(names.size()); ++b) {
252  row_off.push_back(ro); col_off.push_back(co);
253  ro += brs[b]; co += bcs[b];
254  }
255  finish([&](const std::string& dst) {
256  emit_blockdiag(seg_add_node, names, row_off, col_off, brs, bcs, R, C, dst, uniq);
257  });
258  }
259  }
260 
261  // Fuse trailing rename Identities: a node of the form Identity(src) -> oname, where oname is a
262  // declared graph output and src is an internal node output used nowhere else, is a pure rename.
263  // Make the producing node write straight to oname and drop the Identity. Conservative: only
264  // when src is produced by exactly one node and consumed by exactly this Identity (so we never
265  // collapse
266  // an output that aliases an input, or a value feeding two outputs / another consumer).
267  {
268  std::set<std::string> output_names;
269  for (casadi_int i = 0; i < f.n_out(); ++i) output_names.insert(onnx_output_name(f, i));
270  // How many places consume each tensor name (as a node input).
271  std::map<std::string, int> consumers;
272  for (const auto& nd : graph->node())
273  for (const auto& in : nd.input()) consumers[in]++;
274  // Which node produces each tensor name, and is the name produced more than once.
275  std::map<std::string, int> producers;
276  for (const auto& nd : graph->node())
277  for (const auto& o : nd.output()) producers[o]++;
278 
279  auto* nodes = graph->mutable_node();
280  // Plan rewrites: src -> oname for fusable trailing Identities.
281  std::map<std::string, std::string> rename; // src -> oname
282  std::set<int> drop; // node indices (Identity) to remove
283  for (int n = 0; n < nodes->size(); ++n) {
284  const onnx::NodeProto& nd = nodes->Get(n);
285  if (nd.op_type() != "Identity" || nd.input_size() != 1 || nd.output_size() != 1) continue;
286  const std::string& src = nd.input(0);
287  const std::string& dst = nd.output(0);
288  if (!output_names.count(dst)) continue; // only fuse into graph outputs
289  if (output_names.count(src)) continue; // src is itself an output (e.g. aliased)
290  if (producers[src] != 1) continue; // src must be produced by a single node
291  if (consumers[src] != 1) continue; // and consumed only by this Identity
292  if (rename.count(src) || drop.count(n)) continue; // src already claimed by another output
293  rename[src] = dst;
294  drop.insert(n);
295  }
296  if (!drop.empty()) {
297  // Apply: rewrite producer outputs, then rebuild node list without the dropped Identities.
298  for (int n = 0; n < nodes->size(); ++n) {
299  if (drop.count(n)) continue;
300  onnx::NodeProto* nd = nodes->Mutable(n);
301  for (int o = 0; o < nd->output_size(); ++o) {
302  auto it = rename.find(nd->output(o));
303  if (it != rename.end()) nd->set_output(o, it->second);
304  }
305  }
306  google::protobuf::RepeatedPtrField<onnx::NodeProto> kept;
307  for (int n = 0; n < nodes->size(); ++n)
308  if (!drop.count(n)) kept.Add()->CopyFrom(nodes->Get(n));
309  nodes->Swap(&kept);
310  }
311  }
312 
313  // Add graph outputs (for main graph, use empty prefix to use function's output names)
314  add_graph_outputs(graph, f, "");
315 
316  // An output not produced by any node (e.g. an all-structural-zero output, whose MXFunction has
317  // no instructions) needs an explicit zero Constant so both ORT and the importer have a tensor.
318  // A non-dense pattern is planted directly as a sparse_value (all-zero) Constant -- the seed
319  // that imports to exactly that pattern; a dense output gets a plain dense zero Constant.
320  {
321  std::set<std::string> produced;
322  for (const auto& nd : graph->node())
323  for (const auto& o : nd.output()) produced.insert(o);
324  auto add_node = [graph]() -> onnx::NodeProto* { return graph->add_node(); };
325  for (casadi_int i = 0; i < f.n_out(); ++i) {
326  std::string oname = onnx_output_name(f, i);
327  if (!produced.count(oname)) {
328  const Sparsity& sp = f.sparsity_out(i);
329  if (sp.is_dense()) {
330  add_real_constant(add_node, oname,
331  std::vector<double>(sp.size1() * sp.size2(), 0.0),
332  {sp.size2(), sp.size1()});
333  } else {
334  add_sparse_constant(add_node, oname, DM(sp, 0.0));
335  }
336  }
337  }
338  }
339 
340  // Input sparsity seed: the ONNX graph value-flow is dense, so a non-dense CasADi input pattern
341  // is planted as a standard sparse_initializer (all-zero values) sharing the input name -- an
342  // optional input with a sparse zero default, readable by any ONNX tool. Import picks it up as a
343  // sparse MX and re-propagates sparsity natively. OUTPUT patterns need no overlay: each op
344  // restores its own output sparsity from seeds (emit_sparsity_restore), so they recover for
345  // free.
346  for (casadi_int i = 0; i < f.n_in(); ++i) {
347  if (!f.sparsity_in(i).is_dense()) {
348  fill_sparse_tensor(graph->add_sparse_initializer(), onnx_input_name(f, i),
349  DM(f.sparsity_in(i), 0.0));
350  }
351  }
352 
353  // If we exported any functions, add the casadi domain opset_import
354  if (!exported_functions_.empty()) {
355  onnx::OperatorSetIdProto* casadi_opset = model_.add_opset_import();
356  casadi_opset->set_domain("casadi");
357  casadi_opset->set_version(1);
358 
359  if (verbose_) {
360  uout() << " Exported " << exported_functions_.size()
361  << " function(s) to casadi domain" << std::endl;
362  }
363  }
364 
365  has_model_ = true;
366 
367  if (verbose_) {
368  uout() << "Converted CasADi Function to ONNX model: " << f.name() << std::endl;
369  uout() << " Instructions processed: " << n_instr << std::endl;
370  uout() << " ONNX nodes created: " << graph->node_size() << std::endl;
371  }
372  }
373 
374  // ========== Control Flow Support ==========
375 
376  bool Onnx::is_if_else_function(const Function& f) const {
377  return f.class_name() == "Switch";
378  }
379 
380  // map and mapaccum both have class_name "Map"/"OmpMap"; the name distinguishes them.
381  bool Onnx::is_mapaccum_function(const Function& f) const {
382  std::string class_name = f.class_name();
383  if (class_name != "Map" && class_name != "OmpMap") return false;
384  std::string fname = f.name();
385  return fname.find("mapaccum") != std::string::npos ||
386  fname.find("accum") != std::string::npos;
387  }
388 
389  bool Onnx::is_map_function(const Function& f) const {
390  std::string class_name = f.class_name();
391  if (class_name != "Map" && class_name != "OmpMap") return false;
392  std::string fname = f.name();
393  return fname.find("mapaccum") == std::string::npos &&
394  fname.find("accum") == std::string::npos;
395  }
396 
397  bool Onnx::is_reduce_map_function(const Function& f) const {
398  // A reduce-map is an MXFunction wrapping a single MapSum; spot the MapSum sub-function
399  for (const std::string& nm : f.get_function()) {
400  if (f.get_function(nm).class_name() == "MapSum") return true;
401  }
402  return false;
403  }
404 
405  void Onnx::assert_not_control_flow(const Function& called_func) const {
406  casadi_assert(!is_mapaccum_function(called_func),
407  "ONNX export: mapaccum (Loop) is not supported.");
408  }
409 
410  // Prefix the names a subgraph *defines* (graph inputs, initializers, node outputs) so they
411  // can't collide with the enclosing graph. References to names defined elsewhere are left
412  // untouched, which preserves an If branch's implicit captures of outer-scope tensors.
413  static void prefix_graph_names(onnx::GraphProto* g, const std::string& prefix) {
414  std::set<std::string> defined;
415  for (int i = 0; i < g->input_size(); ++i) defined.insert(g->input(i).name());
416  for (int i = 0; i < g->initializer_size(); ++i) defined.insert(g->initializer(i).name());
417  for (int n = 0; n < g->node_size(); ++n) {
418  for (int j = 0; j < g->node(n).output_size(); ++j) defined.insert(g->node(n).output(j));
419  }
420  for (int n = 0; n < g->node_size(); ++n) {
421  onnx::NodeProto* nd = g->mutable_node(n);
422  for (int i = 0; i < nd->input_size(); ++i) {
423  if (defined.count(nd->input(i))) nd->set_input(i, prefix + nd->input(i));
424  }
425  for (int i = 0; i < nd->output_size(); ++i) nd->set_output(i, prefix + nd->output(i));
426  }
427  for (int i = 0; i < g->input_size(); ++i) {
428  g->mutable_input(i)->set_name(prefix + g->input(i).name());
429  }
430  for (int i = 0; i < g->output_size(); ++i) {
431  g->mutable_output(i)->set_name(prefix + g->output(i).name());
432  }
433  }
434 
435  template<typename Container>
436  void Onnx::emit_reshape(Container* container, const std::string& data,
437  const std::vector<casadi_int>& shape,
438  const std::string& output, const std::string& shape_name) {
439  onnx::NodeProto* sc = container->add_node();
440  sc->set_op_type("Constant");
441  sc->add_output(shape_name);
442  onnx::AttributeProto* attr = sc->add_attribute();
443  attr->set_name("value");
444  attr->set_type(onnx::AttributeProto::TENSOR);
445  onnx::TensorProto* t = attr->mutable_t();
446  t->set_data_type(onnx::TensorProto::INT64);
447  t->add_dims(static_cast<casadi_int>(shape.size()));
448  for (casadi_int v : shape) t->add_int64_data(v);
449  onnx::NodeProto* r = container->add_node();
450  r->set_op_type("Reshape");
451  r->add_input(data);
452  r->add_input(shape_name);
453  r->add_output(output);
454  }
455 
456  template<typename Container>
457  void Onnx::colmajor_reshape_into(Container* container, const std::string& data,
458  const std::vector<casadi_int>& dims,
459  const std::string& output, const std::string& uniq) {
460  // Transpose-rep: a CasADi column-major reshape to `dims` is a plain row-major Reshape of the
461  // stored (already transposed) tensor to the REVERSED dims -- no Transpose nodes.
462  std::vector<casadi_int> rev(dims.rbegin(), dims.rend());
463  emit_reshape(container, data, rev, output, uniq + "_s");
464  }
465 
466  template<typename Container>
467  void Onnx::emit_output_node(Container* container, const std::string& data,
468  casadi_int src_rows, casadi_int src_cols, const Sparsity& out_sp,
469  const std::string& output, const std::string& uniq) {
470  // A reshape folded into the output shows up as a source/output shape mismatch
471  if (src_rows != out_sp.size1() || src_cols != out_sp.size2()) {
472  colmajor_reshape_into(container, data, {out_sp.size1(), out_sp.size2()}, output, uniq);
473  } else {
474  onnx::NodeProto* id = container->add_node();
475  id->set_op_type("Identity");
476  id->add_input(data);
477  id->add_output(output);
478  }
479  }
480 
481  onnx::GraphProto Onnx::build_scan_body(const Function& base) {
482  // Scan slices the iteration axis off the 3-D inputs, so the body runs the base on a
483  // clean 2-D (rows, c) block per iteration -- no rank fixing needed.
484  onnx::GraphProto body;
485  body.set_name(base.name() + "_scan_body");
486  std::map<casadi_int, std::string> work_to_onnx;
487 
488  for (casadi_int j = 0; j < base.n_in(); ++j) {
489  onnx::ValueInfoProto* vi = body.add_input();
490  vi->set_name("body_in_" + std::to_string(j));
491  set_real_tensor_type(vi, base.sparsity_in(j));
492  }
493 
494  casadi_int n_instr = base.n_instructions();
495  for (casadi_int k = 0; k < n_instr; ++k) {
496  casadi_int op = base.instruction_id(k);
497  std::vector<casadi_int> o = base.instruction_output(k);
498  std::vector<casadi_int> i_vec = base.instruction_input(k);
499  std::string node_output = "n" + std::to_string(k);
500 
501  if (op == OP_INPUT) {
502  work_to_onnx[o[0]] = "body_in_" + std::to_string(i_vec[0]);
503  continue;
504  }
505 
506  if (op == OP_OUTPUT) {
507  std::string out_name = "body_out_" + std::to_string(o[0]);
508  MX dep = base.instruction_MX(k).dep(0);
509  Sparsity out_sp = base.sparsity_out(o[0]);
510  emit_output_node(&body, work_to_onnx[i_vec[0]], dep.size1(), dep.size2(), out_sp,
511  out_name, "body_out_rs_" + std::to_string(k));
512  onnx::ValueInfoProto* vi = body.add_output();
513  vi->set_name(out_name);
514  set_real_tensor_type(vi, out_sp);
515  continue;
516  }
517 
518  if (op == OP_CALL) {
519  Function called = base.instruction_MX(k).which_function();
520  if (is_map_function(called)) {
521  export_map(&body, called, i_vec, o, work_to_onnx, node_output + "_out");
522  } else if (is_reduce_map_function(called)) {
523  export_reduce_map(&body, called, i_vec, o, work_to_onnx, node_output + "_out");
524  } else if (is_if_else_function(called)) {
525  export_if(&body, called, i_vec, o, work_to_onnx, node_output + "_out");
526  } else {
527  assert_not_control_flow(called);
528  export_call(&body, called, i_vec, o, work_to_onnx, node_output + "_out");
529  }
530  continue;
531  }
532 
533  if (process_operation(&body, base, op, k, i_vec, o, work_to_onnx, node_output)) continue;
534 
535  casadi_error("ONNX export: unsupported operation code " + std::to_string(op) +
536  " in Map body of '" + base.name() + "'");
537  }
538  return body;
539  }
540 
541  template<typename Container>
542  void Onnx::export_map(Container* container, const Function& map_fn,
543  const std::vector<casadi_int>& i_vec,
544  const std::vector<casadi_int>& o,
545  std::map<casadi_int, std::string>& work_to_onnx,
546  const std::string& out_prefix) {
547  Function base = map_fn.get_function(map_fn.get_function().at(0));
548  casadi_int n = map_fn.size2_out(0) / base.size2_out(0);
549 
550  // Only the plain Map is handled: every input repeated n times, every output concatenated.
551  // reduce_in (broadcast inputs) / reduce_out (summed outputs) are not yet supported.
552  for (casadi_int j = 0; j < base.n_in(); ++j) {
553  casadi_assert(map_fn.size2_in(j) == n * base.size2_in(j),
554  "ONNX export: Map with non-repeated (reduce_in) inputs is not yet supported.");
555  }
556  for (casadi_int j = 0; j < base.n_out(); ++j) {
557  casadi_assert(map_fn.size2_out(j) == n * base.size2_out(j),
558  "ONNX export: Map with reduced (reduce_out) outputs is not yet supported.");
559  }
560 
561  // Transpose-rep: the stored input is (n*c, rows); lift to 3-D (n, c, rows) so Scan slices the
562  // leading n-axis into 2-D (c, rows) blocks (each the stored transpose of a (rows, c) block).
563  std::vector<std::string> scan_inputs;
564  for (casadi_int j = 0; j < base.n_in(); ++j) {
565  std::string lifted = out_prefix + "_in" + std::to_string(j);
566  emit_reshape(container, work_to_onnx[i_vec[j]],
567  {n, base.size2_in(j), base.size1_in(j)}, lifted,
568  out_prefix + "_insh" + std::to_string(j));
569  scan_inputs.push_back(lifted);
570  }
571 
572  onnx::NodeProto* scan = container->add_node();
573  scan->set_op_type("Scan");
574  for (const std::string& s : scan_inputs) scan->add_input(s);
575  std::vector<std::string> scan_outputs;
576  for (casadi_int j = 0; j < base.n_out(); ++j) {
577  std::string so = out_prefix + "_s" + std::to_string(j);
578  scan->add_output(so);
579  scan_outputs.push_back(so);
580  }
581  add_int_attribute(scan, "num_scan_inputs", base.n_in());
582  add_ints_attribute(scan, "scan_input_axes", std::vector<casadi_int>(base.n_in(), 0));
583  add_ints_attribute(scan, "scan_output_axes", std::vector<casadi_int>(base.n_out(), 0));
584 
585  onnx::GraphProto body = build_scan_body(base);
586  prefix_graph_names(&body, out_prefix + "_b_"); // keep body names out of the outer scope
587 
588  onnx::AttributeProto* body_attr = scan->add_attribute();
589  body_attr->set_name("body");
590  body_attr->set_type(onnx::AttributeProto::GRAPH);
591  *body_attr->mutable_g() = body;
592 
593  // Transpose-rep: stacked scan output is (n, c, rows); flatten to (n*c, rows) = stored output
594  for (casadi_int j = 0; j < base.n_out(); ++j) {
595  std::string out = out_prefix + std::to_string(j);
596  emit_reshape(container, scan_outputs[j],
597  {n * base.size2_out(j), base.size1_out(j)}, out,
598  out_prefix + "_outsh" + std::to_string(j));
599  work_to_onnx[o[j]] = out;
600  }
601  }
602 
603  onnx::GraphProto Onnx::build_reduce_scan_body(const Function& base,
604  const std::vector<bool>& reduce_in, const std::vector<bool>& reduce_out,
605  const std::vector<std::string>& capture_names) {
606  // The body calls the base once; reduce_in args are captured from the outer scope,
607  // reduce_out results are accumulated into state variables (ONNX needs states declared first).
608  onnx::GraphProto body;
609  body.set_name(base.name() + "_redscan_body");
610  std::map<casadi_int, std::string> w;
611 
612  for (casadi_int j = 0; j < base.n_out(); ++j) {
613  if (!reduce_out[j]) continue;
614  onnx::ValueInfoProto* vi = body.add_input();
615  vi->set_name("acc_in_" + std::to_string(j));
616  set_real_tensor_type(vi, base.sparsity_out(j));
617  }
618 
619  std::vector<std::string> args(base.n_in());
620  for (casadi_int j = 0; j < base.n_in(); ++j) {
621  if (reduce_in[j]) {
622  args[j] = capture_names[j]; // implicit outer-scope capture
623  } else {
624  args[j] = "scan_in_" + std::to_string(j);
625  onnx::ValueInfoProto* vi = body.add_input();
626  vi->set_name(args[j]);
627  set_real_tensor_type(vi, base.sparsity_in(j));
628  }
629  }
630 
631  std::vector<casadi_int> ii(base.n_in()), oo(base.n_out());
632  for (casadi_int j = 0; j < base.n_in(); ++j) { ii[j] = j; w[j] = args[j]; }
633  for (casadi_int j = 0; j < base.n_out(); ++j) oo[j] = base.n_in() + j;
634  export_call(&body, base, ii, oo, w, "bcall_"); // base outputs land in w[oo[j]]
635 
636  for (casadi_int j = 0; j < base.n_out(); ++j) {
637  if (!reduce_out[j]) continue;
638  std::string out = "acc_out_" + std::to_string(j);
639  create_binary_node(&body, "Add", "acc_in_" + std::to_string(j), w[oo[j]], out);
640  onnx::ValueInfoProto* vi = body.add_output();
641  vi->set_name(out);
642  set_real_tensor_type(vi, base.sparsity_out(j));
643  }
644  for (casadi_int j = 0; j < base.n_out(); ++j) {
645  if (reduce_out[j]) continue;
646  std::string out = "yscan_" + std::to_string(j);
647  create_unary_node(&body, "Identity", w[oo[j]], out);
648  onnx::ValueInfoProto* vi = body.add_output();
649  vi->set_name(out);
650  set_real_tensor_type(vi, base.sparsity_out(j));
651  }
652  return body;
653  }
654 
655  template<typename Container>
656  void Onnx::export_reduce_map(Container* container, const Function& wrapper,
657  const std::vector<casadi_int>& i_vec,
658  const std::vector<casadi_int>& o,
659  std::map<casadi_int, std::string>& work_to_onnx,
660  const std::string& out_prefix) {
661  // Unwrap the MXFunction(wrapper) -> MapSum -> base function
662  Function mapsum;
663  for (const std::string& nm : wrapper.get_function()) {
664  if (wrapper.get_function(nm).class_name() == "MapSum") {
665  mapsum = wrapper.get_function(nm);
666  break;
667  }
668  }
669  Function base = mapsum.get_function(mapsum.get_function().at(0));
670 
671  // Repeated I/O is n*base wide; reduce_in/reduce_out keep base width
672  std::vector<bool> reduce_in(base.n_in()), reduce_out(base.n_out());
673  for (casadi_int j = 0; j < base.n_in(); ++j)
674  reduce_in[j] = wrapper.size2_in(j) == base.size2_in(j);
675  for (casadi_int j = 0; j < base.n_out(); ++j)
676  reduce_out[j] = wrapper.size2_out(j) == base.size2_out(j);
677  casadi_int n = 0;
678  for (casadi_int j = 0; j < base.n_in(); ++j)
679  if (!reduce_in[j]) { n = wrapper.size2_in(j) / base.size2_in(j); break; }
680  if (n == 0) for (casadi_int j = 0; j < base.n_out(); ++j)
681  if (!reduce_out[j]) { n = wrapper.size2_out(j) / base.size2_out(j); break; }
682  casadi_assert(n > 0, "ONNX export: reduce-map with no repeated inputs or outputs.");
683 
684  AddNodeFn add_node = [container]() -> onnx::NodeProto* { return container->add_node(); };
685 
686  // Alias each reduce_in input so the body captures a uniquely-named outer tensor
687  std::vector<std::string> capture_names(base.n_in());
688  for (casadi_int j = 0; j < base.n_in(); ++j) {
689  if (!reduce_in[j]) continue;
690  capture_names[j] = out_prefix + "_cap" + std::to_string(j);
691  create_unary_node(add_node, "Identity", work_to_onnx[i_vec[j]], capture_names[j]);
692  }
693 
694  // Lift each repeated input (rows, n*c) -> 3-D (rows, n, c) so Scan slices the n-axis
695  std::vector<std::string> scan_inputs;
696  for (casadi_int j = 0; j < base.n_in(); ++j) {
697  if (reduce_in[j]) continue;
698  std::string lifted = out_prefix + "_in" + std::to_string(j);
699  emit_reshape(container, work_to_onnx[i_vec[j]],
700  {n, base.size2_in(j), base.size1_in(j)}, lifted,
701  out_prefix + "_insh" + std::to_string(j));
702  scan_inputs.push_back(lifted);
703  }
704 
705  // Zero initial accumulators for each reduce_out output (transpose-rep shape (c,r))
706  std::vector<std::string> acc_inits;
707  for (casadi_int j = 0; j < base.n_out(); ++j) {
708  if (!reduce_out[j]) continue;
709  std::string nm = out_prefix + "_acc0_" + std::to_string(j);
710  std::vector<double> zeros(base.size1_out(j) * base.size2_out(j), 0.0);
711  add_real_constant(add_node, nm, zeros, {base.size2_out(j), base.size1_out(j)});
712  acc_inits.push_back(nm);
713  }
714 
715  onnx::NodeProto* scan = add_node();
716  scan->set_op_type("Scan");
717  for (const std::string& s : acc_inits) scan->add_input(s);
718  for (const std::string& s : scan_inputs) scan->add_input(s);
719 
720  // Scan outputs: state accumulators (reduce_out) first, then concatenated scan outputs
721  std::vector<std::string> state_out, scan_out;
722  for (casadi_int j = 0; j < base.n_out(); ++j)
723  if (reduce_out[j]) {
724  state_out.push_back(out_prefix + "_acc" + std::to_string(j));
725  scan->add_output(state_out.back());
726  }
727  for (casadi_int j = 0; j < base.n_out(); ++j)
728  if (!reduce_out[j]) {
729  scan_out.push_back(out_prefix + "_s" + std::to_string(j));
730  scan->add_output(scan_out.back());
731  }
732 
733  add_int_attribute(scan, "num_scan_inputs", static_cast<casadi_int>(scan_inputs.size()));
734  add_ints_attribute(scan, "scan_input_axes", std::vector<casadi_int>(scan_inputs.size(), 0));
735  add_ints_attribute(scan, "scan_output_axes", std::vector<casadi_int>(scan_out.size(), 0));
736 
737  onnx::GraphProto body = build_reduce_scan_body(base, reduce_in, reduce_out, capture_names);
738  prefix_graph_names(&body, out_prefix + "_b_"); // captures stay unprefixed (defined outside)
739  onnx::AttributeProto* battr = scan->add_attribute();
740  battr->set_name("body");
741  battr->set_type(onnx::AttributeProto::GRAPH);
742  *battr->mutable_g() = body;
743 
744  // reduce_out -> accumulator output directly; repeated -> flatten (rows, n, c) -> (rows, n*c)
745  casadi_int si = 0, ci = 0;
746  for (casadi_int j = 0; j < base.n_out(); ++j) {
747  if (reduce_out[j]) {
748  work_to_onnx[o[j]] = state_out[si++];
749  } else {
750  std::string out = out_prefix + std::to_string(j);
751  emit_reshape(container, scan_out[ci++], {n * base.size2_out(j), base.size1_out(j)}, out,
752  out_prefix + "_outsh" + std::to_string(j));
753  work_to_onnx[o[j]] = out;
754  }
755  }
756  }
757 
758  onnx::GraphProto Onnx::build_if_branch(const Function& f,
759  const std::vector<std::string>& arg_names, const std::string& prefix) {
760  // An ONNX If branch has no formal inputs: it captures the outer arg tensors by name.
761  onnx::GraphProto g;
762  g.set_name(f.name() + "_branch");
763  std::map<casadi_int, std::string> work_to_onnx;
764 
765  casadi_int n_instr = f.n_instructions();
766  for (casadi_int k = 0; k < n_instr; ++k) {
767  casadi_int op = f.instruction_id(k);
768  std::vector<casadi_int> o = f.instruction_output(k);
769  std::vector<casadi_int> i_vec = f.instruction_input(k);
770  std::string node_output = "n" + std::to_string(k);
771 
772  if (op == OP_INPUT) {
773  work_to_onnx[o[0]] = arg_names.at(i_vec[0]); // capture the outer tensor
774  continue;
775  }
776  if (op == OP_OUTPUT) {
777  std::string out_name = "branch_out_" + std::to_string(o[0]);
778  MX dep = f.instruction_MX(k).dep(0);
779  Sparsity out_sp = f.sparsity_out(o[0]);
780  emit_output_node(&g, work_to_onnx[i_vec[0]], dep.size1(), dep.size2(), out_sp,
781  out_name, "branch_out_rs_" + std::to_string(k));
782  onnx::ValueInfoProto* vi = g.add_output();
783  vi->set_name(out_name);
784  set_real_tensor_type(vi, out_sp);
785  continue;
786  }
787  if (op == OP_CALL) {
788  Function called = f.instruction_MX(k).which_function();
789  if (is_map_function(called)) {
790  export_map(&g, called, i_vec, o, work_to_onnx, node_output + "_out");
791  } else if (is_reduce_map_function(called)) {
792  export_reduce_map(&g, called, i_vec, o, work_to_onnx, node_output + "_out");
793  } else if (is_if_else_function(called)) {
794  export_if(&g, called, i_vec, o, work_to_onnx, node_output + "_out");
795  } else {
796  assert_not_control_flow(called);
797  export_call(&g, called, i_vec, o, work_to_onnx, node_output + "_out");
798  }
799  continue;
800  }
801  if (process_operation(&g, f, op, k, i_vec, o, work_to_onnx, node_output)) continue;
802 
803  casadi_error("ONNX export: unsupported operation code " + std::to_string(op) +
804  " in if_else branch of '" + f.name() + "'");
805  }
806  prefix_graph_names(&g, prefix); // unique internal names; captures of arg_names are kept
807  return g;
808  }
809 
810  template<typename Container>
811  void Onnx::export_if(Container* container, const Function& switch_fn,
812  const std::vector<casadi_int>& i_vec,
813  const std::vector<casadi_int>& o,
814  std::map<casadi_int, std::string>& work_to_onnx,
815  const std::string& out_prefix) {
816  // CasADi Switch: index 0 selects f[0], otherwise the default. if_else has a single case,
817  // so f[0] is the else-branch and f_def the then-branch. ONNX If is strictly 2-way.
818  Dict info = switch_fn.info();
819  std::vector<Function> cases = info.at("f");
820  Function f_then = info.at("f_def");
821  casadi_assert(cases.size() == 1,
822  "ONNX export: only 2-way if_else is supported (got a multi-case Switch).");
823  Function f_else = cases[0];
824 
825  // i_vec[0] is the condition; i_vec[1..] are the data arguments captured by both branches.
826  // Alias each through an Identity with a unique name so a branch's internal "n<k>" names
827  // can never coincide with a captured outer tensor name.
828  std::vector<std::string> arg_names;
829  for (size_t j = 1; j < i_vec.size(); ++j) {
830  std::string alias = out_prefix + "_arg" + std::to_string(j - 1);
831  onnx::NodeProto* id = container->add_node();
832  id->set_op_type("Identity");
833  id->add_input(work_to_onnx[i_vec[j]]);
834  id->add_output(alias);
835  arg_names.push_back(alias);
836  }
837 
838  std::string cond = out_prefix + "_cond";
839  onnx::NodeProto* cast = container->add_node();
840  cast->set_op_type("Cast");
841  cast->add_input(work_to_onnx[i_vec[0]]);
842  cast->add_output(cond);
843  add_int_attribute(cast, "to", onnx::TensorProto::BOOL);
844 
845  onnx::NodeProto* if_node = container->add_node();
846  if_node->set_op_type("If");
847  if_node->add_input(cond);
848  for (size_t j = 0; j < o.size(); ++j) {
849  std::string out = out_prefix + std::to_string(j);
850  if_node->add_output(out);
851  work_to_onnx[o[j]] = out;
852  }
853  onnx::AttributeProto* then_attr = if_node->add_attribute();
854  then_attr->set_name("then_branch");
855  then_attr->set_type(onnx::AttributeProto::GRAPH);
856  *then_attr->mutable_g() = build_if_branch(f_then, arg_names, out_prefix + "_t_");
857 
858  onnx::AttributeProto* else_attr = if_node->add_attribute();
859  else_attr->set_name("else_branch");
860  else_attr->set_type(onnx::AttributeProto::GRAPH);
861  *else_attr->mutable_g() = build_if_branch(f_else, arg_names, out_prefix + "_e_");
862  }
863 
864  template<typename Container>
865  void Onnx::export_call(Container* container, const Function& called_func,
866  const std::vector<casadi_int>& i_vec,
867  const std::vector<casadi_int>& o,
868  std::map<casadi_int, std::string>& work_to_onnx,
869  const std::string& out_prefix) {
870  std::string func_name = called_func.name();
871  std::string domain = "casadi";
872 
873  // Export the called function as a local FunctionProto the first time we see it
874  if (!exported_functions_.count(func_name)) {
875  onnx::FunctionProto* func_proto = function_to_function_proto(called_func, domain);
876  *model_.add_functions() = *func_proto;
877  delete func_proto;
878  exported_functions_.insert(func_name);
879  }
880 
881  onnx::NodeProto* call_node = container->add_node();
882  call_node->set_op_type(func_name);
883  call_node->set_domain(domain);
884  for (casadi_int idx : i_vec) call_node->add_input(work_to_onnx[idx]);
885  for (size_t j = 0; j < o.size(); ++j) {
886  std::string out = out_prefix + std::to_string(j);
887  call_node->add_output(out);
888  work_to_onnx[o[j]] = out;
889  }
890  }
891 
892  onnx::FunctionProto* Onnx::function_to_function_proto(
893  const Function& f,
894  const std::string& domain) {
895 
896  // Create a new FunctionProto (guarded so a casadi_assert/error mid-build frees it)
897  std::unique_ptr<onnx::FunctionProto> func_guard(new onnx::FunctionProto());
898  onnx::FunctionProto* func = func_guard.get();
899  func->set_name(f.name());
900  func->set_domain(domain);
901 
902  // Add opset imports for the function (required by ONNX Runtime)
903  // Default ONNX opset
904  onnx::OperatorSetIdProto* opset = func->add_opset_import();
905  opset->set_domain(""); // Empty string = default ONNX domain
906  opset->set_version(16);
907  // CasADi domain for nested function calls
908  onnx::OperatorSetIdProto* casadi_opset = func->add_opset_import();
909  casadi_opset->set_domain("casadi");
910  casadi_opset->set_version(1);
911 
912  if (verbose_) {
913  uout() << " Converting Function '" << f.name() << "' to ONNX FunctionProto"
914  << " (domain: " << domain << ")" << std::endl;
915  uout() << " Inputs: " << f.n_in() << ", Outputs: " << f.n_out()
916  << ", Instructions: " << f.n_instructions() << std::endl;
917  }
918 
919  // Add function inputs (parameter names)
920  for (casadi_int i = 0; i < f.n_in(); ++i) {
921  func->add_input(onnx_input_name(f, i));
922  }
923 
924  // Add function outputs (parameter names)
925  for (casadi_int i = 0; i < f.n_out(); ++i) {
926  func->add_output(onnx_output_name(f, i));
927  }
928 
929  // Map from work vector index to ONNX node name
930  std::map<casadi_int, std::string> work_to_onnx;
931 
932  // Multi-segment I/O (a slot read/written in several pieces, as mapaccum produces) needs
933  // the Gather/Concat machinery the top-level graph has; reject it here instead of emitting
934  // a wrong FunctionProto that reuses an output name across segments.
935  std::set<casadi_int> written_outputs;
936 
937  // Process instructions and add nodes to the function
938  casadi_int n_instr = f.n_instructions();
939  for (casadi_int k = 0; k < n_instr; ++k) {
940  casadi_int op = f.instruction_id(k);
941  std::vector<casadi_int> o = f.instruction_output(k);
942  std::vector<casadi_int> i_vec = f.instruction_input(k);
943 
944  std::string node_output = "n" + std::to_string(k);
945 
946  // OP_INPUT maps a work slot directly to the function parameter name (no node)
947  if (op == OP_INPUT) {
948  casadi_assert(f.instruction_MX(k).numel() == f.numel_in(i_vec[0]),
949  "ONNX export: nested functions with multi-segment inputs (e.g. mapaccum) "
950  "are not supported.");
951  work_to_onnx[o[0]] = onnx_input_name(f, i_vec[0]);
952  continue;
953  }
954 
955  // OP_OUTPUT connects the internal result to the declared output name (a folded reshape
956  // shows up as a source/output shape mismatch)
957  if (op == OP_OUTPUT) {
958  casadi_assert(written_outputs.insert(o[0]).second,
959  "ONNX export: nested functions with multi-segment outputs (e.g. mapaccum) "
960  "are not supported.");
961  MX dep = f.instruction_MX(k).dep(0);
962  Sparsity out_sp = f.sparsity_out(o[0]);
963  emit_output_node(func, work_to_onnx[i_vec[0]], dep.size1(), dep.size2(), out_sp,
964  onnx_output_name(f, o[0]), "out_rs_" + std::to_string(k));
965  continue;
966  }
967 
968  // OP_CALL - recursive function call
969  if (op == OP_CALL) {
970  Function called_func = f.instruction_MX(k).which_function();
971  std::string out_prefix = "call_" + called_func.name() + "_" + std::to_string(k) + "_out";
972  if (is_map_function(called_func)) {
973  export_map(func, called_func, i_vec, o, work_to_onnx, out_prefix);
974  } else if (is_reduce_map_function(called_func)) {
975  export_reduce_map(func, called_func, i_vec, o, work_to_onnx, out_prefix);
976  } else if (is_if_else_function(called_func)) {
977  export_if(func, called_func, i_vec, o, work_to_onnx, out_prefix);
978  } else {
979  assert_not_control_flow(called_func);
980  export_call(func, called_func, i_vec, o, work_to_onnx, out_prefix);
981  }
982  continue;
983  }
984 
985  // All other operations use shared implementation via callback
986  if (process_operation([&]() { return func->add_node(); },
987  f, op, k, i_vec, o, work_to_onnx, node_output)) {
988  continue;
989  }
990 
991  // Unsupported operation
992  casadi_error("ONNX export: Unsupported operation code " + std::to_string(op) +
993  " in function '" + f.name() + "' at instruction " + std::to_string(k));
994  }
995 
996  if (verbose_) {
997  uout() << " Created " << func->node_size() << " nodes in FunctionProto" << std::endl;
998  }
999 
1000  return func_guard.release();
1001  }
1002 
1003 } // namespace casadi
static const char * version()
Obtain the version number of CasADi.
Definition: casadi_meta.cpp:30
Function object.
Definition: function.hpp:60
casadi_int n_instructions() const
Number of instruction in the algorithm (SXFunction/MXFunction)
Definition: function.cpp:1906
casadi_int size2_out(casadi_int ind) const
Get output dimension.
Definition: function.cpp:991
const Sparsity & sparsity_out(casadi_int ind) const
Get sparsity of a given output.
Definition: function.cpp:1183
const std::vector< std::string > & name_in() const
Get input scheme.
Definition: function.cpp:1113
const std::string & name() const
Name of the function.
Definition: function.cpp:1504
std::vector< casadi_int > instruction_input(casadi_int k) const
Locations in the work vector for the inputs of the instruction.
Definition: function.cpp:1938
const Sparsity & sparsity_in(casadi_int ind) const
Get sparsity of a given input.
Definition: function.cpp:1167
std::vector< casadi_int > instruction_output(casadi_int k) const
Location in the work vector for the output of the instruction.
Definition: function.cpp:1954
MX instruction_MX(casadi_int k) const
Get the MX node corresponding to an instruction (MXFunction)
Definition: function.cpp:1914
casadi_int n_out() const
Get the number of function outputs.
Definition: function.cpp:975
casadi_int n_in() const
Get the number of function inputs.
Definition: function.cpp:971
casadi_int size1_out(casadi_int ind) const
Get output dimension.
Definition: function.cpp:987
casadi_int instruction_id(casadi_int k) const
Identifier index of the instruction (SXFunction/MXFunction)
Definition: function.cpp:1930
const std::vector< std::string > & name_out() const
Get output scheme.
Definition: function.cpp:1117
casadi_int size2() const
Get the second dimension (i.e. number of columns)
casadi_int size1() const
Get the first dimension (i.e. number of rows)
bool verbose_
Verbose – for debugging.
MX - Matrix expression.
Definition: mx.hpp:92
const Sparsity & sparsity() const
Get the sparsity pattern.
Definition: mx.cpp:612
Function which_function() const
Get function - only valid when is_call() is true.
Definition: mx.cpp:807
Dict info() const
Definition: mx.cpp:855
MX dep(casadi_int ch=0) const
Get the nth dependency as MX.
Definition: mx.cpp:783
std::set< std::string > exported_functions_
Track which functions have been exported as FunctionProto.
Definition: onnx_model.hpp:119
std::string class_name() const override
Readable name of the internal class.
Definition: onnx_model.hpp:72
onnx::ModelProto model_
ONNX model protocol buffer.
Definition: onnx_model.hpp:110
bool has_model_
Whether a model has been loaded.
Definition: onnx_model.hpp:116
void load(const Function &f)
Load a CasADi Function and convert to the ONNX representation.
Definition: onnx_export.cpp:77
std::string class_name() const
Get class name.
General sparsity class.
Definition: sparsity.hpp:106
casadi_int size1() const
Get the number of rows.
Definition: sparsity.cpp:124
static Sparsity dense(casadi_int nrow, casadi_int ncol=1)
Create a dense rectangular sparsity pattern *.
Definition: sparsity.cpp:1028
casadi_int size2() const
Get the number of columns.
Definition: sparsity.cpp:128
bool is_dense() const
Is dense?
Definition: sparsity.cpp:273
The casadi namespace.
Definition: archiver.cpp:28
onnx::NodeProto * create_unary_node(AddNodeFn add_node, const std::string &op_type, const std::string &input, const std::string &output)
Create unary operation ONNX node (callback-based)
GenericType::Dict Dict
C++ equivalent of Python's dict or MATLAB's struct.
std::string onnx_output_name(const Function &f, casadi_int i)
Definition: onnx_export.cpp:39
onnx::NodeProto * create_binary_node(AddNodeFn add_node, const std::string &op_type, const std::string &input1, const std::string &input2, const std::string &output)
Create binary operation ONNX node (callback-based)
void add_int_attribute(onnx::NodeProto *node, const std::string &name, casadi_int value)
Add an integer attribute (e.g. axis) to a node.
static void prefix_graph_names(onnx::GraphProto *g, const std::string &prefix)
std::function< onnx::NodeProto *()> AddNodeFn
Callback type for adding nodes to a container (GraphProto or FunctionProto)
Definition: onnx_model.hpp:48
Matrix< double > DM
Definition: dm_fwd.hpp:33
std::string onnx_input_name(const Function &f, casadi_int i)
Function input/output name, or a generated fallback when unnamed.
Definition: onnx_export.cpp:34
std::ostream & uout()
@ OP_OUTPUT
Definition: calculus.hpp:82
@ OP_INPUT
Definition: calculus.hpp:82
@ OP_CALL
Definition: calculus.hpp:88
void add_ints_attribute(onnx::NodeProto *node, const std::string &name, const std::vector< casadi_int > &values)
Add an integer-list attribute (e.g. scan_input_axes) to a node.