onnx_import.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 
28 #include <onnx/shape_inference/implementation.h>
29 
31 namespace casadi {
32 
33  // Read structural integer constants without passing their payload through DM.
34  static bool integer_tensor(const onnx::TensorProto& tensor, std::vector<int64_t>& values) {
35  if (tensor.data_type() != onnx::TensorProto::INT64 &&
36  tensor.data_type() != onnx::TensorProto::INT32) return false;
37  values.clear();
38  if (tensor.has_raw_data()) {
39  const auto& raw = tensor.raw_data();
40  size_t width = tensor.data_type() == onnx::TensorProto::INT64 ? 8 : 4;
41  casadi_assert(raw.size() % width == 0, "Invalid integer tensor raw_data size");
42  for (size_t i = 0; i < raw.size(); i += width) {
43  uint64_t bits = 0;
44  for (size_t j = 0; j < width; ++j) {
45  bits |= static_cast<uint64_t>(static_cast<unsigned char>(raw[i + j])) << (8 * j);
46  }
47  // ONNX raw_data is little-endian two's complement.
48  if (width == 4) {
49  values.push_back(static_cast<int64_t>(bits) - static_cast<int64_t>((bits >> 31) << 32));
50  } else {
51  values.push_back(bits >> 63 ? -1 - static_cast<int64_t>(~bits) :
52  static_cast<int64_t>(bits));
53  }
54  }
55  } else if (tensor.data_type() == onnx::TensorProto::INT64) {
56  values.assign(tensor.int64_data().begin(), tensor.int64_data().end());
57  } else {
58  values.assign(tensor.int32_data().begin(), tensor.int32_data().end());
59  }
60  return true;
61  }
62 
63  // Whether two stored shapes broadcast against each other (equal or 1 along each axis)
64  static bool broadcastable(const MX& a, const MX& b) {
65  return (a.size1() == b.size1() || a.size1() == 1 || b.size1() == 1)
66  && (a.size2() == b.size2() || a.size2() == 1 || b.size2() == 1);
67  }
68 
69  // Orient a vector operand v towards the other operand o of a binary op
70  static void orient_vector(MX* v, const MX& o) {
71  if (!v->is_vector()) return;
72  // Two equal-length vectors in different orientations are the same rank-1 tensor;
73  // settle on the column form, the convention for rank-1 initializers and inputs
74  if (o.is_vector() && v->numel() == o.numel()) {
75  if (v->size1() != o.size1()) *v = reshape(*v, v->numel(), 1);
76  return;
77  }
78  if (broadcastable(*v, o)) return;
79  if (v->numel() == o.size1()) *v = reshape(*v, o.size1(), 1);
80  else if (v->numel() == o.size2()) *v = reshape(*v, 1, o.size2());
81  }
82 
83  // ONNX (numpy-style) broadcasting of two operands in the 2-D stored representation.
84  // A rank-1 tensor may be stored as a row or a column depending on where it came from
85  // (Gather/Slice results are rows; initializers and graph inputs are columns), so a vector
86  // that is not already broadcast-compatible is first oriented along the other operand's
87  // matching dimension, e.g. a Gemm/Add bias [N] against a stored (N,M) result.
88  static void broadcast_pair(MX* x, MX* y) {
89  if (x->is_scalar() || y->is_scalar()) return;
90  orient_vector(x, *y);
91  orient_vector(y, *x);
92  casadi_int nr = std::max(x->size1(), y->size1()), nc = std::max(x->size2(), y->size2());
93  for (MX* v : {x, y}) {
94  casadi_assert(broadcastable(*v, MX::zeros(nr, nc)), "ONNX import: cannot broadcast "
95  + x->dim() + " with " + y->dim());
96  if (v->size1() != nr || v->size2() != nc) *v = repmat(*v, nr / v->size1(), nc / v->size2());
97  }
98  }
99 
100  // ONNX initializers are pre-loaded constants
101  void Onnx::process_graph_initializers(
102  const onnx::GraphProto& graph,
103  std::map<std::string, MX>& value_map,
104  bool verbose) const {
105 
106  for (int i = 0; i < graph.initializer_size(); ++i) {
107  const onnx::TensorProto& tensor = graph.initializer(i);
108  std::string tensor_name = tensor.name();
109 
110  if (verbose) {
111  uout() << " Processing initializer: " << tensor_name << std::endl;
112  }
113 
114  value_map[tensor_name] = MX(tensor_to_dm(tensor));
115  }
116  }
117 
118  // Create MX symbols for graph inputs
119  void Onnx::process_graph_inputs(
120  const onnx::GraphProto& graph,
121  std::map<std::string, MX>& value_map,
122  std::vector<MX>& func_inputs,
123  std::vector<std::string>& input_names,
124  bool verbose) const {
125 
126  for (int i = 0; i < graph.input_size(); ++i) {
127  const onnx::ValueInfoProto& input = graph.input(i);
128  std::string input_name = input.name();
129 
130  // Skip if already in value_map (it's an initializer, not a variable)
131  if (value_map.count(input_name)) {
132  if (verbose) {
133  uout() << " Skipping input '" << input_name
134  << "' (it's an initializer)" << std::endl;
135  }
136  continue;
137  }
138 
139  // Extract shape
140  const onnx::TensorShapeProto& shape =
141  input.type().tensor_type().shape();
142  casadi_int rows = get_dimension(shape, 0);
143  casadi_int cols = get_dimension(shape, 1);
144 
145  if (verbose) {
146  uout() << " Creating input: " << input_name
147  << " [" << rows << ", " << cols << "]" << std::endl;
148  }
149 
150  // Sparsity overlay: if a sparse_initializer shares this input's name it carries a non-dense
151  // input pattern -- make a SPARSE input symbol and feed it AS-IS, so CasADi propagates the
152  // true sparsity through the body (no densification on entry; densify only where an ONNX op
153  // requires
154  // the dense layout, e.g. Reshape). Transpose-rep: a rank-2 ONNX input (r,c) is stored as
155  // (c,r); a rank<2 input [n] is a column (n,1), matching tensor_to_dm for initializers so
156  // that e.g. a 1-D Gemm bias broadcasts the same whether it is an initializer or an input.
157  Sparsity ov = input_pattern(graph, input_name);
158  MX mx_input = !ov.is_null() ? MX::sym(input_name, ov)
159  : shape.dim_size() < 2 ? MX::sym(input_name, rows, cols)
160  : MX::sym(input_name, cols, rows);
161  value_map[input_name] = mx_input;
162  func_inputs.push_back(mx_input);
163  input_names.push_back(input_name);
164  }
165  }
166 
167  // ONNX ranks the graph declares (inputs, intermediate value_info, outputs, initializers)
168  static std::map<std::string, casadi_int> declared_ranks(const onnx::GraphProto& graph) {
169  std::map<std::string, casadi_int> ranks;
170  for (const auto* infos : {&graph.input(), &graph.value_info(), &graph.output()}) {
171  for (const auto& info : *infos) {
172  if (info.type().tensor_type().has_shape()) {
173  ranks[info.name()] = info.type().tensor_type().shape().dim_size();
174  }
175  }
176  }
177  for (const auto& tensor : graph.initializer()) ranks[tensor.name()] = tensor.dims_size();
178  return ranks;
179  }
180 
181  // ONNX counts a negative "axis" attribute from the end, relative to the rank of input 0
182  // (OneHot, relative to the output rank, aside). CasADi values are 2-D and do not keep the ONNX
183  // rank, e.g. [1, n] and [n] are both stored as (n, 1), so resolve the axis against the rank
184  // the graph declares. Returns node itself if there is nothing to resolve, else *copy.
185  static const onnx::NodeProto& resolve_negative_axis(const onnx::NodeProto& node,
186  const std::map<std::string, casadi_int>& ranks, onnx::NodeProto* copy) {
187  if (node.op_type() == "OneHot" || node.input_size() == 0) return node;
188  for (int a = 0; a < node.attribute_size(); ++a) {
189  if (node.attribute(a).name() != "axis" || node.attribute(a).i() >= 0) continue;
190  auto it = ranks.find(node.input(0));
191  if (it == ranks.end()) return node; // undeclared rank: leave it to the op
192  *copy = node;
193  copy->mutable_attribute(a)->set_i(node.attribute(a).i() + it->second);
194  return *copy;
195  }
196  return node;
197  }
198 
199  // Process all nodes in the graph
200  void Onnx::process_graph_nodes(
201  const onnx::GraphProto& graph,
202  std::map<std::string, MX>& value_map,
203  bool verbose) {
204 
205  IntegerConstants integer_constants;
206  for (const auto& tensor : graph.initializer()) {
207  std::vector<int64_t> values;
208  if (integer_tensor(tensor, values)) integer_constants[tensor.name()] = std::move(values);
209  }
210 
211  std::map<std::string, casadi_int> ranks = declared_ranks(graph);
212 
213  // Per-graph capture of kron operands, keyed by the exporter's node-name tag "kron<k>".
214  std::map<std::string, MX> kron_operand_a_, kron_operand_b_;
215 
216  for (int i = 0; i < graph.node_size(); ++i) {
217  onnx::NodeProto resolved;
218  const onnx::NodeProto& node = resolve_negative_axis(graph.node(i), ranks, &resolved);
219  std::string op_type = node.op_type();
220 
221  if (verbose) {
222  uout() << " Processing node " << i << ": " << op_type << std::endl;
223  }
224 
225  // Only direct Constant nodes and initializers retain an integer payload.
226  if (op_type == "Constant" && node.output_size() == 1) {
227  for (const auto& attr : node.attribute()) {
228  std::vector<int64_t> values;
229  if (attr.name() == "value_int") {
230  values = {attr.i()};
231  } else if (attr.name() == "value_ints") {
232  values.assign(attr.ints().begin(), attr.ints().end());
233  } else if (attr.name() != "value" || !integer_tensor(attr.t(), values)) {
234  continue;
235  }
236  integer_constants[node.output(0)] = std::move(values);
237  }
238  }
239 
240  // ========== Special Handling: Kronecker product group ==========
241  // The exporter tags its kron node group with node name "kron<k>" (see OP_KRON in
242  // onnx_operations.cpp). The 4-D intermediate tensors cannot be MX (MX is 2-D), so we do NOT
243  // process the group generically (the helper tensors kron<k>_A4/_B4/_P are never added to the
244  // value_map). We capture the two logical operands at the leading Reshapes (whose single data
245  // input is the imported, transposed-back A / B) and reconstruct kron(A_imp, B_imp) at the
246  // final Reshape. CasADi kron propagates sparsity natively, so the pattern round-trips for
247  // free
248  // (kron commutes with transpose). This is handled BEFORE the generic input gathering because
249  // the helper tensors are intentionally absent from value_map.
250  if (node.name().rfind("kron", 0) == 0) {
251  const std::string& nm = node.name(); // "kron<k>_<role>"
252  std::size_t us = nm.rfind('_');
253  std::string tag = nm.substr(0, us), role = nm.substr(us + 1);
254  if (role == "A4") {
255  kron_operand_a_[tag] = value_map.at(node.input(0)); // data input = imported A
256  continue;
257  }
258  if (role == "B4") {
259  kron_operand_b_[tag] = value_map.at(node.input(0)); // data input = imported B
260  continue;
261  }
262  if (role == "P") continue; // intermediate broadcast product; nothing to record
263  if (role == "R") {
264  // Final Reshape: emit kron of the captured operands and skip generic 4-D handling.
265  casadi_assert(kron_operand_a_.count(tag) && kron_operand_b_.count(tag),
266  "ONNX import: incomplete kron node group '" + tag + "'");
267  value_map[node.output(0)] = MX::kron(kron_operand_a_.at(tag), kron_operand_b_.at(tag));
268  continue;
269  }
270  }
271 
272  // Gather input tensors
273  std::vector<MX> node_inputs;
274  for (int j = 0; j < node.input_size(); ++j) {
275  std::string input_name = node.input(j);
276 
277  // Empty string means optional input not provided
278  if (input_name.empty()) {
279  node_inputs.push_back(MX());
280  continue;
281  }
282 
283  casadi_assert(value_map.count(input_name),
284  "Unknown input tensor '" + input_name +
285  "' required by node " + std::to_string(i) +
286  " (op_type: " + op_type + ")");
287  node_inputs.push_back(value_map[input_name]);
288  }
289 
290  // Compute output based on operation type
291  MX output;
292 
293  // ========== Special Handling: Multi-output operations ==========
294  if (op_type == "Split") {
295  casadi_assert(node_inputs.size() >= 1, "Split requires 1 input");
296  casadi_int axis = get_int_attribute(node, "axis", 0);
297  casadi_assert(axis == 0 || axis == 1, "Split: only axis 0 and 1 supported");
298 
299  // Split sizes: 2nd input (opset 13+), else 'split' attribute (opset <13), else equal
300  std::vector<casadi_int> split_sizes;
301  if (node_inputs.size() >= 2) {
302  DM split_dm = DM(node_inputs[1]);
303  for (casadi_int k = 0; k < split_dm.nnz(); ++k) {
304  split_sizes.push_back(static_cast<casadi_int>(static_cast<double>(split_dm.nz(k))));
305  }
306  } else {
307  for (int a = 0; a < node.attribute_size(); ++a) {
308  if (node.attribute(a).name() == "split") {
309  for (int k = 0; k < node.attribute(a).ints_size(); ++k) {
310  split_sizes.push_back(node.attribute(a).ints(k));
311  }
312  break;
313  }
314  }
315  }
316  if (split_sizes.empty()) { // equal split across the outputs
317  // Transpose-rep: ONNX axis 0 is CasADi columns, axis 1 is CasADi rows
318  casadi_int total = (axis == 0) ? node_inputs[0].size2() : node_inputs[0].size1();
319  split_sizes.assign(node.output_size(), total / node.output_size());
320  }
321 
322  // Offsets [0, s0, s0+s1, ...]; ONNX axis 0 -> CasADi horzsplit, axis 1 -> vertsplit
323  std::vector<casadi_int> offset = {0};
324  for (casadi_int sz : split_sizes) offset.push_back(offset.back() + sz);
325  std::vector<MX> outputs = (axis == 0) ? horzsplit(node_inputs[0], offset)
326  : vertsplit(node_inputs[0], offset);
327 
328  for (casadi_int j = 0; j < outputs.size(); ++j) value_map[node.output(j)] = outputs[j];
329  continue; // Don't use standard output handling
330 
331  // ========== Scan: iterate a body over columns -> CasADi Map ==========
332  } else if (op_type == "Scan") {
333  const onnx::GraphProto* body = get_graph_attribute(node, "body");
334  casadi_assert(body != nullptr, "Scan node requires a 'body' subgraph");
335  casadi_int num_scan_inputs = get_int_attribute(node, "num_scan_inputs", node.input_size());
336  casadi_int M = node.input_size() - num_scan_inputs; // state variables (reduce_out count)
337 
338  // A body that references outer-scope names (captures) signals reduce_in
339  std::set<std::string> body_in_names, defined;
340  for (int b = 0; b < body->input_size(); ++b) {
341  body_in_names.insert(body->input(b).name());
342  defined.insert(body->input(b).name());
343  }
344  for (int nn = 0; nn < body->node_size(); ++nn)
345  for (int oo = 0; oo < body->node(nn).output_size(); ++oo)
346  defined.insert(body->node(nn).output(oo));
347  bool has_capture = false;
348  for (int nn = 0; nn < body->node_size() && !has_capture; ++nn)
349  for (int ii = 0; ii < body->node(nn).input_size(); ++ii) {
350  const std::string& in = body->node(nn).input(ii);
351  if (!in.empty() && !defined.count(in)) { has_capture = true; break; }
352  }
353 
354  if (M == 0 && !has_capture) {
355  // Plain map: the 3-D lift reshapes pass through on import (CasADi is 2-D), so
356  // node_inputs are the original (rows, n*c) map inputs. Rebuild base.map(n).
357  Function base = function_from_graph(*body, op_type + "_body");
358  casadi_int n = node_inputs[0].size2() / base.size2_in(0);
359  std::vector<MX> outputs =
360  base.map(n)(std::vector<MX>(node_inputs.begin(), node_inputs.end()));
361  for (int j = 0; j < node.output_size(); ++j) value_map[node.output(j)] = outputs[j];
362  continue;
363  }
364 
365  // Reduce-map: the body wraps a single base call; recover base + masks and rebuild
366  // base.map(n, reduce_in, reduce_out).
367  const onnx::NodeProto* call = nullptr;
368  for (int nn = 0; nn < body->node_size(); ++nn)
369  if (!body->node(nn).domain().empty()) { call = &body->node(nn); break; }
370  casadi_assert(call, "reduce-map Scan body must contain a base function call");
371  casadi_int nin = call->input_size(), nout = call->output_size();
372 
373  // reduce_in: base arg is a capture; reduce_out: base result feeds an accumulator Add
374  std::vector<bool> reduce_in(nin), reduce_out(nout, false);
375  for (casadi_int j = 0; j < nin; ++j) reduce_in[j] = !body_in_names.count(call->input(j));
376  for (int nn = 0; nn < body->node_size(); ++nn) {
377  if (body->node(nn).op_type() != "Add") continue;
378  for (int ii = 0; ii < body->node(nn).input_size(); ++ii)
379  for (casadi_int j = 0; j < nout; ++j)
380  if (body->node(nn).input(ii) == call->output(j)) reduce_out[j] = true;
381  }
382 
383  // Base input shapes: captures from the outer tensor, scanned ones from the body input
384  std::vector<std::pair<casadi_int, casadi_int>> in_shapes(nin);
385  for (casadi_int j = 0; j < nin; ++j) {
386  if (reduce_in[j]) {
387  MX cap = value_map.at(call->input(j));
388  in_shapes[j] = {cap.size1(), cap.size2()};
389  } else {
390  for (int b = 0; b < body->input_size(); ++b)
391  if (body->input(b).name() == call->input(j)) {
392  const onnx::TensorShapeProto& sh = body->input(b).type().tensor_type().shape();
393  in_shapes[j] = {get_dimension(sh, 0), get_dimension(sh, 1)};
394  break;
395  }
396  }
397  }
398 
399  const onnx::FunctionProto* fp = find_function(call->op_type(), call->domain());
400  casadi_assert(fp, "reduce-map base function '" + call->op_type() + "' not found");
401  Function base = function_from_function_proto(*fp, in_shapes, op_type + "_base");
402 
403  // base.map args (base order): captures broadcast, repeated come from scan node inputs
404  std::vector<MX> args(nin);
405  casadi_int n = 0, r = 0;
406  for (casadi_int j = 0; j < nin; ++j) {
407  if (reduce_in[j]) {
408  args[j] = value_map.at(call->input(j));
409  } else {
410  args[j] = node_inputs[M + r];
411  if (n == 0) n = args[j].size2() / base.size2_in(j);
412  ++r;
413  }
414  }
415  casadi_assert(n > 0, "reduce-map import: could not infer map size");
416  std::vector<MX> outs = base.map(n, reduce_in, reduce_out)(args);
417 
418  // Node outputs are [state accumulators (reduce_out), scan outputs (repeated)], base order
419  casadi_int si = 0, ci = 0;
420  for (casadi_int j = 0; j < nout; ++j) {
421  if (reduce_out[j]) value_map[node.output(si++)] = outs[j];
422  else
423  value_map[node.output(M + ci++)] = outs[j];
424  }
425  continue;
426 
427  // ========== If: two captured branches combined with if_else ==========
428  } else if (op_type == "If") {
429  const onnx::GraphProto* then_b = get_graph_attribute(node, "then_branch");
430  const onnx::GraphProto* else_b = get_graph_attribute(node, "else_branch");
431  casadi_assert(then_b && else_b, "If node requires then_branch and else_branch");
432  std::vector<MX> t = eval_captured_subgraph(*then_b, value_map);
433  std::vector<MX> e = eval_captured_subgraph(*else_b, value_map);
434  MX cond = node_inputs[0];
435  for (int j = 0; j < node.output_size(); ++j) {
436  value_map[node.output(j)] = if_else(cond, t[j], e[j]);
437  }
438  continue;
439 
440  // ========== Control Flow Operations (not supported) ==========
441  } else if (op_type == "Loop") {
442  casadi_error("ONNX import: 'Loop' control flow operator is not supported.");
443 
444  // ========== Standard Operations (delegated to helper) ==========
445  } else {
446  // A non-empty domain marks a function call.
447  std::string node_domain = node.domain();
448  if (!node_domain.empty()) {
449  const onnx::FunctionProto* func_proto = find_function(op_type, node_domain);
450 
451  if (func_proto != nullptr) {
452  if (verbose) {
453  uout() << " Function call to: " << node_domain << "." << op_type << std::endl;
454  }
455 
456  // Inline the function body: only its nodes are needed (process_graph_nodes reads
457  // graph.node() only); formal inputs/outputs are wired through func_value_map below.
458  onnx::GraphProto func_graph;
459  func_graph.set_name(func_proto->name());
460  for (int n = 0; n < func_proto->node_size(); ++n) {
461  *func_graph.add_node() = func_proto->node(n);
462  }
463 
464  // Map the function's formal inputs to the call-site values.
465  std::map<std::string, MX> func_value_map;
466  for (size_t n = 0; n < node_inputs.size() && n < func_proto->input_size(); ++n) {
467  func_value_map[func_proto->input(n)] = node_inputs[n];
468  }
469 
470  process_graph_nodes(func_graph, func_value_map, false);
471 
472  // Store the function outputs in the outer value_map.
473  for (int n = 0; n < node.output_size() && n < func_proto->output_size(); ++n) {
474  std::string func_output_name = func_proto->output(n);
475  casadi_assert(func_value_map.count(func_output_name),
476  "Function output '" + func_output_name + "' not found");
477  value_map[node.output(n)] = func_value_map[func_output_name];
478 
479  if (verbose) {
480  uout() << " -> " << node.output(n) << std::endl;
481  }
482  }
483 
484  // Skip the normal single-output handling
485  continue;
486  }
487  // Fall through to error if function not found
488  }
489 
490  // All other (single-output) operations
491  output = process_node_operation(op_type, node, node_inputs, integer_constants);
492  }
493 
494  // Store output (assume single output for now)
495  casadi_assert(node.output_size() >= 1,
496  "Node must have at least one output");
497 
498  std::string output_name = node.output(0);
499  value_map[output_name] = output;
500 
501  if (verbose) {
502  uout() << " -> " << output_name << " " << output.dim() << std::endl;
503  }
504  }
505  }
506 
507  // Collect graph outputs from value_map
508  void Onnx::collect_graph_outputs(
509  const onnx::GraphProto& graph,
510  const std::map<std::string, MX>& value_map,
511  std::vector<MX>& func_outputs,
512  std::vector<std::string>& output_names,
513  bool verbose) const {
514 
515  for (int i = 0; i < graph.output_size(); ++i) {
516  const onnx::ValueInfoProto& output = graph.output(i);
517  std::string output_name = output.name();
518 
519  casadi_assert(value_map.count(output_name),
520  "Unknown output tensor: " + output_name +
521  ". This usually means the ONNX graph contains unsupported operations.");
522 
523  // No output overlay: each op restores its own output sparsity from seeds during export, so
524  // the value already carries the exact CasADi pattern (re-propagated natively from the seeds).
525  func_outputs.push_back(value_map.at(output_name));
526  output_names.push_back(output_name);
527 
528  if (verbose) {
529  uout() << " Graph output: " << output_name << std::endl;
530  }
531  }
532  }
533 
534  const onnx::FunctionProto* Onnx::find_function(const std::string& name,
535  const std::string& domain) const {
536  for (int f = 0; f < model_.functions_size(); ++f)
537  if (model_.functions(f).name() == name && model_.functions(f).domain() == domain)
538  return &model_.functions(f);
539  return nullptr;
540  }
541 
542  // Whether a node has a negative axis that resolve_negative_axis cannot resolve
543  static bool has_unresolved_negative_axis(const onnx::GraphProto& graph) {
544  std::map<std::string, casadi_int> ranks = declared_ranks(graph);
545  for (const auto& node : graph.node()) {
546  if (node.op_type() == "OneHot" || node.input_size() == 0) continue;
547  if (get_int_attribute(node, "axis", 0) < 0 && !ranks.count(node.input(0))) return true;
548  }
549  return false;
550  }
551 
552  Function Onnx::create(const std::string& name) {
553  casadi_assert(has_model_, "No ONNX model loaded. Call load() first.");
554  if (!has_unresolved_negative_axis(model_.graph())) {
555  return function_from_graph(model_.graph(), name);
556  }
557  // Exporters need not declare the shapes of intermediate tensors (PyTorch's TorchScript
558  // exporter doesn't). Infer them on a copy, leaving model_ as loaded, so that negative axes
559  // can be resolved. If inference fails, import as is: the affected op reports the axis.
560  onnx::ModelProto inferred = model_;
561  try {
562  onnx::shape_inference::InferShapes(inferred, onnx::OpSchemaRegistry::Instance(),
563  onnx::ShapeInferenceOptions(false, 0, true));
564  } catch (const std::exception& e) {
565  if (verbose_) uout() << "ONNX shape inference failed: " << e.what() << std::endl;
566  return function_from_graph(model_.graph(), name);
567  }
568  return function_from_graph(inferred.graph(), name);
569  }
570 
571  Function Onnx::function_from_graph(const onnx::GraphProto& graph,
572  const std::string& name) {
573  std::map<std::string, MX> value_map;
574  std::vector<MX> inputs, outputs;
575  std::vector<std::string> input_names, output_names;
576 
577  if (verbose_) {
578  uout() << "Building CasADi Function '" << name << "' from graph '" << graph.name()
579  << "' (" << graph.initializer_size() << " initializers, "
580  << graph.input_size() << " inputs, " << graph.output_size() << " outputs, "
581  << graph.node_size() << " nodes)" << std::endl;
582  }
583 
584  process_graph_initializers(graph, value_map, verbose_);
585  process_graph_inputs(graph, value_map, inputs, input_names, verbose_);
586  process_graph_nodes(graph, value_map, verbose_);
587  collect_graph_outputs(graph, value_map, outputs, output_names, verbose_);
588 
589  return Function(name, inputs, outputs, input_names, output_names);
590  }
591 
592  Function Onnx::function_from_function_proto(
593  const onnx::FunctionProto& fp,
594  const std::vector<std::pair<casadi_int, casadi_int>>& in_shapes,
595  const std::string& name) {
596  // A FunctionProto carries no shapes; wrap it as a graph with caller-supplied input shapes
597  onnx::GraphProto g;
598  g.set_name(fp.name());
599  for (int j = 0; j < fp.input_size(); ++j) {
600  onnx::ValueInfoProto* vi = g.add_input();
601  vi->set_name(fp.input(j));
602  onnx::TypeProto::Tensor* tt = vi->mutable_type()->mutable_tensor_type();
603  tt->set_elem_type(real_type());
604  tt->mutable_shape()->add_dim()->set_dim_value(in_shapes[j].first);
605  tt->mutable_shape()->add_dim()->set_dim_value(in_shapes[j].second);
606  }
607  for (int k = 0; k < fp.node_size(); ++k) *g.add_node() = fp.node(k);
608  for (int j = 0; j < fp.output_size(); ++j) g.add_output()->set_name(fp.output(j));
609  return function_from_graph(g, name);
610  }
611 
612  std::vector<MX> Onnx::eval_captured_subgraph(const onnx::GraphProto& graph,
613  std::map<std::string, MX> scope) {
614  // No formal inputs: the branch captures outer tensors, already present in `scope`
615  process_graph_initializers(graph, scope, verbose_);
616  process_graph_nodes(graph, scope, verbose_);
617  std::vector<MX> outputs;
618  for (int i = 0; i < graph.output_size(); ++i)
619  outputs.push_back(scope.at(graph.output(i).name()));
620  return outputs;
621  }
622 
623 
624  // Convert a constant tensor input to a vector of integers
625  static std::vector<casadi_int> constant_ints(const MX& m) {
626  casadi_assert(m.is_constant(), "Expected a constant integer tensor");
627  DM dm = static_cast<DM>(m);
628  std::vector<casadi_int> v;
629  for (casadi_int k = 0; k < dm.numel(); ++k)
630  v.push_back(static_cast<casadi_int>(dm(k).scalar()));
631  return v;
632  }
633 
634  // Numerically stable softplus log(1 + exp(x)). Above x = 40, log1p(exp(x)) == x in double;
635  // the clamp keeps exp finite in the discarded branch, so reverse mode gets no 0*inf = NaN.
636  static MX softplus(const MX& x) {
637  return if_else(x > 40, x, log1p(exp(fmin(x, 50))));
638  }
639 
640  // Equal to 1/(1 + exp(-x)), but with no overflow for large |x|
641  static MX sigmoid(const MX& x) {
642  return 0.5 * (1 + tanh(0.5 * x));
643  }
644 
645  // alpha * (exp(x) - 1) for x <= 0, x otherwise. The clamp keeps expm1 finite for large x.
646  static MX elu(const MX& x, double alpha) {
647  return if_else(x > 0, x, alpha * expm1(fmin(x, 1)));
648  }
649 
650  static bool is_activation(const std::string& op_type) {
651  static const std::set<std::string> ops = {"Relu", "LeakyRelu", "PRelu", "Sigmoid",
652  "HardSigmoid", "HardSwish", "Softplus", "Softsign", "Elu", "Selu", "Celu", "Mish", "Gelu",
653  "Clip", "ThresholdedRelu", "Shrink", "Swish"};
654  return ops.count(op_type) > 0;
655  }
656 
657  // Element-wise activation functions, composed of CasADi primitives so that AD and code
658  // generation work unchanged. Non-smooth ones get CasADi's usual subgradients at the kinks.
659  // Attribute defaults are float literals: ONNX float attributes are float32.
660  static MX activation(const std::string& op_type, const onnx::NodeProto& node,
661  const std::vector<MX>& node_inputs) {
662  casadi_assert(node_inputs.size() >= 1, op_type + " requires 1 input");
663  const MX& x = node_inputs[0];
664 
665  if (op_type == "Relu") {
666  return fmax(x, 0);
667  } else if (op_type == "LeakyRelu") {
668  return if_else(x >= 0, x, get_float_attribute(node, "alpha", 0.01f) * x);
669  } else if (op_type == "PRelu") {
670  casadi_assert(node_inputs.size() >= 2, "PRelu requires 2 inputs");
671  MX bx = x, slope = node_inputs[1];
672  broadcast_pair(&bx, &slope);
673  return if_else(bx >= 0, bx, slope * bx);
674  } else if (op_type == "ThresholdedRelu") {
675  return if_else(x > get_float_attribute(node, "alpha", 1.0), x, 0);
676  } else if (op_type == "Shrink") {
677  double lambd = get_float_attribute(node, "lambd", 0.5);
678  double bias = get_float_attribute(node, "bias", 0.0);
679  return if_else(x < -lambd, x + bias, if_else(x > lambd, x - bias, 0));
680  } else if (op_type == "Sigmoid") {
681  return sigmoid(x);
682  } else if (op_type == "Swish") {
683  return x * sigmoid(get_float_attribute(node, "alpha", 1.0) * x);
684  } else if (op_type == "HardSigmoid") {
685  double alpha = get_float_attribute(node, "alpha", 0.2f);
686  double beta = get_float_attribute(node, "beta", 0.5f);
687  return fmax(0, fmin(1, alpha * x + beta));
688  } else if (op_type == "HardSwish") {
689  return x * fmax(0, fmin(1, x / 6 + 0.5));
690  } else if (op_type == "Softplus") {
691  return softplus(x);
692  } else if (op_type == "Softsign") {
693  return x / (1 + fabs(x));
694  } else if (op_type == "Elu") {
695  return elu(x, get_float_attribute(node, "alpha", 1.0));
696  } else if (op_type == "Selu") {
697  double alpha = get_float_attribute(node, "alpha", 1.67326319217681884765625f);
698  double gamma = get_float_attribute(node, "gamma", 1.05070102214813232421875f);
699  return gamma * elu(x, alpha);
700  } else if (op_type == "Celu") {
701  // max(0, x) + min(0, alpha * (exp(x / alpha) - 1)) is, for either sign of alpha, x for
702  // x > 0 and alpha * expm1(x / alpha) otherwise. Written so that it stays C1 at 0.
703  double alpha = get_float_attribute(node, "alpha", 1.0);
704  casadi_assert(alpha != 0, "Celu requires a nonzero alpha");
705  return if_else(x > 0, x, alpha * expm1(fmin(x, std::fabs(alpha)) / alpha));
706  } else if (op_type == "Mish") {
707  return x * tanh(softplus(x));
708  } else if (op_type == "Gelu") {
709  std::string approximate = get_string_attribute(node, "approximate");
710  if (approximate.empty() || approximate == "none") {
711  return 0.5 * x * (1 + erf(x / std::sqrt(2.0)));
712  }
713  casadi_assert(approximate == "tanh",
714  "Gelu: unsupported approximate '" + approximate + "'");
715  return 0.5 * x * (1 + tanh(std::sqrt(2 / pi) * (x + 0.044715 * pow(x, 3))));
716  } else if (op_type == "Clip") {
717  // Opset >= 11: optional inputs min and max (an empty name is absent).
718  // Opset < 11: attributes min and max. An absent bound is not applied.
719  MX output = x;
720  for (size_t k = 1; k <= 2; ++k) {
721  MX bound;
722  if (node_inputs.size() > k) {
723  bound = node_inputs[k];
724  } else {
725  double inf = std::numeric_limits<double>::infinity();
726  double b = get_float_attribute(node, k == 1 ? "min" : "max", k == 1 ? -inf : inf);
727  if (!std::isinf(b)) bound = b;
728  }
729  if (bound.is_empty()) continue;
730  output = k == 1 ? fmax(output, bound) : fmin(output, bound);
731  }
732  return output;
733  }
734  casadi_error("Unsupported activation '" + op_type + "'");
735  }
736 
737  MX Onnx::process_node_operation(
738  const std::string& op_type,
739  const onnx::NodeProto& node,
740  const std::vector<MX>& node_inputs,
741  const IntegerConstants& integer_constants) {
742 
743  auto input_ints = [&](size_t i) -> std::vector<casadi_int> {
744  auto it = integer_constants.find(node.input(i));
745  if (it == integer_constants.end()) return constant_ints(node_inputs[i]);
746  std::vector<casadi_int> values;
747  for (int64_t v : it->second) {
748  casadi_assert(v >= std::numeric_limits<casadi_int>::min() &&
749  v <= std::numeric_limits<casadi_int>::max(), "Integer constant out of range: " + str(v));
750  values.push_back(static_cast<casadi_int>(v));
751  }
752  return values;
753  };
754 
755  MX output;
756 
757  // Simple ops via the centralized lookup table
758  const OpMapping* mapping = get_op_mapping_by_name(op_type);
759  if (mapping) {
760  const MX& x = node_inputs[0];
761  if (mapping->arity == 1) {
762  casadi_assert(node_inputs.size() >= 1, op_type + " requires 1 input");
763  switch (mapping->casadi_op) {
764  case OP_SIN: return sin(x);
765  case OP_COS: return cos(x);
766  case OP_TAN: return tan(x);
767  case OP_ASIN: return asin(x);
768  case OP_ACOS: return acos(x);
769  case OP_ATAN: return atan(x);
770  case OP_SINH: return sinh(x);
771  case OP_COSH: return cosh(x);
772  case OP_TANH: return tanh(x);
773  case OP_ASINH: return asinh(x);
774  case OP_ACOSH: return acosh(x);
775  case OP_ATANH: return atanh(x);
776  case OP_EXP: return exp(x);
777  case OP_LOG: return log(x);
778  case OP_SQRT: return sqrt(x);
779  case OP_NEG: return -x;
780  case OP_FABS: return fabs(x);
781  case OP_CEIL: return ceil(x);
782  case OP_FLOOR: return floor(x);
783  case OP_SIGN: return sign(x);
784  case OP_ERF: return erf(x);
785  case OP_INV: return 1.0 / x;
786  case OP_TRANSPOSE: return x.T();
787  case OP_NORM1: return norm_1(x);
788  case OP_NORM2: return norm_2(x);
789  case OP_NORMF: return norm_fro(x);
790  case OP_MMIN: return mmin(x);
791  case OP_MMAX: return mmax(x);
792  default: break;
793  }
794  } else if (mapping->arity == 2) {
795  casadi_assert(node_inputs.size() >= 2, op_type + " requires 2 inputs");
796  MX bx = x, by = node_inputs[1];
797  broadcast_pair(&bx, &by);
798  switch (mapping->casadi_op) {
799  case OP_ADD: return bx + by;
800  case OP_SUB: return bx - by;
801  case OP_MUL: return bx * by;
802  case OP_DIV: return bx / by;
803  case OP_POW: return pow(bx, by);
804  default: break;
805  }
806  }
807  }
808 
809  // Everything else needs attributes, multiple inputs, etc.
810 
811  if (op_type == "Less") {
812  casadi_assert(node_inputs.size() >= 2, "Less requires 2 inputs");
813  output = if_else(node_inputs[0] < node_inputs[1], MX(1.0), MX(0.0));
814 
815  } else if (op_type == "Equal") {
816  casadi_assert(node_inputs.size() >= 2, "Equal requires 2 inputs");
817  output = !ne(node_inputs[0], node_inputs[1]); // CasADi has ne() but no eq()
818 
819  } else if (op_type == "LessOrEqual") {
820  casadi_assert(node_inputs.size() >= 2, "LessOrEqual requires 2 inputs");
821  output = if_else(node_inputs[0] <= node_inputs[1], MX(1.0), MX(0.0));
822 
823  } else if (op_type == "Greater") {
824  casadi_assert(node_inputs.size() >= 2, "Greater requires 2 inputs");
825  output = if_else(node_inputs[0] > node_inputs[1], MX(1.0), MX(0.0));
826 
827  } else if (op_type == "GreaterOrEqual") {
828  casadi_assert(node_inputs.size() >= 2, "GreaterOrEqual requires 2 inputs");
829  output = if_else(node_inputs[0] >= node_inputs[1], MX(1.0), MX(0.0));
830 
831  } else if (op_type == "Min") {
832  casadi_assert(node_inputs.size() >= 2, "Min requires 2 inputs");
833  output = fmin(node_inputs[0], node_inputs[1]);
834 
835  } else if (op_type == "Max") {
836  casadi_assert(node_inputs.size() >= 2, "Max requires 2 inputs");
837  output = fmax(node_inputs[0], node_inputs[1]);
838 
839  } else if (op_type == "Mod") {
840  casadi_assert(node_inputs.size() >= 2, "Mod requires 2 inputs");
841  output = fmod(node_inputs[0], node_inputs[1]);
842 
843  } else if (op_type == "ReduceSum") {
844  // Sum all elements (ReduceMin/Max/L1/L2 are in the op_map table)
845  casadi_assert(node_inputs.size() >= 1, "ReduceSum requires 1 input");
846  output = sum1(sum2(node_inputs[0]));
847 
848  } else if (op_type == "Not") {
849  casadi_assert(node_inputs.size() >= 1, "Not requires 1 input");
850  output = logic_not(node_inputs[0]);
851 
852  } else if (op_type == "And") {
853  casadi_assert(node_inputs.size() >= 2, "And requires 2 inputs");
854  output = logic_and(node_inputs[0], node_inputs[1]);
855 
856  } else if (op_type == "Or") {
857  casadi_assert(node_inputs.size() >= 2, "Or requires 2 inputs");
858  output = logic_or(node_inputs[0], node_inputs[1]);
859 
860  } else if (op_type == "Where") {
861  casadi_assert(node_inputs.size() >= 3, "Where requires 3 inputs");
862  output = if_else(node_inputs[0], node_inputs[1], node_inputs[2]);
863 
864  } else if (op_type == "Identity") {
865  casadi_assert(node_inputs.size() >= 1, "Identity requires 1 input");
866  output = node_inputs[0];
867 
868  } else if (op_type == "Cast") {
869  // Everything is double in CasADi (converted at the tensor_to_dm boundary), so Cast is an
870  // identity during symbolic computation; the 'to' attribute is irrelevant here.
871  casadi_assert(node_inputs.size() >= 1, "Cast requires 1 input");
872  output = node_inputs[0];
873 
874  } else if (op_type == "MatMul") {
875  // Transpose-rep: ONNX MatMul(P,Q)=P*Q is the transpose-rep of q*p, so swap operands.
876  casadi_assert(node_inputs.size() >= 2, "MatMul requires 2 inputs");
877  output = mtimes(node_inputs[1], node_inputs[0]);
878 
879  } else if (op_type == "Gemm") {
880  // Transpose-rep: the stored result is (opQ(Q)^T)*(opP(P)^T); recover the CasADi value by
881  // swapping operands and the transA/transB roles.
882  casadi_assert(node_inputs.size() >= 2, "Gemm requires at least 2 inputs");
883  MX A = get_int_attribute(node, "transB", 0) ? node_inputs[1].T() : node_inputs[1];
884  MX B = get_int_attribute(node, "transA", 0) ? node_inputs[0].T() : node_inputs[0];
885  output = get_float_attribute(node, "alpha", 1.0) * mtimes(A, B);
886  if (node_inputs.size() >= 3) {
887  MX C = node_inputs[2]; // unidirectionally broadcastable to the result, e.g. a bias [N]
888  broadcast_pair(&output, &C);
889  output = output + get_float_attribute(node, "beta", 1.0) * C;
890  }
891 
892  } else if (op_type == "Sum") {
893  // Variadic elementwise sum
894  casadi_assert(node_inputs.size() >= 1, "Sum requires at least 1 input");
895  output = node_inputs[0];
896  for (casadi_int idx = 1; idx < node_inputs.size(); ++idx) output = output + node_inputs[idx];
897 
898  } else if (op_type == "Pad") {
899  // Inverse of the block-diagonal Pad: drop the (dense) block into a zero frame at its offset.
900  // Transpose-rep: pads are [axis0_begin, axis1_begin, axis0_end, axis1_end] on the stored
901  // (C x R) tensor, i.e. [col_off, row_off, col_end, row_end].
902  casadi_assert(node_inputs.size() >= 2, "Pad requires data and pads");
903  MX block = node_inputs[0];
904  std::vector<casadi_int> pads = constant_ints(node_inputs[1]);
905  casadi_int col_off = pads[0], row_off = pads[1];
906  casadi_int br = block.size1(), bc = block.size2();
907  casadi_int C = col_off + bc + pads[2];
908  // Pad with SPARSE zero blocks via horz/vertcat: the block's pattern is preserved and no
909  // project/densify is introduced (a triangular block stays triangular), so the block matrix's
910  // sparsity emerges natively from CasADi's own propagation.
911  MX padded = block;
912  if (col_off > 0 || pads[2] > 0) {
913  padded = horzcat(MX(Sparsity(br, col_off)), padded, MX(Sparsity(br, pads[2])));
914  }
915  if (row_off > 0 || pads[3] > 0) {
916  padded = vertcat(MX(Sparsity(row_off, C)), padded, MX(Sparsity(pads[3], C)));
917  }
918  output = padded;
919 
920  } else if (op_type == "Einsum") {
921  // Reconstruct a CasADi einstein contraction (inverse of the export envelope). The
922  // operands arrive as the 2-D reshaped tensors; their column-major vec is the original.
923  casadi_assert(node_inputs.size() >= 2, "Einsum requires 2 inputs");
924  std::string eq;
925  for (char ch : get_string_attribute(node, "equation")) if (ch != ' ') eq += ch;
926  size_t comma = eq.find(','), arrow = eq.find("->");
927  casadi_assert(comma != std::string::npos && arrow != std::string::npos,
928  "ONNX import: only binary Einsum 'a,b->c' is supported");
929  std::string sa = eq.substr(0, comma);
930  std::string sb = eq.substr(comma + 1, arrow - comma - 1);
931  std::string sc = eq.substr(arrow + 2);
932 
933  // Transpose-rep: the exporter reversed each subscript to label the axis-reversed ONNX
934  // tensors. node_inputs here are the imported CasADi values (transposed back to LOGICAL axis
935  // order), so reverse the subscripts back to natural order to pair them with the logical axes
936  // (size1,size2).
937  std::reverse(sa.begin(), sa.end());
938  std::reverse(sb.begin(), sb.end());
939  std::reverse(sc.begin(), sc.end());
940 
941  // Each letter's size, read from the operand shapes (axes follow the subscript order). A
942  // single-index (vector) operand is stored as a rank-1 ONNX tensor that imports with an
943  // ambiguous 1xN/Nx1 orientation, so take its length from numel() (orientation-independent);
944  // a 2-index operand reads size1/size2 directly.
945  std::map<char, casadi_int> lsize;
946  for (int t = 0; t < 2; ++t) {
947  const std::string& s = (t == 0) ? sa : sb;
948  const MX& m = node_inputs[t];
949  if (s.size() == 1) {
950  lsize[s[0]] = m.numel();
951  } else if (s.size() >= 2) {
952  lsize[s[0]] = m.size1();
953  lsize[s[1]] = m.size2();
954  }
955  }
956  // Assign a label to each distinct letter
957  std::map<char, casadi_int> lab;
958  casadi_int next_label = -1;
959  for (char ch : sa + sb + sc) if (!lab.count(ch)) lab[ch] = next_label--;
960 
961  // Labels in natural order; operands' column-major vec is the original einstein input
962  std::vector<casadi_int> da, db, dc, La, Lb, Lc;
963  for (char ch : sa) { da.push_back(lsize[ch]); La.push_back(lab[ch]); }
964  for (char ch : sb) { db.push_back(lsize[ch]); Lb.push_back(lab[ch]); }
965  for (char ch : sc) { dc.push_back(lsize[ch]); Lc.push_back(lab[ch]); }
966  output = einstein(vec(node_inputs[0]), vec(node_inputs[1]), da, db, dc, La, Lb, Lc);
967 
968  } else if (op_type == "Det") {
969  casadi_assert(node_inputs.size() >= 1, "Det requires 1 input");
970  output = det(node_inputs[0]);
971 
972  } else if (op_type == "ReduceLogSumExp") {
973  // log(sum(exp(x))) over all elements
974  casadi_assert(node_inputs.size() >= 1, "ReduceLogSumExp requires 1 input");
975  output = log(sum1(sum2(exp(node_inputs[0]))));
976 
977  } else if (op_type == "Constant") {
978  // ONNX permits tensor, sparse tensor, and scalar/vector numeric attributes.
979  const onnx::AttributeProto* value_attr = nullptr;
980  for (int a = 0; a < node.attribute_size(); ++a) {
981  const auto& attr = node.attribute(a);
982  const std::string& an = attr.name();
983  if (an == "value" || an == "sparse_value" || an == "value_int" ||
984  an == "value_ints" || an == "value_float" || an == "value_floats") {
985  casadi_assert(value_attr == nullptr, "Constant node has multiple value attributes");
986  value_attr = &attr;
987  }
988  }
989  casadi_assert(value_attr != nullptr,
990  "Constant node requires a numeric value or sparse_value attribute");
991  const auto& attr = *value_attr;
992  if (attr.name() == "sparse_value") {
993  output = MX(sparse_tensor_to_dm(attr.sparse_tensor()));
994  } else if (attr.name() == "value") {
995  output = MX(tensor_to_dm(attr.t()));
996  } else if (attr.name() == "value_int") {
997  output = MX(static_cast<double>(attr.i()));
998  } else if (attr.name() == "value_float") {
999  output = MX(static_cast<double>(attr.f()));
1000  } else {
1001  std::vector<double> values;
1002  if (attr.name() == "value_ints") {
1003  for (auto v : attr.ints()) values.push_back(static_cast<double>(v));
1004  } else {
1005  for (auto v : attr.floats()) values.push_back(static_cast<double>(v));
1006  }
1007  output = MX(DM(values));
1008  }
1009 
1010  // Complex tensor operations (Transpose is handled by the op_map table)
1011  } else if (op_type == "Reshape") {
1012  casadi_assert(node_inputs.size() >= 2,
1013  "Reshape operation requires 2 inputs (data and shape)");
1014  // Second input is the target shape - should be a constant
1015  casadi_assert(node_inputs[1].is_constant(),
1016  "Reshape shape must be a constant");
1017  std::vector<casadi_int> shape = input_ints(1);
1018  // ONNX Reshape is PSEUDO-DENSE (it operates on the full numel), so densify the operand here:
1019  // this is the ONE place the imported graph must drop sparsity, because CasADi's own reshape
1020  // PRESERVES it (a sparse operand would reshape to a flat with only nnz entries, shifting
1021  // every
1022  // downstream Gather/Scatter index). True sparsity propagates everywhere else.
1023  if (shape.size() > 2) {
1024  // 3-D reshape (the Map/Scan lift envelope): CasADi is 2-D, so pass through unchanged
1025  output = densify(node_inputs[0]);
1026  } else {
1027  // Transpose-rep: the ONNX target (s0,s1) is the reverse of the CasADi target.
1028  casadi_int s0 = shape.empty() ? 1 : shape[0];
1029  casadi_int s1 = shape.size() > 1 ? shape[1] : 1;
1030  output = reshape(densify(node_inputs[0]), s1, s0);
1031  }
1032 
1033  } else if (op_type == "Unsqueeze") {
1034  casadi_assert(!node_inputs.empty() && node_inputs[0].is_vector(),
1035  "Unsqueeze: only scalar and vector operands are supported");
1036  std::vector<casadi_int> axes;
1037  if (node_inputs.size() > 1) {
1038  axes = constant_ints(node_inputs[1]);
1039  } else {
1040  for (const auto& attr : node.attribute()) {
1041  if (attr.name() == "axes") axes.assign(attr.ints().begin(), attr.ints().end());
1042  }
1043  }
1044  casadi_assert(axes.size() == 1 && axes[0] >= -2 && axes[0] < 2,
1045  "Unsqueeze: expected one axis for a 2-D result");
1046  casadi_int axis = axes[0] < 0 ? axes[0] + 2 : axes[0];
1047  output = axis == 0 ? reshape(node_inputs[0], node_inputs[0].numel(), 1) :
1048  reshape(node_inputs[0], 1, node_inputs[0].numel());
1049 
1050  } else if (op_type == "Concat") {
1051  // Transpose-rep: ONNX axis 0 is CasADi columns (horzcat), axis 1 is CasADi rows (vertcat)
1052  casadi_int axis = get_int_attribute(node, "axis", 0);
1053  if (axis == 0) {
1054  output = horzcat(node_inputs);
1055  } else if (axis == 1) {
1056  output = vertcat(node_inputs);
1057  } else {
1058  casadi_error("Concat with axis=" + std::to_string(axis) +
1059  " not supported. Only axis=0 (vertcat) and axis=1 (horzcat) are supported.");
1060  }
1061 
1062  } else if (op_type == "Slice") {
1063  // Inputs: data, starts, ends, [axes], [steps].
1064  casadi_assert(node_inputs.size() >= 3,
1065  "Slice requires at least 3 inputs (data, starts, ends)");
1066 
1067  MX data = node_inputs[0];
1068  casadi_assert(node_inputs[1].is_constant() && node_inputs[2].is_constant(),
1069  "Slice starts and ends must be constants");
1070 
1071  DM starts_dm = static_cast<DM>(node_inputs[1]);
1072  DM ends_dm = static_cast<DM>(node_inputs[2]);
1073 
1074  // Axes (default [0, 1, ...]) and steps (default all 1s)
1075  std::vector<casadi_int> axes, steps;
1076  if (node_inputs.size() >= 4 && !node_inputs[3].is_empty()) {
1077  axes = input_ints(3);
1078  } else {
1079  for (casadi_int k = 0; k < starts_dm.numel(); ++k) axes.push_back(k);
1080  }
1081  if (node_inputs.size() >= 5 && !node_inputs[4].is_empty()) {
1082  steps = input_ints(4);
1083  } else {
1084  steps.assign(starts_dm.numel(), 1);
1085  }
1086 
1087  casadi_assert(axes.size() <= 2, "Slice: only up to 2D slicing supported");
1088  casadi_assert(starts_dm.numel() == axes.size() && ends_dm.numel() == axes.size() &&
1089  steps.size() == axes.size(), "Slice: starts, ends, axes and steps must have equal lengths");
1090 
1091  // Transpose-rep: ONNX axis 0 is CasADi columns, axis 1 is CasADi rows.
1092  Slice row_slice, col_slice;
1093  casadi_int nrow = data.size1(), ncol = data.size2();
1094  for (casadi_int a = 0; a < static_cast<casadi_int>(axes.size()); ++a) {
1095  casadi_int axis = axes[a] < 0 ? axes[a] + 2 : axes[a];
1096  casadi_assert(axis == 0 || axis == 1, "Slice: axis out of range");
1097  casadi_int n = axis == 0 ? ncol : nrow;
1098  casadi_int sp = steps[a];
1099  casadi_assert(sp != 0, "Slice: step must not be zero");
1100  sp = std::max(-std::max(n, casadi_int(1)), std::min(sp, std::max(n, casadi_int(1))));
1101  auto bound = [&](size_t input, const DM& dm) -> casadi_int {
1102  auto it = integer_constants.find(node.input(input));
1103  if (it != integer_constants.end()) {
1104  int64_t v = it->second.at(a);
1105  if (v < 0) v += n;
1106  return static_cast<casadi_int>(std::max<int64_t>(sp > 0 ? 0 : -1,
1107  std::min<int64_t>(v, sp > 0 ? n : n - 1)));
1108  }
1109  // Preserve the existing path for computed constants; clip before narrowing.
1110  double v = dm(a).scalar();
1111  if (v < 0) v += n;
1112  return static_cast<casadi_int>(std::max(sp > 0 ? 0. : -1.,
1113  std::min(v, static_cast<double>(sp > 0 ? n : n - 1))));
1114  };
1115  casadi_int st = bound(1, starts_dm);
1116  casadi_int en = bound(2, ends_dm);
1117  // CasADi uses an unbounded stop to represent -1 for a backwards slice.
1118  Slice indices = (sp > 0 ? st >= en : st <= en) ? Slice(0, 0) :
1119  Slice(st, en == -1 ? std::numeric_limits<casadi_int>::max() : en, sp);
1120  if (axis == 0) {
1121  col_slice = indices;
1122  } else {
1123  row_slice = indices;
1124  }
1125  }
1126  output = data(row_slice, col_slice);
1127 
1128  } else if (op_type == "Gather") {
1129  // Extract element(s) at the given indices along an axis.
1130  casadi_assert(node_inputs.size() >= 2, "Gather requires data and indices");
1131 
1132  // Gather indexes the pseudo-dense column-major layout -> densify a sparse operand.
1133  MX data = densify(node_inputs[0]);
1134  MX indices_mx = node_inputs[1];
1135  casadi_int axis = get_int_attribute(node, "axis", 0);
1136 
1137  casadi_assert(indices_mx.is_constant(), "Gather indices must be constant");
1138  std::vector<casadi_int> indices = input_ints(1);
1139 
1140  // Keep even scalar indices in an integer vector: Matrix's scalar constructor takes double.
1141  if (data.size2() == 1 || axis == 1) {
1142  output = data(indices, Slice());
1143  } else if (axis == 0) {
1144  output = data(Slice(), indices);
1145  } else {
1146  casadi_error("Gather: only axis 0 and 1 supported for 2D tensors");
1147  }
1148 
1149  } else if (op_type == "ScatterElements") {
1150  // data with data.nz[indices] {=,+=} updates -- nz indexing is pseudo-dense, so densify the
1151  // target. Constant indices -> (set/add)nonzeros; runtime indices -> the _PARAM variants.
1152  casadi_assert(node_inputs.size() >= 3, "ScatterElements requires data, indices, updates");
1153  MX se_data = densify(node_inputs[0]);
1154  MX se_idx = node_inputs[1];
1155  MX se_upd = vec(node_inputs[2]);
1156  if (get_string_attribute(node, "reduction") == "add") {
1157  // {add,addparam}nonzeros: out = data with data.nz[idx] += updates, accumulating DUPLICATE
1158  // idx. No direct MX builder; it is the reverse-mode adjoint of a (parametric)
1159  // getnonzeros --
1160  // jtimes(w.nz[idx], w, updates, true) is the scatter-add (dups summed) into a zero frame.
1161  MX w = MX::sym("scel_w", se_data.numel(), 1), g;
1162  if (se_idx.is_constant()) {
1163  w.get_nz(g, false, Matrix<casadi_int>(constant_ints(se_idx)));
1164  } else {
1165  w.get_nz(g, false, vec(se_idx)); // runtime indices -> getnonzeros_param
1166  }
1167  output = se_data + reshape(jtimes(g, w, se_upd, true), se_data.size1(), se_data.size2());
1168  } else {
1169  output = se_data;
1170  if (se_idx.is_constant()) {
1171  output.set_nz(se_upd, false, Matrix<casadi_int>(constant_ints(se_idx)));
1172  } else {
1173  output.set_nz(se_upd, false, vec(se_idx)); // runtime indices -> setnonzeros_param
1174  }
1175  }
1176 
1177  } else if (op_type == "ScatterND") {
1178  // setnonzeros: overwrite x's cells at the given (onnx_row, onnx_col) coordinates. With x
1179  // stored as x^T, coord (a,b) is CasADi cell (b,a) -> column-major position a*nrow+b, which is
1180  // exactly the original nz position, so reconstruct via set_nz on the 2-D value.
1181  casadi_assert(node_inputs.size() >= 3, "ScatterND requires data, indices, updates");
1182  MX data = densify(node_inputs[0]); // nz indexing is pseudo-dense -> densify the target
1183  std::vector<casadi_int> coords = constant_ints(node_inputs[1]); // [r0,c0,r1,c1,...]
1184  casadi_int R = data.size1();
1185  std::vector<casadi_int> pos(coords.size() / 2);
1186  for (casadi_int i = 0; i < static_cast<casadi_int>(pos.size()); ++i) {
1187  pos[i] = coords[2 * i] * R + coords[2 * i + 1];
1188  }
1189  if (get_string_attribute(node, "reduction") == "add") {
1190  // ADDNONZEROS: out = data with out.nz[pos] += updates, accumulating DUPLICATE pos. An
1191  // addnonzeros MX cannot be built directly; it is the reverse-mode adjoint of a getnonzeros.
1192  // Build g = w.nz[pos] on a dense numel-vector w, then jtimes(g, w, updates, /*tr*/true) is
1193  // exactly the scatter-add of updates into a zero frame (duplicates summed); add it to data.
1194  MX w = MX::sym("addnz_w", data.numel(), 1), g;
1195  w.get_nz(g, false, Matrix<casadi_int>(pos));
1196  MX scatter = jtimes(g, w, vec(node_inputs[2]), true);
1197  output = data + reshape(scatter, data.size1(), data.size2());
1198  } else {
1199  output = data;
1200  output.set_nz(node_inputs[2], false, Matrix<casadi_int>(pos));
1201  }
1202 
1203  } else if (op_type == "Tile") {
1204  // Repeat the tensor along each dimension by repeats = [rows, cols].
1205  casadi_assert(node_inputs.size() >= 2, "Tile requires data and repeats inputs");
1206  MX data = node_inputs[0];
1207  MX repeats_mx = node_inputs[1];
1208  casadi_assert(repeats_mx.is_constant(), "Tile repeats must be constant");
1209  DM repeats_dm = static_cast<DM>(repeats_mx);
1210 
1211  casadi_int rows_repeat = 1, cols_repeat = 1;
1212  if (repeats_dm.numel() >= 1) rows_repeat = static_cast<casadi_int>(repeats_dm(0).scalar());
1213  if (repeats_dm.numel() >= 2) cols_repeat = static_cast<casadi_int>(repeats_dm(1).scalar());
1214 
1215  // Transpose-rep: ONNX repeats [rows,cols] map to CasADi repmat(data, cols, rows)
1216  output = repmat(data, cols_repeat, rows_repeat);
1217 
1218  } else if (op_type == "GatherElements") {
1219  // out[..,j,..] = data[.., idx[..,j,..], ..] along `axis`. The export of OP_GETNONZEROS_PARAM
1220  // emits this on a 1 x N flat row with a 1 x M index row (-> imported as N x 1 / M x 1 cols),
1221  // which is exactly the parametric getnonzeros y = data.nz[idx]. Reconstruct via get_nz so the
1222  // sparse/runtime composition (loc-mapping gather then data gather) rebuilds the right MX.
1223  casadi_assert(node_inputs.size() >= 2, "GatherElements requires data and indices");
1224  MX data = densify(node_inputs[0]); // nz indexing is pseudo-dense -> densify the operand
1225  MX indices_mx = node_inputs[1];
1226  // Orient indices as a column vector of nz-indices (a 1 x M ONNX row imports as M x 1).
1227  MX idx_vec = vec(indices_mx);
1228  MX flat = vec(data); // column-major flat of data: flat.nz[k] = data's k-th nonzero
1229  if (indices_mx.is_constant()) {
1230  // Constant indices -> the plain constant getnonzeros (OP_GETNONZEROS) path.
1231  flat.get_nz(output, false, Matrix<casadi_int>(constant_ints(indices_mx)));
1232  } else {
1233  // Runtime indices -> parametric getnonzeros (OP_GETNONZEROS_PARAM).
1234  flat.get_nz(output, false, idx_vec);
1235  }
1236 
1237  } else if (is_activation(op_type)) {
1238  output = activation(op_type, node, node_inputs);
1239 
1240  } else {
1241  casadi_error("Unsupported operation '" + op_type + "'");
1242  }
1243 
1244  return output;
1245  }
1246 
1247 } // namespace casadi
Function object.
Definition: function.hpp:126
casadi_int numel() const
Get the number of elements, size1()*size2()
bool is_empty(bool both=false) const
Check if the matrix is empty, i.e. if one of the dimensions is zero.
bool is_vector() const
Check if the matrix is a row or column vector.
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)
static MX sym(const std::string &name, casadi_int nrow=1, casadi_int ncol=1)
Create an nrow-by-ncol symbolic primitive.
static MX zeros(casadi_int nrow=1, casadi_int ncol=1)
Create a dense matrix or a matrix with specified sparsity with all entries zero.
bool verbose_
Verbose – for debugging.
casadi_int numel() const
Get shape.
Definition: mx_node.hpp:426
casadi_int size2() const
Definition: mx_node.hpp:429
casadi_int size1() const
Definition: mx_node.hpp:428
MX - Matrix expression.
Definition: mx.hpp:145
static MX kron(const MX &x, const MX &b)
Definition: mx.cpp:2070
bool is_constant() const
Check if the node is a constant.
Definition: mx.cpp:802
void get_nz(MX &m, bool ind1, const Slice &kk) const
Get a set of nonzeros (what x.nz[k] does)
Definition: mx.cpp:406
Function create(const std::string &name)
Create a CasADi Function from the loaded ONNX graph.
onnx::ModelProto model_
ONNX model protocol buffer.
Definition: onnx_model.hpp:111
bool has_model_
Whether a model has been loaded.
Definition: onnx_model.hpp:117
The casadi namespace.
Definition: archiver.cpp:28
template class CASADI_EXPORT Matrix< casadi_int >
static std::vector< casadi_int > constant_ints(const MX &m)
static bool broadcastable(const MX &a, const MX &b)
Definition: onnx_import.cpp:64
T norm_1(const std::vector< T > &x)
static MX activation(const std::string &op_type, const onnx::NodeProto &node, const std::vector< MX > &node_inputs)
double if_else(double x, double y, double z)
Definition: calculus.hpp:296
double sign(double x)
Sign function, note that sign(nan) == nan.
Definition: calculus.hpp:270
double get_float_attribute(const onnx::NodeProto &node, const std::string &name, double default_value)
Read a float node attribute by name, or default_value if absent.
static bool is_activation(const std::string &op_type)
const onnx::GraphProto * get_graph_attribute(const onnx::NodeProto &node, const std::string &name)
Read a subgraph (GraphProto) node attribute by name, or nullptr if absent.
static MX elu(const MX &x, double alpha)
static void orient_vector(MX *v, const MX &o)
Definition: onnx_import.cpp:70
static bool has_unresolved_negative_axis(const onnx::GraphProto &graph)
static MX sigmoid(const MX &x)
std::string str(const T &v)
String representation, any type.
static void broadcast_pair(MX *x, MX *y)
Definition: onnx_import.cpp:88
const double inf
infinity
Definition: calculus.hpp:50
void copy(const T1 *x, casadi_int n, T2 *y)
static bool integer_tensor(const onnx::TensorProto &tensor, std::vector< int64_t > &values)
Definition: onnx_import.cpp:34
const OpMapping * get_op_mapping_by_name(const std::string &onnx_name)
Lookup operation mapping by ONNX name (for import)
casadi_int get_int_attribute(const onnx::NodeProto &node, const std::string &name, casadi_int default_value)
Read an integer node attribute by name, or default_value if absent.
T norm_2(const std::vector< T > &x)
static const onnx::NodeProto & resolve_negative_axis(const onnx::NodeProto &node, const std::map< std::string, casadi_int > &ranks, onnx::NodeProto *copy)
std::string get_string_attribute(const onnx::NodeProto &node, const std::string &name)
Read a string node attribute by name, or "" if absent.
static MX softplus(const MX &x)
Matrix< double > DM
Definition: dm_fwd.hpp:33
std::ostream & uout()
@ OP_SIGN
Definition: calculus.hpp:71
@ OP_COS
Definition: calculus.hpp:68
@ OP_ERF
Definition: calculus.hpp:72
@ OP_SINH
Definition: calculus.hpp:74
@ OP_COSH
Definition: calculus.hpp:74
@ OP_ASINH
Definition: calculus.hpp:75
@ OP_ACOS
Definition: calculus.hpp:69
@ OP_MMAX
Definition: calculus.hpp:181
@ OP_ATAN
Definition: calculus.hpp:69
@ OP_SQRT
Definition: calculus.hpp:67
@ OP_INV
Definition: calculus.hpp:73
@ OP_EXP
Definition: calculus.hpp:66
@ OP_MMIN
Definition: calculus.hpp:181
@ OP_SIN
Definition: calculus.hpp:68
@ OP_ASIN
Definition: calculus.hpp:69
@ OP_ACOSH
Definition: calculus.hpp:75
@ OP_CEIL
Definition: calculus.hpp:71
@ OP_SUB
Definition: calculus.hpp:65
@ OP_ATANH
Definition: calculus.hpp:75
@ OP_POW
Definition: calculus.hpp:66
@ OP_FABS
Definition: calculus.hpp:71
@ OP_LOG
Definition: calculus.hpp:66
@ OP_TANH
Definition: calculus.hpp:74
@ OP_NORM1
Definition: calculus.hpp:178
@ OP_ADD
Definition: calculus.hpp:65
@ OP_NORM2
Definition: calculus.hpp:178
@ OP_DIV
Definition: calculus.hpp:65
@ OP_TRANSPOSE
Definition: calculus.hpp:106
@ OP_FLOOR
Definition: calculus.hpp:71
@ OP_NEG
Definition: calculus.hpp:66
@ OP_MUL
Definition: calculus.hpp:65
@ OP_NORMF
Definition: calculus.hpp:178
@ OP_TAN
Definition: calculus.hpp:68
static std::map< std::string, casadi_int > declared_ranks(const onnx::GraphProto &graph)
const double pi
Define pi.
Definition: calculus.hpp:46