onnx_operations.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 
29 namespace casadi {
30 
31  // ========== Operation Mapping ==========
32  // Single source of truth for simple CasADi <-> ONNX ops; shared by export and import.
33  // (OpMapping is declared in onnx_model.hpp.)
34 
35  static const OpMapping op_map[] = {
36  // Unary operations
37  {OP_SIN, "Sin", 1},
38  {OP_COS, "Cos", 1},
39  {OP_TAN, "Tan", 1},
40  {OP_ASIN, "Asin", 1},
41  {OP_ACOS, "Acos", 1},
42  {OP_ATAN, "Atan", 1},
43  {OP_SINH, "Sinh", 1},
44  {OP_COSH, "Cosh", 1},
45  {OP_TANH, "Tanh", 1},
46  {OP_ASINH, "Asinh", 1},
47  {OP_ACOSH, "Acosh", 1},
48  {OP_ATANH, "Atanh", 1},
49  {OP_EXP, "Exp", 1},
50  {OP_LOG, "Log", 1},
51  {OP_SQRT, "Sqrt", 1},
52  {OP_NEG, "Neg", 1},
53  {OP_FABS, "Abs", 1},
54  {OP_CEIL, "Ceil", 1},
55  {OP_FLOOR, "Floor", 1},
56  {OP_SIGN, "Sign", 1},
57  {OP_ERF, "Erf", 1},
58  {OP_INV, "Reciprocal", 1},
59  {OP_TRANSPOSE, "Transpose", 1},
60  {OP_NORM1, "ReduceL1", 1},
61  {OP_NORMF, "ReduceL2", 1}, // NORMF first: import uses norm_fro (vectors and matrices)
62  {OP_NORM2, "ReduceL2", 1},
63  {OP_MMIN, "ReduceMin", 1},
64  {OP_MMAX, "ReduceMax", 1},
65  // Binary operations
66  {OP_ADD, "Add", 2},
67  {OP_SUB, "Sub", 2},
68  {OP_MUL, "Mul", 2},
69  {OP_DIV, "Div", 2},
70  {OP_POW, "Pow", 2},
71  {OP_CONSTPOW, "Pow", 2},
72  };
73 
74  // Lookup by CasADi opcode (for export)
75  const OpMapping* get_op_mapping(casadi_int op) {
76  for (const auto& entry : op_map) {
77  if (entry.casadi_op == op) return &entry;
78  }
79  return nullptr;
80  }
81 
82  // Lookup by ONNX name (for import)
83  const OpMapping* get_op_mapping_by_name(const std::string& onnx_name) {
84  for (const auto& entry : op_map) {
85  if (entry.onnx_name == onnx_name) return &entry;
86  }
87  return nullptr;
88  }
89 
90  // ========== Export Helpers ==========
91 
92  // Create a Constant node with a tensor value. Setting the attribute type explicitly is
93  // required for ONNX Runtime compatibility.
94  onnx::TensorProto* create_constant_tensor(
95  AddNodeFn add_node,
96  const std::string& output_name,
97  onnx::TensorProto::DataType data_type) {
98  onnx::NodeProto* node = add_node();
99  node->set_op_type("Constant");
100  node->add_output(output_name);
101  onnx::AttributeProto* attr = node->add_attribute();
102  attr->set_name("value");
103  attr->set_type(onnx::AttributeProto::TENSOR); // Required for ONNX Runtime
104  onnx::TensorProto* tensor = attr->mutable_t();
105  tensor->set_data_type(data_type);
106  return tensor;
107  }
108 
109  // Add a Constant node holding an INT64 tensor (default shape: 1-D of data.size())
110  void add_int_constant(AddNodeFn add_node, const std::string& name,
111  const std::vector<casadi_int>& data,
112  std::vector<casadi_int> dims = {}) {
113  onnx::TensorProto* t = create_constant_tensor(add_node, name, onnx::TensorProto::INT64);
114  if (dims.empty()) dims = {static_cast<casadi_int>(data.size())};
115  for (casadi_int d : dims) t->add_dims(d);
116  for (casadi_int v : data) t->add_int64_data(v);
117  }
118 
119  // Add a Constant node holding a real tensor in the configured type (empty dims => scalar)
120  void Onnx::add_real_constant(AddNodeFn add_node, const std::string& name,
121  const std::vector<double>& data,
122  const std::vector<casadi_int>& dims) {
123  onnx::TensorProto* t = create_constant_tensor(add_node, name, real_type());
124  for (casadi_int d : dims) t->add_dims(d);
125  if (real_type() == onnx::TensorProto::FLOAT) {
126  for (double v : data) t->add_float_data(static_cast<float>(v));
127  } else {
128  for (double v : data) t->add_double_data(v);
129  }
130  }
131 
132  // Add a Constant node holding a SPARSE real tensor (ONNX sparse_value attribute, COO format).
133  void Onnx::add_sparse_constant(AddNodeFn add_node, const std::string& name, const DM& dm) {
134  onnx::NodeProto* node = add_node();
135  node->set_op_type("Constant");
136  node->add_output(name);
137  onnx::AttributeProto* attr = node->add_attribute();
138  attr->set_name("sparse_value");
139  attr->set_type(onnx::AttributeProto::SPARSE_TENSOR);
140  fill_sparse_tensor(attr->mutable_sparse_tensor(), name, dm);
141  }
142 
143  void Onnx::fill_sparse_tensor(onnx::SparseTensorProto* st, const std::string& name,
144  const DM& dm) const {
145  // Transpose-rep: store V^T (shape c x r). V's NATIVE column-major nonzero order is exactly
146  // V^T's ascending row-major order, so emit V's (col,row) coordinates directly -- no sort,
147  // no transpose.
148  st->add_dims(dm.size2());
149  st->add_dims(dm.size1());
150  casadi_int nnz = dm.nnz();
151  std::vector<casadi_int> row = dm.sparsity().get_row(); // V's rows (column-major order)
152  std::vector<casadi_int> col = dm.sparsity().get_col(); // V's cols (column-major order)
153  const std::vector<double>& vals = dm.nonzeros();
154  // values [NNZ]
155  onnx::TensorProto* vt = st->mutable_values();
156  vt->set_name(name); // a SparseTensorProto's name is its values' name
157  vt->set_data_type(real_type());
158  vt->add_dims(nnz);
159  if (real_type() == onnx::TensorProto::FLOAT) {
160  for (double v : vals) vt->add_float_data(static_cast<float>(v));
161  } else {
162  for (double v : vals) vt->add_double_data(v);
163  }
164  // indices [NNZ, 2] = V^T coordinates (col, row), 0-based, ascending row-major of V^T
165  onnx::TensorProto* it = st->mutable_indices();
166  it->set_data_type(onnx::TensorProto::INT64);
167  it->add_dims(nnz);
168  it->add_dims(2);
169  for (casadi_int k = 0; k < nnz; ++k) {
170  it->add_int64_data(col[k]); // row in V^T (= column of V)
171  it->add_int64_data(row[k]); // column in V^T (= row of V)
172  }
173  }
174 
175  // Add an integer attribute (e.g. axis) to a node
176  void add_int_attribute(onnx::NodeProto* node, const std::string& name, casadi_int value) {
177  onnx::AttributeProto* attr = node->add_attribute();
178  attr->set_name(name);
179  attr->set_type(onnx::AttributeProto::INT);
180  attr->set_i(value);
181  }
182 
183  // Add an integer-list attribute (e.g. scan_input_axes) to a node
184  void add_ints_attribute(onnx::NodeProto* node, const std::string& name,
185  const std::vector<casadi_int>& values) {
186  onnx::AttributeProto* attr = node->add_attribute();
187  attr->set_name(name);
188  attr->set_type(onnx::AttributeProto::INTS);
189  for (casadi_int v : values) attr->add_ints(v);
190  }
191 
192  // GraphProto convenience wrapper for add_int_constant
193  void add_int_constant(onnx::GraphProto* graph, const std::string& name,
194  const std::vector<casadi_int>& data, std::vector<casadi_int> dims) {
195  add_int_constant([graph]() { return graph->add_node(); }, name, data, dims);
196  }
197 
198  // Gather(data, indices) along an axis -> output
199  onnx::NodeProto* create_gather_node(AddNodeFn add_node, const std::string& data,
200  const std::string& indices, const std::string& output,
201  casadi_int axis = 0) {
202  onnx::NodeProto* node = add_node();
203  node->set_op_type("Gather");
204  node->add_input(data);
205  node->add_input(indices);
206  node->add_output(output);
207  add_int_attribute(node, "axis", axis);
208  return node;
209  }
210 
211  // Slice(data) on the given axes from starts to ends (step 1), emitting the index constants
212  onnx::NodeProto* create_slice_node(AddNodeFn add_node, const std::string& uniq,
213  const std::string& data, const std::vector<casadi_int>& starts,
214  const std::vector<casadi_int>& ends, const std::vector<casadi_int>& axes,
215  const std::vector<casadi_int>& steps, const std::string& output) {
216  add_int_constant(add_node, uniq + "_starts", starts);
217  add_int_constant(add_node, uniq + "_ends", ends);
218  add_int_constant(add_node, uniq + "_axes", axes);
219  // before the Slice node
220  if (!steps.empty()) add_int_constant(add_node, uniq + "_steps", steps);
221  onnx::NodeProto* node = add_node();
222  node->set_op_type("Slice");
223  node->add_input(data);
224  node->add_input(uniq + "_starts");
225  node->add_input(uniq + "_ends");
226  node->add_input(uniq + "_axes");
227  if (!steps.empty()) node->add_input(uniq + "_steps"); // omit -> ONNX defaults steps to 1
228  node->add_output(output);
229  return node;
230  }
231 
232  // Reshape `data` (which, under the transpose-representation invariant, holds the transpose of a
233  // CasADi value) to a CasADi target shape `dims`. Because every ONNX tensor stores its CasADi
234  // value's column-major bytes with the shape declared reversed, a CasADi column-major reshape to
235  // `dims` is just a plain row-major Reshape to the REVERSED dims -- no Transpose nodes needed.
236  void emit_colmajor_reshape(AddNodeFn add_node, const std::string& data,
237  const std::vector<casadi_int>& dims, const std::string& output,
238  const std::string& uniq) {
239  std::vector<casadi_int> rev(dims.rbegin(), dims.rend());
240  add_int_constant(add_node, uniq + "_s", rev);
241  create_unary_node(add_node, "Reshape", data, output)->add_input(uniq + "_s");
242  }
243 
244  // Callback-based implementations (main logic)
245  onnx::NodeProto* create_binary_node(
246  AddNodeFn add_node,
247  const std::string& op_type,
248  const std::string& input1,
249  const std::string& input2,
250  const std::string& output) {
251  onnx::NodeProto* node = add_node();
252  node->set_op_type(op_type);
253  node->add_input(input1);
254  node->add_input(input2);
255  node->add_output(output);
256  return node;
257  }
258 
259  onnx::NodeProto* create_unary_node(
260  AddNodeFn add_node,
261  const std::string& op_type,
262  const std::string& input,
263  const std::string& output) {
264  onnx::NodeProto* node = add_node();
265  node->set_op_type(op_type);
266  node->add_input(input);
267  node->add_output(output);
268  return node;
269  }
270 
271  // Cast(input) -> output of the given ONNX data type
272  onnx::NodeProto* create_cast_node(
273  AddNodeFn add_node,
274  const std::string& input,
275  const std::string& output,
276  onnx::TensorProto::DataType to_type) {
277  onnx::NodeProto* node = add_node();
278  node->set_op_type("Cast");
279  node->add_input(input);
280  node->add_output(output);
281  onnx::AttributeProto* attr = node->add_attribute();
282  attr->set_name("to");
283  attr->set_type(onnx::AttributeProto::INT);
284  attr->set_i(to_type);
285  return node;
286  }
287 
288  // Gemm: the CALLER intends CasADi-level output = (A or A')*(B or B') [+ C]. Under the
289  // transpose-rep invariant the stored output must be out^T = (A*B+C)^T = B^T*A^T + C^T, so we
290  // emit Gemm(stored_B, stored_A [, stored_C]) with the transA/transB flags swapped too.
291  // C empty -> 2-input form (alpha=beta=1).
292  onnx::NodeProto* create_gemm_node(
293  AddNodeFn add_node,
294  const std::string& A, const std::string& B, const std::string& C,
295  const std::string& output, bool transA = false, bool transB = false) {
296  onnx::NodeProto* node = add_node();
297  node->set_op_type("Gemm");
298  node->add_input(B);
299  node->add_input(A);
300  if (!C.empty()) node->add_input(C);
301  node->add_output(output);
302  if (transB) add_int_attribute(node, "transA", 1);
303  if (transA) add_int_attribute(node, "transB", 1);
304  return node;
305  }
306 
307  // Where(condition, if_true, if_false) -> output
308  onnx::NodeProto* create_where_node(
309  AddNodeFn add_node,
310  const std::string& cond,
311  const std::string& if_true,
312  const std::string& if_false,
313  const std::string& output) {
314  onnx::NodeProto* node = add_node();
315  node->set_op_type("Where");
316  node->add_input(cond);
317  node->add_input(if_true);
318  node->add_input(if_false);
319  node->add_output(output);
320  return node;
321  }
322 
323  // GraphProto convenience wrappers
324  onnx::NodeProto* create_binary_node(
325  onnx::GraphProto* graph,
326  const std::string& op_type,
327  const std::string& input1,
328  const std::string& input2,
329  const std::string& output) {
330  return create_binary_node([graph]() { return graph->add_node(); },
331  op_type, input1, input2, output);
332  }
333 
334  onnx::NodeProto* create_unary_node(
335  onnx::GraphProto* graph,
336  const std::string& op_type,
337  const std::string& input,
338  const std::string& output) {
339  return create_unary_node([graph]() { return graph->add_node(); },
340  op_type, input, output);
341  }
342 
343  // Emit the general "rearrange nonzeros between two sparsities" envelope (the poorest-form
344  // primitive shared by OP_GETNONZEROS's fallback and OP_PROJECT/densify). `idx[k]` = input
345  // nonzero index feeding output nonzero k, or -1 = fill-with-zero.
346  void Onnx::emit_nonzero_remap(AddNodeFn add_node, const std::string& data,
347  const Sparsity& sp_in, const Sparsity& out_sp,
348  std::vector<casadi_int> idx, const std::string& uniq,
349  const std::string& node_output) {
350  casadi_int numel_in = sp_in.size1() * sp_in.size2();
351  std::vector<casadi_int> loc = sp_in.find();
352  bool has_fill = false;
353  for (casadi_int& v : idx) {
354  if (v < 0) { v = numel_in; has_fill = true; } // -> the appended zero (below)
355  else
356  v = loc[v];
357  }
358 
359  // Flatten the input to a 1 x numel ROW vector (skip when it is already a row)
360  std::string flat = data;
361  if (sp_in.size2() != 1) {
362  flat = "gnz_flat_" + uniq;
363  emit_colmajor_reshape(add_node, data, {numel_in, 1}, flat, "gnz_flat_" + uniq);
364  }
365  // Zero-fill (-1) entries gather position `numel_in`: append a single 0 to the flat row.
366  if (has_fill) {
367  std::string zc = "gnz_fz_" + uniq, fl2 = "gnz_flz_" + uniq;
368  add_real_constant(add_node, zc, {0.0}, {1, 1});
369  onnx::NodeProto* cc = add_node();
370  cc->set_op_type("Concat");
371  cc->add_input(flat); cc->add_input(zc); cc->add_output(fl2);
372  add_int_attribute(cc, "axis", 1);
373  flat = fl2;
374  }
375 
376  // Gather the nnz_out values along axis 1 -> a (1, nnz_out) row.
377  casadi_int numel_out = out_sp.size1() * out_sp.size2();
378  bool dense_out = (static_cast<casadi_int>(idx.size()) == numel_out);
379  // A dense column-vector output is already in stored form -> write straight to the output.
380  std::string gathered = (dense_out && out_sp.size2() == 1) ? node_output : ("gnz_g_" + uniq);
381  add_int_constant(add_node, "indices_" + uniq, idx);
382  create_gather_node(add_node, flat, "indices_" + uniq, gathered, 1);
383 
384  if (dense_out) {
385  if (out_sp.size2() != 1) {
386  emit_colmajor_reshape(add_node, gathered, {out_sp.size1(), out_sp.size2()}, node_output,
387  "gnz_out_" + uniq);
388  }
389  } else {
390  // SPARSE output: scatter the gathered values into a dense (1, numel_out) zero frame at the
391  // output's dense column-major positions, then reshape to a DENSE (out_sp shape) value with
392  // exact numerics (zero outside out_sp). The pattern is then restored to out_sp natively via
393  // the seed recipe -- import re-propagates out_sp for free (no overlay).
394  std::vector<casadi_int> ofind = out_sp.find();
395  std::string zname = "gnz_zeros_" + uniq, fname = "gnz_ofind_" + uniq;
396  std::string scat = "gnz_scat_" + uniq;
397  add_real_constant(add_node, zname, std::vector<double>(numel_out, 0.0), {1, numel_out});
398  add_int_constant(add_node, fname, ofind, {1, static_cast<casadi_int>(ofind.size())});
399  onnx::NodeProto* sc = add_node();
400  sc->set_op_type("ScatterElements");
401  sc->add_input(zname); sc->add_input(fname); sc->add_input(gathered);
402  sc->add_output(scat);
403  add_int_attribute(sc, "axis", 1);
404  std::string dframe = "gnz_dense_" + uniq;
405  emit_colmajor_reshape(add_node, scat, {out_sp.size1(), out_sp.size2()}, dframe,
406  "gnz_out_" + uniq);
407  emit_sparsity_restore(add_node, dframe, Sparsity::dense(out_sp.size1(),
408  out_sp.size2()), out_sp, "gnzr_" + uniq, node_output);
409  }
410  }
411 
412  // Plant a sparsity seed so a dense ONNX value imports to exactly target_sp (see header).
413  std::string Onnx::emit_sparsity_restore(AddNodeFn add_node, const std::string& value,
414  const Sparsity& value_sp, const Sparsity& target_sp,
415  const std::string& uniq,
416  const std::string& final_output) {
417  // Already imports to the right pattern -> nothing to do (0-node passthrough).
418  if (value_sp == target_sp) return value;
419 
420  if (target_sp.is_dense()) {
421  // value is non-dense here (a dense value_sp of this shape would equal target_sp above).
422  // Add a dense real zero of target_sp's shape: dense + dense -> dense.
423  std::string zc = "sr_dz_" + uniq;
424  add_real_constant(add_node, zc,
425  std::vector<double>(target_sp.size1() * target_sp.size2(), 0.0),
426  {target_sp.size2(), target_sp.size1()});
427  create_binary_node(add_node, "Add", value, zc, final_output);
428  return final_output;
429  }
430 
431  // Non-dense target. Classify the value pattern against the target:
432  // - value_sp subset of target_sp: the value only DROPS entries vs target; a single
433  // Add(value, zeros_target) suffices, since pattern(value) U target = target.
434  // - value_sp superset of target_sp (e.g. dense -> sparse narrowing project): the value only
435  // carries EXTRA entries; a single Mul(value, ones_target) suffices, since
436  // pattern(value) ^ target = target.
437  // - neither: the value both adds and drops entries, so mask with ones_target (intersection)
438  // then union with zeros_target -- exactly project(value, target).
439  bool subset = value_sp.is_subset(target_sp);
440  bool superset = target_sp.is_subset(value_sp);
441  if (subset) {
442  std::string zeros = "sr_zeros_" + uniq;
443  add_sparse_constant(add_node, zeros, DM(target_sp, 0.0));
444  create_binary_node(add_node, "Add", value, zeros, final_output);
445  return final_output;
446  }
447  if (superset) {
448  std::string ones = "sr_ones_" + uniq;
449  add_sparse_constant(add_node, ones, DM(target_sp, 1.0));
450  create_binary_node(add_node, "Mul", value, ones, final_output);
451  return final_output;
452  }
453  // General: Mul(value, ones_target) then Add(_, zeros_target).
454  std::string ones = "sr_ones_" + uniq;
455  add_sparse_constant(add_node, ones, DM(target_sp, 1.0));
456  std::string mul = "sr_mul_" + uniq;
457  create_binary_node(add_node, "Mul", value, ones, mul);
458  std::string zeros = "sr_zeros_" + uniq;
459  add_sparse_constant(add_node, zeros, DM(target_sp, 0.0));
460  create_binary_node(add_node, "Add", mul, zeros, final_output);
461  return final_output;
462  }
463 
464  // Assemble a block matrix pseudo-dense: Pad each DENSE block to (R,C) at its offset, then Sum.
465  void Onnx::emit_blockdiag(AddNodeFn add_node, const std::vector<std::string>& names,
466  const std::vector<casadi_int>& row_off,
467  const std::vector<casadi_int>& col_off,
468  const std::vector<casadi_int>& br, const std::vector<casadi_int>& bc,
469  casadi_int R, casadi_int C, const std::string& output,
470  const std::string& uniq) {
471  std::vector<std::string> padded;
472  for (casadi_int b = 0; b < static_cast<casadi_int>(names.size()); ++b) {
473  // Stored block is (bc x br); stored output is (C x R). ONNX Pad's `pads` is
474  // [axis0_begin, axis1_begin, axis0_end, axis1_end] = [col_off, row_off, end_col, end_row].
475  std::string pn = uniq + "_pp" + std::to_string(b);
476  add_int_constant(add_node, pn, {col_off[b], row_off[b],
477  C - col_off[b] - bc[b], R - row_off[b] - br[b]});
478  std::string pd = uniq + "_pad" + std::to_string(b);
479  onnx::NodeProto* pad = add_node();
480  pad->set_op_type("Pad");
481  pad->add_input(names[b]);
482  pad->add_input(pn); // mode defaults to "constant", value to 0
483  pad->add_output(pd);
484  padded.push_back(pd);
485  }
486  if (padded.size() == 1) {
487  create_unary_node(add_node, "Identity", padded[0], output);
488  } else {
489  onnx::NodeProto* sum = add_node();
490  sum->set_op_type("Sum");
491  for (const auto& p : padded) sum->add_input(p);
492  sum->add_output(output);
493  }
494  }
495 
496  // Callback-based process_operation (main implementation)
497  bool Onnx::process_operation(
498  AddNodeFn add_node,
499  const Function& f,
500  casadi_int op,
501  casadi_int k,
502  const std::vector<casadi_int>& i_vec,
503  const std::vector<casadi_int>& o_vec,
504  std::map<casadi_int, std::string>& work_to_onnx,
505  const std::string& node_output) {
506 
507  onnx::NodeProto* node = nullptr;
508 
509  // Simple ops via the centralized lookup table
510  const OpMapping* mapping = get_op_mapping(op);
511  if (mapping) {
512  if (mapping->arity == 1 && i_vec.size() >= 1 && o_vec.size() == 1) {
513  create_unary_node(add_node, mapping->onnx_name, work_to_onnx[i_vec[0]], node_output);
514  work_to_onnx[o_vec[0]] = node_output;
515  return true;
516  } else if (mapping->arity == 2 && i_vec.size() >= 2 && o_vec.size() == 1) {
517  create_binary_node(add_node, mapping->onnx_name, work_to_onnx[i_vec[0]],
518  work_to_onnx[i_vec[1]], node_output);
519  work_to_onnx[o_vec[0]] = node_output;
520  return true;
521  }
522  }
523 
524  // Everything else needs attributes, multiple nodes, or shape logic.
525  switch (op) {
526  case OP_INPUT: {
527  std::string input_name = onnx_input_name(f, i_vec[0]);
528 
529  // A sub-slice of a larger input has offset/numel narrower than the whole input.
530  MX mx_input = f.instruction_MX(k);
531  Dict info = mx_input.info();
532  casadi_int offset = info["offset"];
533  casadi_int input_numel = f.numel_in(i_vec[0]);
534  casadi_int output_numel = mx_input.numel();
535 
536  if (output_numel < input_numel) {
537  // Extract output_numel consecutive column-major elements starting at `offset`.
538  // Transpose-rep: flatten the stored input to a 1 x numel row, Gather along axis 1.
539  std::string uniq = std::to_string(k);
540  std::string flat = input_name;
541  if (f.size2_in(i_vec[0]) != 1) { // not already stored as a row
542  flat = "in_flat_" + uniq;
543  emit_colmajor_reshape(add_node, input_name, {input_numel, 1}, flat, "in_flat_" + uniq);
544  }
545  std::vector<casadi_int> idx;
546  for (casadi_int t = 0; t < output_numel; ++t) idx.push_back(offset + t);
547  std::string index_name = "input_idx_" + uniq;
548  add_int_constant(add_node, index_name, idx);
549  bool out_is_col = (mx_input.size2() == 1);
550  std::string gathered = out_is_col ? node_output : ("in_g_" + uniq);
551  create_gather_node(add_node, flat, index_name, gathered, 1);
552  if (!out_is_col) {
553  emit_colmajor_reshape(add_node, gathered, {mx_input.size1(), mx_input.size2()},
554  node_output, "in_out_" + uniq);
555  }
556  } else {
557  // Full input access (output size matches input size): no rename node -- downstream nodes
558  // reference the graph input tensor directly. If this input is returned DIRECTLY as an
559  // output, the OP_OUTPUT path emits the single Identity needed to give it the output name
560  // (a graph input cannot be renamed in place).
561  work_to_onnx[o_vec[0]] = input_name;
562  return true;
563  }
564  work_to_onnx[o_vec[0]] = node_output;
565  return true;
566  }
567 
568  case OP_OUTPUT: {
569  // Connect the work value to the output name; o_vec[0] is the output slot, not a work index
570  create_unary_node(add_node, "Identity", work_to_onnx[i_vec[0]],
571  onnx_output_name(f, o_vec[0]));
572  return true;
573  }
574 
575  case OP_CONST: {
576  DM dm_const = static_cast<DM>(f.instruction_MX(k));
577  if (dm_const.is_dense()) {
578  // Transpose-rep: the column-major bytes go in as-is, shape declared REVERSED (c,r).
579  std::vector<double> data(dm_const->begin(), dm_const->end());
580  add_real_constant(add_node, node_output, data, {dm_const.size2(), dm_const.size1()});
581  } else {
582  // Store the sparsity pattern faithfully as an ONNX sparse constant (COO)
583  add_sparse_constant(add_node, node_output, dm_const);
584  }
585  work_to_onnx[o_vec[0]] = node_output;
586  return true;
587  }
588 
589  case OP_MTIMES: {
590  // Fused multiply-add input[1]*input[2] + input[0] -> a single Gemm
591  create_gemm_node(add_node, work_to_onnx[i_vec[1]], work_to_onnx[i_vec[2]],
592  work_to_onnx[i_vec[0]], node_output);
593  work_to_onnx[o_vec[0]] = node_output;
594  return true;
595  }
596 
597  case OP_SQ: {
598  // x^2 = Mul(x, x)
599  create_binary_node(add_node, "Mul", work_to_onnx[i_vec[0]],
600  work_to_onnx[i_vec[0]], node_output);
601  work_to_onnx[o_vec[0]] = node_output;
602  return true;
603  }
604 
605  case OP_TWICE: {
606  // 2*x
607  std::string const_name = "const_2_" + std::to_string(k);
608  add_real_constant(add_node, const_name, {2.0});
609  create_binary_node(add_node, "Mul", const_name, work_to_onnx[i_vec[0]], node_output);
610  work_to_onnx[o_vec[0]] = node_output;
611  return true;
612  }
613 
614  case OP_DETERMINANT:
615  create_unary_node(add_node, "Det", work_to_onnx[i_vec[0]], node_output);
616  work_to_onnx[o_vec[0]] = node_output;
617  return true;
618 
619  case OP_LOGSUMEXP:
620  create_unary_node(add_node, "ReduceLogSumExp", work_to_onnx[i_vec[0]], node_output);
621  work_to_onnx[o_vec[0]] = node_output;
622  return true;
623 
624  case OP_LOG1P: {
625  // log(1 + x)
626  std::string one = "one_" + std::to_string(k), s = "log1p_" + std::to_string(k);
627  add_real_constant(add_node, one, {1.0});
628  create_binary_node(add_node, "Add", one, work_to_onnx[i_vec[0]], s);
629  create_unary_node(add_node, "Log", s, node_output);
630  work_to_onnx[o_vec[0]] = node_output;
631  return true;
632  }
633 
634  case OP_EXPM1: {
635  // exp(x) - 1
636  std::string e = "exp_" + std::to_string(k), one = "one_" + std::to_string(k);
637  create_unary_node(add_node, "Exp", work_to_onnx[i_vec[0]], e);
638  add_real_constant(add_node, one, {1.0});
639  create_binary_node(add_node, "Sub", e, one, node_output);
640  work_to_onnx[o_vec[0]] = node_output;
641  return true;
642  }
643 
644  case OP_HYPOT: {
645  // sqrt(x^2 + y^2)
646  std::string x2 = "hx_" + std::to_string(k), y2 = "hy_" + std::to_string(k);
647  std::string s = "hs_" + std::to_string(k);
648  create_binary_node(add_node, "Mul", work_to_onnx[i_vec[0]], work_to_onnx[i_vec[0]], x2);
649  create_binary_node(add_node, "Mul", work_to_onnx[i_vec[1]], work_to_onnx[i_vec[1]], y2);
650  create_binary_node(add_node, "Add", x2, y2, s);
651  create_unary_node(add_node, "Sqrt", s, node_output);
652  work_to_onnx[o_vec[0]] = node_output;
653  return true;
654  }
655 
656  // Pure pass-throughs (drop CasADi-internal markers)
657  case OP_ASSIGN:
658  case OP_LIFT:
659  case OP_ASSERTION: // attachAssert: output = dep(0); the condition (dep(1)) is a side guard
660  case OP_MONITOR: // monitor/printme-style debug passthrough: output = dep(0)
661  create_unary_node(add_node, "Identity", work_to_onnx[i_vec[0]], node_output);
662  work_to_onnx[o_vec[0]] = node_output;
663  return true;
664 
665  case OP_SOLVE: {
666  // ONNX has no native linear solver. tr selects A'*x = b and is reported for diagnostics.
667  MX mx_solve = f.instruction_MX(k);
668  Dict info = mx_solve.info();
669  bool tr = info["tr"];
670  casadi_error("ONNX export: OP_SOLVE (linear solver) is not supported. "
671  "ONNX does not provide native linear algebra solvers. "
672  "Consider using explicit matrix operations or iterative methods. "
673  "Transpose flag was: " + std::string(tr ? "true" : "false"));
674  return false;
675  }
676 
677  case OP_NORMINF: {
678  // Infinity norm: max(abs(x))
679  std::string abs_result = "abs_" + std::to_string(k);
680  create_unary_node(add_node, "Abs", work_to_onnx[i_vec[0]], abs_result);
681  create_unary_node(add_node, "ReduceMax", abs_result, node_output);
682  work_to_onnx[o_vec[0]] = node_output;
683  return true;
684  }
685 
686  // Comparisons output a bool tensor in ONNX; cast to double for the double-typed graph
687  case OP_LT: {
688  std::string b = "cmp_" + std::to_string(k);
689  create_binary_node(add_node, "Less", work_to_onnx[i_vec[0]], work_to_onnx[i_vec[1]], b);
690  create_cast_node(add_node, b, node_output, real_type());
691  work_to_onnx[o_vec[0]] = node_output;
692  return true;
693  }
694 
695  case OP_LE: {
696  std::string b = "cmp_" + std::to_string(k);
697  create_binary_node(add_node, "LessOrEqual", work_to_onnx[i_vec[0]],
698  work_to_onnx[i_vec[1]], b);
699  create_cast_node(add_node, b, node_output, real_type());
700  work_to_onnx[o_vec[0]] = node_output;
701  return true;
702  }
703 
704  case OP_EQ: {
705  std::string b = "cmp_" + std::to_string(k);
706  create_binary_node(add_node, "Equal", work_to_onnx[i_vec[0]], work_to_onnx[i_vec[1]], b);
707  create_cast_node(add_node, b, node_output, real_type());
708  work_to_onnx[o_vec[0]] = node_output;
709  return true;
710  }
711 
712  // Logic ops need bool operands: cast 0/1 doubles to bool, apply, cast back
713  case OP_NOT: {
714  std::string b = "bin_" + std::to_string(k);
715  create_cast_node(add_node, work_to_onnx[i_vec[0]], b, onnx::TensorProto::BOOL);
716  std::string r = "bres_" + std::to_string(k);
717  create_unary_node(add_node, "Not", b, r);
718  create_cast_node(add_node, r, node_output, real_type());
719  work_to_onnx[o_vec[0]] = node_output;
720  return true;
721  }
722 
723  case OP_AND:
724  case OP_OR: {
725  std::string b0 = "bin0_" + std::to_string(k), b1 = "bin1_" + std::to_string(k);
726  create_cast_node(add_node, work_to_onnx[i_vec[0]], b0, onnx::TensorProto::BOOL);
727  create_cast_node(add_node, work_to_onnx[i_vec[1]], b1, onnx::TensorProto::BOOL);
728  std::string r = "bres_" + std::to_string(k);
729  create_binary_node(add_node, op == OP_AND ? "And" : "Or", b0, b1, r);
730  create_cast_node(add_node, r, node_output, real_type());
731  work_to_onnx[o_vec[0]] = node_output;
732  return true;
733  }
734 
735  case OP_FMIN:
736  create_binary_node(add_node, "Min", work_to_onnx[i_vec[0]], work_to_onnx[i_vec[1]],
737  node_output);
738  work_to_onnx[o_vec[0]] = node_output;
739  return true;
740 
741  case OP_FMOD: {
742  node = add_node();
743  node->set_op_type("Mod");
744  node->add_input(work_to_onnx[i_vec[0]]);
745  node->add_input(work_to_onnx[i_vec[1]]);
746  node->add_output(node_output);
747  add_int_attribute(node, "fmod", 1); // fmod=1 required for floating point types
748  work_to_onnx[o_vec[0]] = node_output;
749  return true;
750  }
751 
752  case OP_COPYSIGN: {
753  // copysign(x, y) = sign(y) * abs(x)
754  std::string sign_result = "sign_" + std::to_string(k);
755  std::string abs_result = "abs_" + std::to_string(k);
756  create_unary_node(add_node, "Sign", work_to_onnx[i_vec[1]], sign_result);
757  create_unary_node(add_node, "Abs", work_to_onnx[i_vec[0]], abs_result);
758  create_binary_node(add_node, "Mul", sign_result, abs_result, node_output);
759  work_to_onnx[o_vec[0]] = node_output;
760  return true;
761  }
762 
763  case OP_FMAX:
764  create_binary_node(add_node, "Max", work_to_onnx[i_vec[0]], work_to_onnx[i_vec[1]],
765  node_output);
766  work_to_onnx[o_vec[0]] = node_output;
767  return true;
768 
769  case OP_NE: {
770  // Not equal: Not(Equal(...)), then cast the bool result to double
771  std::string equal_result = "equal_" + std::to_string(k);
772  create_binary_node(add_node, "Equal", work_to_onnx[i_vec[0]], work_to_onnx[i_vec[1]],
773  equal_result);
774  std::string r = "bres_" + std::to_string(k);
775  create_unary_node(add_node, "Not", equal_result, r);
776  create_cast_node(add_node, r, node_output, real_type());
777  work_to_onnx[o_vec[0]] = node_output;
778  return true;
779  }
780 
781  case OP_IF_ELSE_ZERO: {
782  // if (condition != 0) value else 0 => Where(bool(condition), value, 0)
783  std::string cond = "cond_" + std::to_string(k);
784  create_cast_node(add_node, work_to_onnx[i_vec[0]], cond, onnx::TensorProto::BOOL);
785  std::string zero_name = "const_0_" + std::to_string(k);
786  add_real_constant(add_node, zero_name, {0.0});
787  create_where_node(add_node, cond, work_to_onnx[i_vec[1]], zero_name, node_output);
788  work_to_onnx[o_vec[0]] = node_output;
789  return true;
790  }
791 
792  case OP_DOT: {
793  // dot(a, b) = ReduceSum(Mul(a, b))
794  std::string mul_result = "mul_" + std::to_string(k);
795  create_binary_node(add_node, "Mul", work_to_onnx[i_vec[0]], work_to_onnx[i_vec[1]],
796  mul_result);
797  create_unary_node(add_node, "ReduceSum", mul_result, node_output);
798  work_to_onnx[o_vec[0]] = node_output;
799  return true;
800  }
801 
802  case OP_BILIN: {
803  // Bilinear form input[1]' * input[0] * input[2]. Gemm(transA) avoids a separate
804  // Transpose (which ORT would fuse into a float64-less FusedMatMul).
805  std::string xa = "bilin_" + std::to_string(k);
806  create_gemm_node(add_node, work_to_onnx[i_vec[1]], work_to_onnx[i_vec[0]], "", xa, true);
807  // Transpose-rep: stored out = y_stored * xa_stored, so emit MatMul(y, xa)
808  create_binary_node(add_node, "MatMul", work_to_onnx[i_vec[2]], xa, node_output);
809  work_to_onnx[o_vec[0]] = node_output;
810  return true;
811  }
812 
813  case OP_RANK1: {
814  // Rank-1 update input[0] + input[1]*input[2]*input[3]'. Gemm(transB) forms x*y'
815  // without a separate Transpose; input[1] is a scalar alpha.
816  std::string xyt = "rank1_" + std::to_string(k);
817  create_gemm_node(add_node, work_to_onnx[i_vec[2]], work_to_onnx[i_vec[3]], "", xyt,
818  false, true);
819  std::string scaled = "rank1s_" + std::to_string(k);
820  create_binary_node(add_node, "Mul", work_to_onnx[i_vec[1]], xyt, scaled);
821  create_binary_node(add_node, "Add", work_to_onnx[i_vec[0]], scaled, node_output);
822  work_to_onnx[o_vec[0]] = node_output;
823  return true;
824  }
825 
826  case OP_EINSTEIN: {
827  // Tensor contraction C_c += A_a * B_b. deps [C, A, B], operands column-vec flattened.
828  // Reshape each operand (column-major) to its logical dims and contract with Einsum
829  // using the labels in natural order; reshape the result back to a column vector.
830  MX mx_e = f.instruction_MX(k);
831  Dict info = mx_e.info();
832  std::vector<casadi_int> la = info["a"], lb = info["b"], lc = info["c"];
833  std::vector<casadi_int> da = info["dim_a"], db = info["dim_b"], dc = info["dim_c"];
834  casadi_assert(da.size() <= 2 && db.size() <= 2 && dc.size() <= 2,
835  "ONNX export: einstein with >2-index operands is not supported.");
836 
837  // Assign a letter to each distinct label (natural order)
838  std::map<casadi_int, char> letter;
839  char nxt = 'a';
840  for (const std::vector<casadi_int>& labs : {la, lb, lc}) {
841  for (casadi_int L : labs) if (!letter.count(L)) letter[L] = nxt++;
842  }
843  std::string sa, sb, sc;
844  for (casadi_int L : la) sa += letter[L];
845  for (casadi_int L : lb) sb += letter[L];
846  for (casadi_int L : lc) sc += letter[L];
847  // Transpose-rep: emit_colmajor_reshape stores each operand with axes REVERSED (logical
848  // d0xd1 -> ONNX [d1,d0]). Reverse each subscript so the equation labels the stored axes
849  // correctly;
850  // the reversed-axis output then flattens column-major back into C's vector.
851  std::reverse(sa.begin(), sa.end());
852  std::reverse(sb.begin(), sb.end());
853  std::reverse(sc.begin(), sc.end());
854 
855  std::string a_re = "ein_a_" + std::to_string(k), b_re = "ein_b_" + std::to_string(k);
856  emit_colmajor_reshape(add_node, work_to_onnx[i_vec[1]], da, a_re, a_re);
857  emit_colmajor_reshape(add_node, work_to_onnx[i_vec[2]], db, b_re, b_re);
858 
859  std::string ein_out = "ein_o_" + std::to_string(k);
860  node = add_node();
861  node->set_op_type("Einsum");
862  node->add_input(a_re);
863  node->add_input(b_re);
864  node->add_output(ein_out);
865  onnx::AttributeProto* eq = node->add_attribute();
866  eq->set_name("equation");
867  eq->set_type(onnx::AttributeProto::STRING);
868  eq->set_s(sa + "," + sb + "->" + sc);
869 
870  // Flatten the contraction result back to a column vector and add the C accumulator
871  casadi_int prod_c = 1;
872  for (casadi_int d : dc) prod_c *= d;
873  std::string ein_vec = "ein_v_" + std::to_string(k);
874  emit_colmajor_reshape(add_node, ein_out, {prod_c, 1}, ein_vec, ein_vec);
875  create_binary_node(add_node, "Add", work_to_onnx[i_vec[0]], ein_vec, node_output);
876  work_to_onnx[o_vec[0]] = node_output;
877  return true;
878  }
879 
880  case OP_KRON: {
881  // Kronecker product kron(A, B), A is ra x ca, B is rb x cb, result (ra*rb) x (ca*cb).
882  // kron commutes with transpose: kron(A,B)^T == kron(A^T,B^T). The stored tensors are the
883  // transposes (At=[ca,ra], Bt=[cb,rb]) and the stored output is the result's transpose, so
884  // in
885  // stored space kron maps to kron of the stored operands with NO extra transposes. Emit
886  // A4 = Reshape(At, [ca,1,ra,1]); B4 = Reshape(Bt, [1,cb,1,rb])
887  // P = Mul(A4, B4) -> broadcast [ca,cb,ra,rb]
888  // R = Reshape(P, [ca*cb, ra*rb]) (= stored result)
889  // These are plain row-major Reshapes (insert/collapse singleton axes), so emit the shape
890  // constants directly (no axis reversal -> do NOT use emit_colmajor_reshape). The node group
891  // is tagged with name "kron<k>" so the importer reconstructs kron(A_imp,B_imp) directly
892  // (the 4-D intermediates cannot be represented as 2-D MX).
893  MX mx_kron = f.instruction_MX(k);
894  casadi_int ra = mx_kron.dep(0).size1(), ca = mx_kron.dep(0).size2();
895  casadi_int rb = mx_kron.dep(1).size1(), cb = mx_kron.dep(1).size2();
896  std::string uniq = std::to_string(k);
897  std::string tag = "kron" + uniq;
898 
899  // ONNX node names must be unique; give each kron node a distinct name sharing the
900  // "kron<k>_"
901  // prefix and a role suffix the importer dispatches on.
902  auto emit_reshape = [&](const std::string& data, const std::vector<casadi_int>& shape,
903  const std::string& out, const std::string& role) -> void {
904  add_int_constant(add_node, out + "_s", shape);
905  onnx::NodeProto* rn = create_unary_node(add_node, "Reshape", data, out);
906  rn->add_input(out + "_s");
907  rn->set_name(tag + "_" + role);
908  };
909 
910  std::string a4 = tag + "_A4", b4 = tag + "_B4", p = tag + "_P";
911  emit_reshape(work_to_onnx[i_vec[0]], {ca, 1, ra, 1}, a4, "A4");
912  emit_reshape(work_to_onnx[i_vec[1]], {1, cb, 1, rb}, b4, "B4");
913  onnx::NodeProto* mul = create_binary_node(add_node, "Mul", a4, b4, p);
914  mul->set_name(tag + "_P");
915  emit_reshape(p, {ca * cb, ra * rb}, node_output, "R");
916 
917  work_to_onnx[o_vec[0]] = node_output;
918  return true;
919  }
920 
921  // Tensor operations
922  case OP_RESHAPE: {
923  // ONNX Reshape is pseudo-dense; the importer densifies the operand (it must, to keep flat
924  // indices aligned), so a non-dense reshape result is restored to its CasADi pattern
925  // natively
926  // from the seed (no overlay).
927  auto sp = f.instruction_MX(k).sparsity();
928  if (sp.is_dense()) {
929  emit_colmajor_reshape(add_node, work_to_onnx[i_vec[0]], {sp.size1(), sp.size2()},
930  node_output, "rs_" + std::to_string(k));
931  } else {
932  std::string rs = "rs_d_" + std::to_string(k);
933  emit_colmajor_reshape(add_node, work_to_onnx[i_vec[0]], {sp.size1(), sp.size2()},
934  rs, "rs_" + std::to_string(k));
935  std::string r = emit_sparsity_restore(add_node, rs,
936  Sparsity::dense(sp.size1(), sp.size2()), sp,
937  "rsr_" + std::to_string(k), node_output);
938  work_to_onnx[o_vec[0]] = r;
939  return true;
940  }
941  work_to_onnx[o_vec[0]] = node_output;
942  return true;
943  }
944 
945  case OP_HORZCAT:
946  case OP_VERTCAT: {
947  // Transpose-rep: CasADi columns/rows are ONNX rows/columns, so axes swap:
948  // horzcat (CasADi axis 1) -> ONNX axis 0, vertcat (CasADi axis 0) -> ONNX axis 1.
949  node = add_node();
950  node->set_op_type("Concat");
951  for (casadi_int idx : i_vec) node->add_input(work_to_onnx[idx]);
952  node->add_output(node_output);
953  add_int_attribute(node, "axis", op == OP_HORZCAT ? 0 : 1);
954  work_to_onnx[o_vec[0]] = node_output;
955  return true;
956  }
957 
958  case OP_DIAGCAT: {
959  // Block-diagonal, pseudo-dense: Pad each DENSE block to the output shape at its diagonal
960  // offset, then Sum -- no flatten/gather (a triangular block is just a dense block with a
961  // structural zero; the output pattern is restored from the seed).
962  MX mx_dc = f.instruction_MX(k);
963  std::string uniq = std::to_string(k);
964  std::vector<std::string> names;
965  std::vector<casadi_int> row_off, col_off, brs, bcs;
966  casadi_int ro = 0, co = 0;
967  for (casadi_int j = 0; j < static_cast<casadi_int>(i_vec.size()); ++j) {
968  casadi_int br = mx_dc.dep(j).size1(), bc = mx_dc.dep(j).size2();
969  names.push_back(work_to_onnx[i_vec[j]]);
970  row_off.push_back(ro); col_off.push_back(co); brs.push_back(br); bcs.push_back(bc);
971  ro += br; co += bc; // diagonal placement
972  }
973  Sparsity dc_sp = mx_dc.sparsity();
974  if (dc_sp.is_dense()) {
975  emit_blockdiag(add_node, names, row_off, col_off, brs, bcs,
976  mx_dc.size1(), mx_dc.size2(), node_output, "dc" + uniq);
977  } else {
978  // The Pad+Sum assembly imports dense; restore the block-diagonal pattern from the seed.
979  std::string tmp = "dc_pre_" + uniq;
980  emit_blockdiag(add_node, names, row_off, col_off, brs, bcs,
981  mx_dc.size1(), mx_dc.size2(), tmp, "dc" + uniq);
982  std::string r = emit_sparsity_restore(add_node, tmp,
983  Sparsity::dense(mx_dc.size1(), mx_dc.size2()), dc_sp, "dcr_" + uniq,
984  node_output);
985  work_to_onnx[o_vec[0]] = r;
986  return true;
987  }
988  work_to_onnx[o_vec[0]] = node_output;
989  return true;
990  }
991 
992  case OP_HORZSPLIT:
993  case OP_VERTSPLIT: {
994  // Split the pseudo-dense tensor by COLUMN (horzsplit) / ROW (vertsplit) counts. The MX
995  // "offset" is a NONZERO offset, so dividing by a stride only yields column/row counts for a
996  // DENSE input; for a sparse input it is wrong (nonzeros are not uniform per column). The
997  // segment dimensions are the authoritative sizes -- read them straight off the split
998  // primitive's output Function. The importer splits the sparse seed natively, so each
999  // segment recovers its own pattern (no overlay).
1000  MX mx_split = f.instruction_MX(k);
1001  bool horz = (op == OP_HORZSPLIT);
1002  Function split_out = mx_split.info()["output"];
1003 
1004  std::vector<casadi_int> split_sizes;
1005  for (casadi_int j = 0; j < split_out.n_out(); ++j) {
1006  split_sizes.push_back(horz ? split_out.size2_out(j) : split_out.size1_out(j));
1007  }
1008 
1009  std::string split_sizes_name = "split_sizes_" + std::to_string(k);
1010  add_int_constant(add_node, split_sizes_name, split_sizes);
1011 
1012  node = add_node();
1013  node->set_op_type("Split");
1014  node->add_input(work_to_onnx[i_vec[0]]);
1015  node->add_input(split_sizes_name);
1016  // Transpose-rep: horzsplit (CasADi axis 1) -> ONNX axis 0, vertsplit -> ONNX axis 1
1017  add_int_attribute(node, "axis", horz ? 0 : 1);
1018 
1019  for (casadi_int j = 0; j < o_vec.size(); ++j) {
1020  std::string output_name = "n" + std::to_string(k) + "_out" + std::to_string(j);
1021  node->add_output(output_name);
1022  work_to_onnx[o_vec[j]] = output_name;
1023  }
1024  return true;
1025  }
1026 
1027  case OP_HORZREPMAT: {
1028  // repmat(x, 1, n) -> ONNX Tile with repeats [1, n]
1029  MX mx_repmat = f.instruction_MX(k);
1030  casadi_int n = mx_repmat.size2() / mx_repmat.dep(0).size2();
1031 
1032  std::string repeats_name = "repeats_" + std::to_string(k);
1033  add_int_constant(add_node, repeats_name, {n, 1}); // transpose-rep: tile over rows
1034 
1035  node = add_node();
1036  node->set_op_type("Tile");
1037  node->add_input(work_to_onnx[i_vec[0]]);
1038  node->add_input(repeats_name);
1039  node->add_output(node_output);
1040  work_to_onnx[o_vec[0]] = node_output;
1041  return true;
1042  }
1043 
1044  case OP_HORZREPSUM: {
1045  // repsum(x, 1, n): inverse of repmat - split [rows, cols*n] into n parts and sum
1046  MX mx_repsum = f.instruction_MX(k);
1047  casadi_int output_cols = mx_repsum.size2();
1048  casadi_int n = mx_repsum.dep(0).size2() / output_cols;
1049 
1050  std::string split_sizes_name = "split_sizes_" + std::to_string(k);
1051  add_int_constant(add_node, split_sizes_name, std::vector<casadi_int>(n, output_cols));
1052 
1053  node = add_node();
1054  node->set_op_type("Split");
1055  node->add_input(work_to_onnx[i_vec[0]]);
1056  node->add_input(split_sizes_name);
1057  add_int_attribute(node, "axis", 0); // transpose-rep: repmat tiled over rows
1058 
1059  // Sum all n parts in a single variadic Sum node
1060  onnx::NodeProto* sum_node = add_node();
1061  sum_node->set_op_type("Sum");
1062  for (casadi_int i = 0; i < n; ++i) {
1063  std::string out_name = "split_" + std::to_string(k) + "_" + std::to_string(i);
1064  node->add_output(out_name);
1065  sum_node->add_input(out_name);
1066  }
1067  sum_node->add_output(node_output);
1068  work_to_onnx[o_vec[0]] = node_output;
1069  return true;
1070  }
1071 
1072  case OP_GETNONZEROS: {
1073  // x.nz[indices], CasADi column-major. Transpose-rep: row-major-flatten the stored input
1074  // (its row-major buffer IS x's column-major order) to a 1 x numel ROW vector, Gather along
1075  // axis 1, reshape back. The nz indices address x's NONZERO array; map them to dense
1076  // column-major flat positions with find() (identity when x is dense).
1077  MX mx_getnonzeros = f.instruction_MX(k);
1078  Dict info = mx_getnonzeros.info();
1079  std::string data = work_to_onnx[i_vec[0]];
1080  std::string uniq = std::to_string(k);
1081 
1082  // Collect the explicit column-major nonzero indices
1083  std::vector<casadi_int> idx;
1084  if (info.count("nz")) {
1085  idx = info["nz"];
1086  } else if (info.count("slice")) {
1087  Dict s = info["slice"];
1088  casadi_int start = s["start"], stop = s["stop"], step = s["step"];
1089  for (casadi_int i = start; i < stop; i += step) idx.push_back(i);
1090  } else if (info.count("inner")) {
1091  Dict inner_info = info["inner"], outer_info = info["outer"];
1092  casadi_int is = inner_info["start"], ip = inner_info["stop"], ist = inner_info["step"];
1093  casadi_int os = outer_info["start"], op2 = outer_info["stop"], ost = outer_info["step"];
1094  for (casadi_int o = os; o < op2; o += ost) {
1095  for (casadi_int i = is; i < ip; i += ist) idx.push_back(o + i);
1096  }
1097  } else {
1098  return false; // unknown variant
1099  }
1100 
1101  // Recognize a structured slice of the (pretend-dense) input and emit a clean ONNX Slice.
1102  // - DENSE input: ANY regular grid A[r0:r1:rs, c0:c1:cs] (single row/col, range, block,
1103  // strided) decoded geometrically from the column-major positions -> Slice on both axes.
1104  // - SPARSE input: a single full row/col matched against the ACTUAL nonzeros, which
1105  // round-trips WITH sparsity (the importer rebuilds A[r,:]/A[:,c] on the sparse value).
1106  // Transpose-rep: CasADi rows -> ONNX axis 1, CasADi cols -> ONNX axis 0.
1107  {
1108  const Sparsity& sp_in = mx_getnonzeros.dep(0).sparsity();
1109  casadi_int m_in = sp_in.size1(), n_in = sp_in.size2();
1110  if (sp_in.is_dense() && !idx.empty()) {
1111  // `idx` are dense column-major positions. Column-major layout means the first run of
1112  // equal-column entries is the row set; its repeat stride is the column stride.
1113  casadi_int N = static_cast<casadi_int>(idx.size());
1114  casadi_int c0 = idx[0] / m_in, r0 = idx[0] % m_in;
1115  casadi_int k = 1;
1116  while (k < N && idx[k] / m_in == c0) ++k; // rows in the first column
1117  bool grid = (N % k == 0);
1118  casadi_int nc = grid ? N / k : 0;
1119  casadi_int rstep = (k > 1) ? (idx[1] % m_in) - r0 : 1;
1120  casadi_int cstep = (grid && nc > 1) ? idx[k] / m_in - c0 : 1;
1121  if (grid && rstep > 0 && cstep > 0) {
1122  for (casadi_int cj = 0; cj < nc && grid; ++cj)
1123  for (casadi_int ri = 0; ri < k && grid; ++ri)
1124  if (idx[cj * k + ri] != (c0 + cj * cstep) * m_in + (r0 + ri * rstep))
1125  grid = false;
1126  if (grid) {
1127  create_slice_node(add_node, "gnzs_" + uniq, data,
1128  {c0, r0}, {c0 + (nc - 1) * cstep + 1, r0 + (k - 1) * rstep + 1},
1129  {0, 1}, {cstep, rstep}, node_output);
1130  work_to_onnx[o_vec[0]] = node_output;
1131  return true;
1132  }
1133  }
1134  } else if (!idx.empty()) {
1135  std::vector<casadi_int> irow = sp_in.get_row(), icol = sp_in.get_col();
1136  casadi_int nnz_in = static_cast<casadi_int>(irow.size());
1137  if (mx_getnonzeros.size1() == 1 && mx_getnonzeros.size2() == n_in) {
1138  casadi_int r = irow[idx[0]];
1139  std::vector<casadi_int> exp;
1140  for (casadi_int p = 0; p < nnz_in; ++p) if (irow[p] == r) exp.push_back(p);
1141  if (exp == idx) {
1142  create_slice_node(add_node, "gnzs_" + uniq, data, {r}, {r + 1}, {1}, {},
1143  node_output);
1144  work_to_onnx[o_vec[0]] = node_output;
1145  return true;
1146  }
1147  }
1148  if (mx_getnonzeros.size1() == m_in && mx_getnonzeros.size2() == 1) {
1149  casadi_int c = icol[idx[0]];
1150  std::vector<casadi_int> exp;
1151  for (casadi_int p = 0; p < nnz_in; ++p) if (icol[p] == c) exp.push_back(p);
1152  if (exp == idx) {
1153  create_slice_node(add_node, "gnzs_" + uniq, data, {c}, {c + 1}, {0}, {},
1154  node_output);
1155  work_to_onnx[o_vec[0]] = node_output;
1156  return true;
1157  }
1158  }
1159  }
1160  }
1161 
1162  // General fallback: route the explicit nonzero map `idx` (output-nz -> input-nz, -1=fill)
1163  // through the shared remap helper -- the poorest-form primitive shared with OP_PROJECT.
1164  emit_nonzero_remap(add_node, data, mx_getnonzeros.dep(0).sparsity(),
1165  mx_getnonzeros.sparsity(), idx, uniq, node_output);
1166  work_to_onnx[o_vec[0]] = node_output;
1167  return true;
1168  }
1169 
1170  case OP_GETNONZEROS_PARAM: {
1171  // y = x.nz[p] with p a RUNTIME index vector (dep(1)), p[k] addressing x's column-major
1172  // NONZERO array. Mirrors the constant OP_GETNONZEROS but the indices flow as a value.
1173  // Transpose-rep: flatten the stored x to a 1 x numel ROW (its row-major buffer IS x's
1174  // column-major order); p (shape np x 1) is stored as a 1 x np ROW. Map nz->dense
1175  // col-major positions with find() (a compile-time GatherElements; identity if x dense),
1176  // then GatherElements the flat row by those positions -> a 1 x np ROW == stored output.
1177  // GatherElements(data[1,N], idx[1,M], axis=1) returns shape == idx [1,M] (no rank blowup).
1178  MX mx_gnzp = f.instruction_MX(k);
1179  const Sparsity& sp_in = mx_gnzp.dep(0).sparsity();
1180  casadi_int numel_in = sp_in.size1() * sp_in.size2();
1181  std::string uniq = std::to_string(k);
1182 
1183  // x's column-major flat as a 1 x numel ROW.
1184  std::string flat = work_to_onnx[i_vec[0]];
1185  if (sp_in.size2() != 1) {
1186  flat = "gnzp_flat_" + uniq;
1187  emit_colmajor_reshape(add_node, work_to_onnx[i_vec[0]], {numel_in, 1}, flat,
1188  "gnzp_flat_" + uniq);
1189  }
1190 
1191  // Runtime indices p (stored as a 1 x np row of real values) -> INT64 nz-indices.
1192  std::string nzidx = "gnzp_pi_" + uniq;
1193  create_cast_node(add_node, work_to_onnx[i_vec[1]], nzidx, onnx::TensorProto::INT64);
1194 
1195  // Map nz-indices -> dense column-major positions. Identity if x dense; otherwise gather the
1196  // constant find() table by the runtime nz-indices (GatherElements out shape == idx [1,np]).
1197  std::string dpos = nzidx;
1198  if (!sp_in.is_dense()) {
1199  std::vector<casadi_int> loc = sp_in.find(); // nz-index -> dense col-major position
1200  std::string loc_c = "gnzp_loc_" + uniq;
1201  add_int_constant(add_node, loc_c, loc, {1, static_cast<casadi_int>(loc.size())});
1202  dpos = "gnzp_dpos_" + uniq;
1203  onnx::NodeProto* ge = add_node();
1204  ge->set_op_type("GatherElements");
1205  ge->add_input(loc_c); ge->add_input(nzidx); ge->add_output(dpos);
1206  add_int_attribute(ge, "axis", 1);
1207  }
1208 
1209  // Gather x's flat row by the dense positions: GatherElements(flat[1,numel], dpos[1,np]) ->
1210  // [1,np] == the stored output (a column vector shaped like p stored transposed).
1211  onnx::NodeProto* ge = add_node();
1212  ge->set_op_type("GatherElements");
1213  ge->add_input(flat); ge->add_input(dpos); ge->add_output(node_output);
1214  add_int_attribute(ge, "axis", 1);
1215 
1216  work_to_onnx[o_vec[0]] = node_output;
1217  return true;
1218  }
1219 
1220  case OP_SETNONZEROS_PARAM:
1221  case OP_ADDNONZEROS_PARAM: {
1222  // y = x with y.nz[p] {=,+=} v, p a RUNTIME index vector (dep(2)) into x's column-major
1223  // NONZERO array; v = dep(1) the values, x = dep(0) the base. Pseudo-dense, on the flattened
1224  // x: ScatterElements(flat[1,numel], dpos[1,np], updates[1,np], axis=1, reduction). ADD uses
1225  // reduction="add" (adds onto x AND accumulates DUPLICATE p, as the getnonzeros-adjoint
1226  // requires); SET uses "none" (overwrite, last wins). Mirrors OP_GETNONZEROS_PARAM's
1227  // flatten + INT64 cast + find() nz->dense map. ADDNONZEROS_PARAM has no direct MX builder
1228  // --
1229  // it is the reverse-mode adjoint of a parametric getnonzeros; the importer rebuilds it so.
1230  bool add = (op == OP_ADDNONZEROS_PARAM);
1231  MX mx_snzp = f.instruction_MX(k);
1232  const Sparsity& sp_in = mx_snzp.dep(0).sparsity();
1233  casadi_int numel_in = sp_in.size1() * sp_in.size2();
1234  casadi_int np_idx = mx_snzp.dep(2).nnz();
1235  std::string uniq = std::to_string(k);
1236 
1237  // x's column-major flat as a 1 x numel ROW.
1238  std::string flat = work_to_onnx[i_vec[0]];
1239  if (sp_in.size2() != 1) {
1240  flat = "snzp_flat_" + uniq;
1241  emit_colmajor_reshape(add_node, work_to_onnx[i_vec[0]], {numel_in, 1}, flat,
1242  "snzp_flat_" + uniq);
1243  }
1244 
1245  // Runtime indices p -> INT64 nz-indices, then map nz -> dense col-major positions (identity
1246  // if x dense; else gather the constant find() table -- GatherElements out shape == idx).
1247  std::string nzidx = "snzp_pi_" + uniq;
1248  create_cast_node(add_node, work_to_onnx[i_vec[2]], nzidx, onnx::TensorProto::INT64);
1249  std::string dpos = nzidx;
1250  if (!sp_in.is_dense()) {
1251  std::vector<casadi_int> loc = sp_in.find();
1252  std::string loc_c = "snzp_loc_" + uniq;
1253  add_int_constant(add_node, loc_c, loc, {1, static_cast<casadi_int>(loc.size())});
1254  dpos = "snzp_dpos_" + uniq;
1255  onnx::NodeProto* ge = add_node();
1256  ge->set_op_type("GatherElements");
1257  ge->add_input(loc_c); ge->add_input(nzidx); ge->add_output(dpos);
1258  add_int_attribute(ge, "axis", 1);
1259  }
1260 
1261  // Values v as a 1 x np ROW (its column-major flat; ORT materializes any structural zeros).
1262  std::string updates = work_to_onnx[i_vec[1]];
1263  if (mx_snzp.dep(1).size2() != 1) {
1264  updates = "snzp_v_" + uniq;
1265  emit_colmajor_reshape(add_node, work_to_onnx[i_vec[1]], {np_idx, 1}, updates,
1266  "snzp_v_" + uniq);
1267  }
1268 
1269  // ScatterElements onto the flat row.
1270  std::string scat = "snzp_scat_" + uniq;
1271  onnx::NodeProto* se = add_node();
1272  se->set_op_type("ScatterElements");
1273  se->add_input(flat); se->add_input(dpos); se->add_input(updates); se->add_output(scat);
1274  add_int_attribute(se, "axis", 1);
1275  if (add) {
1276  onnx::AttributeProto* red = se->add_attribute();
1277  red->set_name("reduction");
1278  red->set_type(onnx::AttributeProto::STRING);
1279  red->set_s("add");
1280  }
1281 
1282  // Reshape the modified flat back to x's stored shape, then restore the output pattern.
1283  std::string back = "snzp_back_" + uniq;
1284  emit_colmajor_reshape(add_node, scat, {mx_snzp.size1(), mx_snzp.size2()}, back,
1285  "snzp_back_" + uniq);
1286  std::string r = emit_sparsity_restore(add_node, back,
1287  Sparsity::dense(mx_snzp.size1(), mx_snzp.size2()),
1288  mx_snzp.sparsity(), "snzpr_" + uniq, node_output);
1289  work_to_onnx[o_vec[0]] = r;
1290  return true;
1291  }
1292 
1293  case OP_PROJECT: {
1294  // project(A, sp_out) / densify keeps the matrix shape and only ZEROS the dense cells not in
1295  // sp_out (it never permutes data). Numerically that is project(A, sp_out); pseudo-dense,
1296  // the
1297  // emit_sparsity_restore seed reproduces both the numerics and the exact import pattern
1298  // sp_out natively (no overlay): densify -> dense Add-zero; widening -> sparse Add-zeros;
1299  // narrowing -> sparse Mul-ones then Add-zeros.
1300  MX mx_proj = f.instruction_MX(k);
1301  Sparsity sp_out = mx_proj.sparsity();
1302  std::string r = emit_sparsity_restore(add_node, work_to_onnx[i_vec[0]],
1303  mx_proj.dep(0).sparsity(), sp_out,
1304  std::to_string(k), node_output);
1305  work_to_onnx[o_vec[0]] = r;
1306  return true;
1307  }
1308 
1309  case OP_SETNONZEROS:
1310  case OP_ADDNONZEROS: {
1311  // y = x with y.nz[idx] = z (SETNONZEROS) or y.nz[idx] += z (ADDNONZEROS). Emit ONNX
1312  // ScatterND: scatter z's values into the 2-D pseudo-dense x at the (row,col) COORDINATES of
1313  // the target cells -- the canonical subtensor write, no flatten/reshape. idx address x's
1314  // NONZERO array -> map to dense column-major positions with find() (identity if dense).
1315  // ADDNONZEROS uses reduction="add" (adds onto x AND accumulates DUPLICATE idx, as required
1316  // by
1317  // the adjoint of a getnonzeros with repeated indices); SETNONZEROS overwrites (reduction
1318  // "none"). ADDNONZEROS cannot be built as an MX directly -- it arises from reverse-mode AD
1319  // (jtimes) of a getnonzeros; the importer reconstructs it that way.
1320  MX mx_set = f.instruction_MX(k);
1321  Dict info = mx_set.info();
1322  std::string uniq = std::to_string(k);
1323  bool add = info["add"];
1324 
1325  std::vector<casadi_int> idx;
1326  if (info.count("nz")) {
1327  idx = info["nz"];
1328  } else if (info.count("slice")) {
1329  Dict s = info["slice"];
1330  casadi_int start = s["start"], stop = s["stop"], step = s["step"];
1331  for (casadi_int i = start; i < stop; i += step) idx.push_back(i);
1332  } else if (info.count("inner")) {
1333  Dict ii = info["inner"], oo = info["outer"];
1334  casadi_int is = ii["start"], ip = ii["stop"], ist = ii["step"];
1335  casadi_int os = oo["start"], op2 = oo["stop"], ost = oo["step"];
1336  for (casadi_int o = os; o < op2; o += ost) {
1337  for (casadi_int i = is; i < ip; i += ist) idx.push_back(o + i);
1338  }
1339  } else {
1340  casadi_error("ONNX export: unsupported setnonzeros pattern.");
1341  }
1342  std::vector<casadi_int> loc = mx_set.dep(0).sparsity().find();
1343  for (casadi_int& v : idx) v = loc[v]; // dense column-major positions in x
1344 
1345  // ScatterND coordinates [nnz, 2]: x is CasADi (nrow,ncol) stored as x^T (ncol,nrow), so a
1346  // dense position p -> CasADi cell (p%nrow, p/nrow) -> ONNX coord (p/nrow, p%nrow).
1347  casadi_int nrow = mx_set.dep(0).size1();
1348  std::vector<casadi_int> coords;
1349  coords.reserve(2 * idx.size());
1350  for (casadi_int p : idx) { coords.push_back(p / nrow); coords.push_back(p % nrow); }
1351  std::string ind = "snz_idx_" + uniq;
1352  add_int_constant(add_node, ind, coords, {static_cast<casadi_int>(idx.size()), 2});
1353 
1354  // updates for ScatterND must be a 1-D [nnz] tensor. Build z's nonzeros while still 2-D
1355  // (CasADi
1356  // has no true 1-D, so a 2-D Gather round-trips cleanly), then reshape to 1-D last.
1357  casadi_int numel_z = mx_set.dep(1).numel();
1358  std::string vals = work_to_onnx[i_vec[1]];
1359  if (mx_set.dep(1).size2() != 1) { // flatten to a (1,numel_z) row (a column on import)
1360  vals = "snz_zr_" + uniq;
1361  emit_colmajor_reshape(add_node, work_to_onnx[i_vec[1]], {numel_z, 1}, vals,
1362  "snz_zr_" + uniq);
1363  }
1364  // The write positions are z's NONZEROS, so a SPARSE z contributes only its nonzeros.
1365  if (mx_set.dep(1).nnz() != numel_z) {
1366  std::vector<casadi_int> zfind = mx_set.dep(1).sparsity().find();
1367  std::string zi = "snz_zi_" + uniq, vg = "snz_zg_" + uniq;
1368  add_int_constant(add_node, zi, zfind);
1369  create_gather_node(add_node, vals, zi, vg, 1); // axis 1 (column-flat on import)
1370  vals = vg;
1371  }
1372  std::string upd = "snz_upd_" + uniq, ush = "snz_ush_" + uniq;
1373  add_int_constant(add_node, ush, {static_cast<casadi_int>(idx.size())}); // 1-D shape [nnz]
1374  node = add_node();
1375  node->set_op_type("Reshape");
1376  node->add_input(vals); node->add_input(ush); node->add_output(upd);
1377 
1378  std::string scat = "snz_out_" + uniq;
1379  node = add_node();
1380  node->set_op_type("ScatterND");
1381  node->add_input(work_to_onnx[i_vec[0]]); // 2-D pseudo-dense x (no flatten)
1382  node->add_input(ind);
1383  node->add_input(upd);
1384  node->add_output(scat);
1385  if (add) { // accumulate onto x and over duplicate indices (opset 16 reduction)
1386  onnx::AttributeProto* red = node->add_attribute();
1387  red->set_name("reduction");
1388  red->set_type(onnx::AttributeProto::STRING);
1389  red->set_s("add");
1390  }
1391  // Restore the instruction's output pattern natively from the seed (no overlay). ScatterND
1392  // imports as a DENSE value (the importer densifies the target), so seed mx_set.sparsity().
1393  std::string r = emit_sparsity_restore(add_node, scat,
1394  Sparsity::dense(mx_set.size1(), mx_set.size2()),
1395  mx_set.sparsity(), uniq, node_output);
1396  work_to_onnx[o_vec[0]] = r;
1397  return true;
1398  }
1399 
1400  case OP_ATAN2: {
1401  // atan2(y, x) = atan(y/x) + correction for x<0, with origin→0
1402  std::string y = work_to_onnx[i_vec[0]], x = work_to_onnx[i_vec[1]];
1403  std::string p = "atan2_" + std::to_string(k) + "_";
1404 
1405  add_real_constant(add_node, p+"c0", {0.0});
1406  add_real_constant(add_node, p+"pi", {M_PI});
1407  create_binary_node(add_node, "Div", y, x, p+"r");
1408  create_unary_node(add_node, "Atan", p+"r", p+"b");
1409  create_binary_node(add_node, "Less", x, p+"c0", p+"xn");
1410  create_binary_node(add_node, "Less", y, p+"c0", p+"yn");
1411  create_unary_node(add_node, "Neg", p+"pi", p+"npi");
1412  create_where_node(add_node, p+"yn", p+"npi", p+"pi", p+"corr"); // x<0: -π if y<0 else +π
1413  create_where_node(add_node, p+"xn", p+"corr", p+"c0", p+"adj"); // 0 when x>=0
1414  create_binary_node(add_node, "Add", p+"b", p+"adj", p+"res");
1415  create_binary_node(add_node, "Equal", x, p+"c0", p+"xz");
1416  create_binary_node(add_node, "Equal", y, p+"c0", p+"yz");
1417  create_binary_node(add_node, "And", p+"xz", p+"yz", p+"oz");
1418  create_where_node(add_node, p+"oz", p+"c0", p+"res", node_output); // origin → 0
1419 
1420  work_to_onnx[o_vec[0]] = node_output;
1421  return true;
1422  }
1423 
1424  // Handled by the caller (OP_CALL: control flow / function call) or unsupported.
1425  case OP_CALL:
1426  case OP_SUBREF:
1427  default:
1428  return false;
1429  }
1430  }
1431 
1432  // GraphProto convenience wrapper for process_operation
1433  bool Onnx::process_operation(
1434  onnx::GraphProto* graph,
1435  const Function& f,
1436  casadi_int op,
1437  casadi_int k,
1438  const std::vector<casadi_int>& i_vec,
1439  const std::vector<casadi_int>& o_vec,
1440  std::map<casadi_int, std::string>& work_to_onnx,
1441  const std::string& node_output) {
1442  return process_operation([graph]() { return graph->add_node(); },
1443  f, op, k, i_vec, o_vec, work_to_onnx, node_output);
1444  }
1445 
1446 } // namespace casadi
static Sparsity dense(casadi_int nrow, casadi_int ncol=1)
Create a dense rectangular sparsity pattern *.
Definition: sparsity.cpp:1028
The casadi namespace.
Definition: archiver.cpp:28
onnx::NodeProto * create_where_node(AddNodeFn add_node, const std::string &cond, const std::string &if_true, const std::string &if_false, const std::string &output)
onnx::NodeProto * create_gemm_node(AddNodeFn add_node, const std::string &A, const std::string &B, const std::string &C, const std::string &output, bool transA=false, bool transB=false)
static const OpMapping op_map[]
onnx::NodeProto * create_gather_node(AddNodeFn add_node, const std::string &data, const std::string &indices, const std::string &output, casadi_int axis=0)
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
const OpMapping * get_op_mapping(casadi_int op)
Lookup operation mapping by CasADi opcode (for export)
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)
const OpMapping * get_op_mapping_by_name(const std::string &onnx_name)
Lookup operation mapping by ONNX name (for import)
void add_int_attribute(onnx::NodeProto *node, const std::string &name, casadi_int value)
Add an integer attribute (e.g. axis) to a node.
void emit_colmajor_reshape(AddNodeFn add_node, const std::string &data, const std::vector< casadi_int > &dims, const std::string &output, const std::string &uniq)
onnx::NodeProto * create_slice_node(AddNodeFn add_node, const std::string &uniq, const std::string &data, const std::vector< casadi_int > &starts, const std::vector< casadi_int > &ends, const std::vector< casadi_int > &axes, const std::vector< casadi_int > &steps, const std::string &output)
onnx::NodeProto * create_cast_node(AddNodeFn add_node, const std::string &input, const std::string &output, onnx::TensorProto::DataType to_type)
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
onnx::TensorProto * create_constant_tensor(AddNodeFn add_node, const std::string &output_name, onnx::TensorProto::DataType data_type)
void add_int_constant(onnx::GraphProto *graph, const std::string &name, const std::vector< casadi_int > &data, std::vector< casadi_int > dims={})
Add a Constant node holding an INT64 tensor to a graph (default shape: 1-D)
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
T sum(const std::vector< T > &values)
sum
@ OP_DIAGCAT
Definition: calculus.hpp:130
@ OP_SIGN
Definition: calculus.hpp:71
@ OP_COS
Definition: calculus.hpp:68
@ OP_ERF
Definition: calculus.hpp:72
@ OP_NE
Definition: calculus.hpp:70
@ OP_HORZCAT
Definition: calculus.hpp:124
@ OP_FMAX
Definition: calculus.hpp:72
@ OP_SINH
Definition: calculus.hpp:74
@ OP_COSH
Definition: calculus.hpp:74
@ OP_VERTCAT
Definition: calculus.hpp:127
@ OP_ASINH
Definition: calculus.hpp:75
@ OP_ACOS
Definition: calculus.hpp:69
@ OP_ATAN2
Definition: calculus.hpp:76
@ OP_HORZREPSUM
Definition: calculus.hpp:187
@ OP_ADDNONZEROS_PARAM
Definition: calculus.hpp:160
@ OP_COPYSIGN
Definition: calculus.hpp:71
@ OP_IF_ELSE_ZERO
Definition: calculus.hpp:71
@ OP_MMAX
Definition: calculus.hpp:181
@ OP_AND
Definition: calculus.hpp:70
@ OP_ATAN
Definition: calculus.hpp:69
@ OP_KRON
Definition: calculus.hpp:214
@ OP_SQRT
Definition: calculus.hpp:67
@ OP_INV
Definition: calculus.hpp:73
@ OP_EXP
Definition: calculus.hpp:66
@ OP_OUTPUT
Definition: calculus.hpp:82
@ OP_MMIN
Definition: calculus.hpp:181
@ OP_SETNONZEROS
Definition: calculus.hpp:163
@ OP_LOG1P
Definition: calculus.hpp:202
@ OP_SIN
Definition: calculus.hpp:68
@ OP_ASIN
Definition: calculus.hpp:69
@ OP_VERTSPLIT
Definition: calculus.hpp:136
@ OP_LT
Definition: calculus.hpp:70
@ OP_ACOSH
Definition: calculus.hpp:75
@ OP_CEIL
Definition: calculus.hpp:71
@ OP_EQ
Definition: calculus.hpp:70
@ OP_CONST
Definition: calculus.hpp:79
@ OP_OR
Definition: calculus.hpp:70
@ OP_TWICE
Definition: calculus.hpp:67
@ OP_HYPOT
Definition: calculus.hpp:206
@ OP_EINSTEIN
Definition: calculus.hpp:193
@ OP_INPUT
Definition: calculus.hpp:82
@ OP_LIFT
Definition: calculus.hpp:191
@ OP_SUB
Definition: calculus.hpp:65
@ OP_ATANH
Definition: calculus.hpp:75
@ OP_SUBREF
Definition: calculus.hpp:145
@ OP_DETERMINANT
Definition: calculus.hpp:109
@ OP_DOT
Definition: calculus.hpp:115
@ OP_FMIN
Definition: calculus.hpp:72
@ OP_POW
Definition: calculus.hpp:66
@ OP_PROJECT
Definition: calculus.hpp:169
@ OP_EXPM1
Definition: calculus.hpp:204
@ OP_ADDNONZEROS
Definition: calculus.hpp:157
@ OP_SETNONZEROS_PARAM
Definition: calculus.hpp:166
@ OP_FABS
Definition: calculus.hpp:71
@ OP_BILIN
Definition: calculus.hpp:118
@ OP_MTIMES
Definition: calculus.hpp:100
@ OP_LOG
Definition: calculus.hpp:66
@ OP_LOGSUMEXP
Definition: calculus.hpp:208
@ OP_TANH
Definition: calculus.hpp:74
@ OP_NORM1
Definition: calculus.hpp:178
@ OP_CALL
Definition: calculus.hpp:88
@ OP_ADD
Definition: calculus.hpp:65
@ OP_NORM2
Definition: calculus.hpp:178
@ OP_LE
Definition: calculus.hpp:70
@ OP_RESHAPE
Definition: calculus.hpp:142
@ OP_DIV
Definition: calculus.hpp:65
@ OP_TRANSPOSE
Definition: calculus.hpp:106
@ OP_SOLVE
Definition: calculus.hpp:103
@ OP_FLOOR
Definition: calculus.hpp:71
@ OP_ASSERTION
Definition: calculus.hpp:172
@ OP_NEG
Definition: calculus.hpp:66
@ OP_RANK1
Definition: calculus.hpp:121
@ OP_CONSTPOW
Definition: calculus.hpp:66
@ OP_NOT
Definition: calculus.hpp:70
@ OP_MUL
Definition: calculus.hpp:65
@ OP_ASSIGN
Definition: calculus.hpp:62
@ OP_HORZREPMAT
Definition: calculus.hpp:184
@ OP_HORZSPLIT
Definition: calculus.hpp:133
@ OP_GETNONZEROS_PARAM
Definition: calculus.hpp:154
@ OP_SQ
Definition: calculus.hpp:67
@ OP_NORMF
Definition: calculus.hpp:178
@ OP_MONITOR
Definition: calculus.hpp:175
@ OP_GETNONZEROS
Definition: calculus.hpp:151
@ OP_FMOD
Definition: calculus.hpp:71
@ OP_TAN
Definition: calculus.hpp:68
@ OP_NORMINF
Definition: calculus.hpp:178
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.
Operation mapping between CasADi and ONNX.
Definition: onnx_model.hpp:420