blazing_spline.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 "blazing_spline_impl.hpp"
27 #include "interpolant_impl.hpp"
28 #include "bspline_impl.hpp"
29 #include "casadi_misc.hpp"
30 #include "serializer.hpp"
31 
32 #include <fstream>
33 #include <iostream>
34 #include <sstream>
35 
36 namespace casadi {
37 
38  static void handle_pedantic(const std::string& mode,
39  const std::string& opt_name,
40  const std::string& msg) {
41  if (mode == "ignore") return;
42  std::string full = msg + "\n(Controlled by option '" + opt_name +
43  "'; set to 'ignore' to silence, 'warn' to demote, 'error' to escalate.)";
44  if (mode == "warn") {
45  casadi_warning(full);
46  } else if (mode == "error") {
47  casadi_error(full);
48  } else {
49  casadi_error("Option '" + opt_name + "' must be one of "
50  "'ignore', 'warn', 'error'; got '" + mode + "'.");
51  }
52  }
53 
54  static bool is_pow2_ge8(casadi_int n) {
55  return n >= 8 && (n & (n - 1)) == 0;
56  }
57 
58  // Cumulative prefix products of length >= 2 of an extent vector.
59  // Length-1 prefixes are the individual extents themselves and are
60  // reported separately at the call site.
61  static void scan_prefix_pow2(std::vector<std::string>& offenders,
62  const std::vector<casadi_int>& ext,
63  const std::string& tag) {
64  casadi_int p = 1;
65  for (size_t i = 0; i < ext.size(); ++i) {
66  p *= ext[i];
67  if (i >= 1 && is_pow2_ge8(p)) {
68  offenders.push_back("prefix product over dims 0.." + str(i)
69  + " = " + str(p) + tag);
70  }
71  }
72  }
73 
74  static std::vector<casadi_int> knot_offsets(const std::vector<casadi_int>& knot_dims) {
75  std::vector<casadi_int> offsets(knot_dims.size() + 1);
76  offsets[0] = 0;
77  for (size_t i = 0; i < knot_dims.size(); ++i)
78  offsets[i + 1] = offsets[i] + knot_dims[i];
79  return offsets;
80  }
81 
82  // Build the per-dimension knot cache consumed by the blazing runtime.
83  // Each dim's slice has layout [intercept, slope, inv1[n_k], inv2[n_k], inv3[n_k]]:
84  // slope = (ng-1) / (grid[ng-1] - grid[0]) with grid = K[degree:n_k-degree]
85  // intercept = -grid[0] * slope
86  // invS[k] = 1/(t[k+S] - t[k]) (zeroed where the span collapses)
87  // The two scalars feed 'exact' lookup_mode (single FMA, no FP divide); the
88  // inv spans feed the de Boor recurrence. The scalars are emitted
89  // unconditionally so the runtime layout stays uniform regardless of
90  // lookup_mode.
91  template<typename M>
92  static M compute_knots_cache(const M& K, const std::vector<casadi_int>& offsets) {
93  casadi_int nd = offsets.size() - 1;
94  casadi_int degree = 3;
95  std::vector<M> parts;
96  for (casadi_int d = 0; d < nd; ++d) {
97  casadi_int off = offsets[d];
98  casadi_int n_k = offsets[d + 1] - off;
99  M t = K(Slice(off, off + n_k));
100 
101  // {intercept, slope} for 'exact' lookup. ng is the length of the
102  // searched grid (K[degree : n_k-degree]).
103  casadi_int ng = n_k - 2*degree;
104  M slope, intercept;
105  if (ng >= 2) {
106  M g0 = t(degree);
107  M dg = t(n_k - degree - 1) - g0;
108  // dg=0 would mean a degenerate grid; guard the divide.
109  slope = if_else_zero(dg, static_cast<double>(ng-1) / (dg + 1e-100));
110  intercept = -g0 * slope;
111  } else {
112  slope = M::zeros(1, 1);
113  intercept = M::zeros(1, 1);
114  }
115  parts.push_back(intercept);
116  parts.push_back(slope);
117 
118  // Existing inv1/inv2/inv3 spans.
119  for (casadi_int span = 1; span <= 3; ++span) {
120  M inv_span;
121  if (span < n_k) {
122  M diff = t(Slice(span, n_k)) - t(Slice(0, n_k - span));
123  diff = vertcat(diff, M::zeros(span, 1));
124  inv_span = if_else_zero(diff, 1.0 / (diff + 1e-100));
125  } else {
126  inv_span = M::zeros(n_k, 1);
127  }
128  parts.push_back(inv_span);
129  }
130  }
131  return vertcat(parts);
132  }
133 
134  // MX version of BSplineCommon::derivative_coeff for parametric knots.
135  // Builds the bidiagonal transformation matrix T from symbolic knots
136  // and applies it to the coefficient tensor along axis i.
137  static MX derivative_coeff_mx(casadi_int i,
138  const std::vector<MX>& knots_per_dim,
139  const std::vector<casadi_int>& degree,
140  const std::vector<casadi_int>& coeffs_dims,
141  const MX& coeffs) {
142  casadi_int n_k = knots_per_dim[i].size1();
143  casadi_int n = n_k - degree[i] - 1;
144 
145  MX K_i = knots_per_dim[i];
146  MX delta_knots = K_i(range(1+degree[i], n_k-1))
147  - K_i(range(1, n_k-degree[i]-1));
148  MX d = static_cast<double>(degree[i]) / delta_knots; // length n-1
149 
150  // T = diag(-d) + upper_band(+d) is a scaled finite-difference operator.
151  // Apply via slice-subtract + broadcast-multiply — no T, no kron, no densify.
152  std::vector<casadi_int> coeffs_dims_new = coeffs_dims;
153  coeffs_dims_new[i+1] = n - 1;
154 
155  casadi_int L = 1, R = 1;
156  for (casadi_int k=0; k<=i; ++k) L *= coeffs_dims[k];
157  for (casadi_int k=i+2; k<(casadi_int)coeffs_dims.size(); ++k) R *= coeffs_dims[k];
158  casadi_int K = coeffs_dims[i+1];
159  casadi_int Kp = n - 1;
160 
161  MX M_coeffs = reshape(coeffs, L*K, R);
162  MX top = M_coeffs(Slice(L, L*K), Slice());
163  MX bot = M_coeffs(Slice(0, L*(K-1)), Slice());
164  MX diffed = top - bot;
165 
166  std::vector<casadi_int> dims{L, Kp, R};
167  std::vector<casadi_int> a{-1, -2, -3};
168  std::vector<casadi_int> b{-2};
169  std::vector<casadi_int> c{-1, -2, -3};
170  return MX::einstein(vec(diffed), d,
171  dims, std::vector<casadi_int>{Kp}, dims,
172  a, b, c);
173  }
174 
175  Function blazing_spline(const std::string& name,
176  const std::vector< std::vector<double> >& knots,
177  const Dict& opts) {
178  return Function::create(new BlazingSplineFunction(name, knots, 0), opts);
179  }
180 
181  Function blazing_spline(const std::string& name,
182  const std::vector<casadi_int>& knot_dims,
183  const Dict& opts) {
184  bool precompute_coeff = false, precompute_grid = false;
185  auto it = opts.find("precompute_coeff");
186  if (it != opts.end()) precompute_coeff = it->second;
187  it = opts.find("precompute_grid");
188  if (it != opts.end()) precompute_grid = it->second;
189  bool use_inv = precompute_grid;
190  Function F_inner = Function::create(
191  new BlazingSplineFunction(name, knot_dims, 0,
192  precompute_coeff, precompute_grid, use_inv), opts);
193  if (!use_inv) return F_inner;
194 
195  // Wrap: user sees (x, C, knots), wrapper computes inv and calls inner
196  std::vector<casadi_int> offsets = knot_offsets(knot_dims);
197  casadi_int nd = knot_dims.size();
198  casadi_int nk = offsets.back();
199  MX x = MX::sym("x", nd);
200  MX C = MX::sym("C", F_inner.size_in(1));
201  MX knots = MX::sym("knots", nk);
202  MX inv = compute_knots_cache(knots, offsets);
203  std::vector<MX> ret = F_inner(std::vector<MX>{x, C, knots, inv});
204  return Function(name, {x, C, knots}, ret,
205  {"x", "C", "knots"}, F_inner.name_out(),
206  {{"always_inline", true}});
207  }
208 
210  casadi_int n = 2; // x, C
211  if (precompute_coeff_) n += diff_order_; // dC, ddC
212  if (has_parametric_knots()) n += 1; // knots
213  if (inv_input_) n += 1; // inv
214  return n;
215  }
217  return diff_order_+1;
218  }
219 
221  return i==0;
222  }
223 
225  if (i==0) {
226  return Sparsity::dense(ndim());
227  } else if (i==1) {
228  return Sparsity::dense(nc_);
229  } else if (has_parametric_knots() && i==arg_knots()) {
230  return Sparsity::dense(knots_offset_.back());
231  } else if (inv_input_ && i==arg_inv()) {
232  // Per-dim slice = 2 (intercept,slope) + 3*n_k (inv1,inv2,inv3).
233  casadi_int nd = knots_offset_.size() - 1;
234  return Sparsity::dense(2 * nd + 3 * knots_offset_.back());
235  } else if (precompute_coeff_ && i==2+has_parametric_knots()+inv_input_) {
236  return Sparsity::dense(ndc_);
237  } else if (precompute_coeff_ && i==3+has_parametric_knots()+inv_input_) {
238  return Sparsity::dense(nddc_);
239  } else {
240  casadi_assert_dev(false);
241  return Sparsity();
242  }
243  }
245  if (i==0) {
246  return Sparsity::dense(1, 1);
247  } else if (i==1) {
248  return Sparsity::dense(1, ndim());
249  } else if (i==2) {
250  return Sparsity::dense(ndim(), ndim());
251  } else {
252  casadi_assert_dev(false);
253  return Sparsity();
254  }
255  }
256 
257  std::string BlazingSplineFunction::get_name_in(casadi_int i) {
258  if (i==0) {
259  return "x";
260  } else if (i==1) {
261  return "C";
262  } else if (has_parametric_knots() && i==arg_knots()) {
263  return "knots";
264  } else if (inv_input_ && i==arg_inv()) {
265  return "inv";
266  } else if (precompute_coeff_ && i==2+has_parametric_knots()+inv_input_) {
267  return "dC";
268  } else if (precompute_coeff_ && i==3+has_parametric_knots()+inv_input_) {
269  return "ddC";
270  } else {
271  casadi_assert_dev(false);
272  return "";
273  }
274  }
275  std::string BlazingSplineFunction::get_name_out(casadi_int i) {
276  if (i==0) {
277  return "f";
278  } else if (i==1) {
279  return "g";
280  } else if (i==2) {
281  return "h";
282  } else {
283  casadi_assert_dev(false);
284  return "";
285  }
286  }
287 
289  const std::vector< std::vector<double> >& knots,
290  casadi_int diff_order,
291  bool precompute_coeff,
292  bool precompute_grid) : FunctionInternal(name), diff_order_(diff_order),
293  precompute_coeff_(precompute_coeff), precompute_grid_(precompute_grid),
294  knots_(knots) {
295 
297 
298  casadi_assert(knots.size()>=1, "blazing_spline only defined for 1D-5D");
299  casadi_assert(knots.size()<=5, "blazing_spline only defined for 1D-5D");
300  }
301 
303  const std::vector<casadi_int>& knot_dims,
304  casadi_int diff_order,
305  bool precompute_coeff,
306  bool precompute_grid,
307  bool inv_input) : FunctionInternal(name), diff_order_(diff_order),
308  precompute_coeff_(precompute_coeff), precompute_grid_(precompute_grid),
309  inv_input_(inv_input) {
310  // knots_ left empty → has_parametric_knots() returns true
311  // Build knots_offset_ from dimension sizes
312  knots_offset_.resize(knot_dims.size()+1);
313  knots_offset_[0] = 0;
314  for (size_t i=0; i<knot_dims.size(); ++i) {
315  knots_offset_[i+1] = knots_offset_[i] + knot_dims[i];
316  }
317 
319 
320  casadi_assert(knot_dims.size()>=1, "blazing_spline only defined for 1D-5D");
321  casadi_assert(knot_dims.size()<=5, "blazing_spline only defined for 1D-5D");
322  }
323 
325  // For non-parametric knots, stack the grid to compute offsets
326  if (!has_parametric_knots()) {
328  }
329 
330  casadi_int nd = ndim();
331 
332  // Compute coefficient tensor size
333  nc_ = 1;
334  for (casadi_int i=0; i<nd; ++i) {
335  nc_ *= (knots_offset_[i+1] - knots_offset_[i]) - 4;
336  }
337 
338  // Compute derivative coefficient tensor size
339  ndc_ = 0;
340  for (casadi_int k=0;k<nd;++k) {
341  casadi_int ndc = 1;
342  for (casadi_int i=0;i<nd;++i) {
343  ndc *= (knots_offset_[i+1] - knots_offset_[i]) - 4 - (i==k);
344  }
345  ndc_+= ndc;
346  }
347 
348  nddc_ = 0;
349  for (casadi_int k=0;k<nd;++k) {
350  for (casadi_int kk=0;kk<nd;++kk) {
351  casadi_int ndc = 1;
352  for (casadi_int i=0;i<nd;++i) {
353  ndc *= (knots_offset_[i+1] - knots_offset_[i]) - 4 - (i==k)-(i==kk);
354  }
355  // We only need the triangular part
356  if (kk>=k) {
357  nddc_+= ndc;
358  }
359  }
360  }
361 
362  // Precompute reciprocal knot spans (only when knot values are known)
363  if (!has_parametric_knots()) {
365  knots_inv_ = inv_dm.nonzeros();
366  }
367  }
368 
371  {{"precompute_coeff",
372  {OT_BOOL,
373  "If true, derivative evaluation requires precomputed derivative "
374  "coefficient tensors (dC, ddC) as function inputs. Only supported "
375  "up to 3D. Default: true for fixed knots, false for parametric knots."}},
376  {"precompute_grid",
377  {OT_BOOL,
378  "If true, precompute reciprocal knot spans to replace runtime "
379  "divisions with multiplications. For parametric knots, inv is "
380  "computed symbolically from the knots input. Default: false."}},
381  {"lookup_mode",
383  "Specifies, for each grid dimension, the lookup algorithm used to find the "
384  "correct index. 'linear' uses a forward linear search. 'exact' uses "
385  "a comparator function optimized for uniformly distributed data "
386  "(requires equally spaced knots). 'binary' uses a binary search. "
387  "'auto' (default) uses 'linear' for small grids and 'binary' for large."}},
388  {"pedantic_mode_order",
389  {OT_STRING,
390  "How to react when per-dimension knot counts are increasing "
391  "in dimension index. Deviating from this sorting may cost "
392  "up to ~30% speedup but may also be harmless of even slightly beneficial. "
393  "One of 'ignore', 'warn' (default), 'error'."}},
394  {"pedantic_mode_size",
395  {OT_STRING,
396  "How to react when an internal coefficient-tensor extent or "
397  "cumulative product is a power of 2 (8, 16, 32, ...). Such extents "
398  "cause cache-set aliasing on power-of-2 strides / cache eviction and "
399  "will incur costs. These costs can vary from 30% to 400% runtime. "
400  "One of 'ignore', 'warn', 'error' (default)."}}
401  }
402  };
403 
404  void BlazingSplineFunction::init(const Dict& opts) {
405  // Call the initialization method of the base class
407 
408  // Read options (pedantic_mode_* defaults set in the header)
409  for (auto&& op : opts) {
410  if (op.first=="precompute_coeff") {
411  precompute_coeff_ = op.second;
412  } else if (op.first=="precompute_grid") {
413  precompute_grid_ = op.second;
414  } else if (op.first=="lookup_mode") {
415  lookup_modes_ = op.second;
416  } else if (op.first=="pedantic_mode_order") {
417  pedantic_mode_order_ = op.second.to_string();
418  } else if (op.first=="pedantic_mode_size") {
419  pedantic_mode_size_ = op.second.to_string();
420  }
421  }
422 
423  casadi_int n_dims = ndim();
424 
425  if (precompute_coeff_) {
426  casadi_assert(n_dims<=3,
427  "blazing_spline with precompute_coeff=true only supports up to 3D. "
428  "Use precompute_coeff=false for 4D/5D.");
429  }
430 
431  // pedantic_mode_order: per-dim knot counts should be non-decreasing.
432  std::vector<casadi_int> dim_sizes(n_dims);
433  for (casadi_int i = 0; i < n_dims; ++i) {
434  dim_sizes[i] = knots_offset_[i+1] - knots_offset_[i];
435  }
436  if (!is_nondecreasing(dim_sizes)) {
437  handle_pedantic(pedantic_mode_order_, "pedantic_mode_order",
438  "blazing_spline '" + name_ + "': per-dimension knot counts " +
439  str(dim_sizes) + " are not increasing in dimension index. "
440  "Deviating from this sorting may cost up to ~30% speedup but may "
441  "also be harmless of even slightly beneficial.");
442  }
443 
444  // pedantic_mode_size: check individual extents and cumulative-prefix
445  // products against power-of-2 cache aliasing. Variants:
446  // nc_ (always) extents = (n_i - 4)
447  // ndc_ at deriv k (precompute && >=1) extent k uses (n_k - 5)
448  // nddc_ at (k,kk) (!precompute && >=2) extents k,kk subtract 1 each
449  std::vector<std::string> offenders;
450  std::vector<casadi_int> ext_nc(n_dims);
451  for (casadi_int i = 0; i < n_dims; ++i) {
452  casadi_int n_i = knots_offset_[i+1] - knots_offset_[i];
453  ext_nc[i] = n_i - 4;
454  if (is_pow2_ge8(ext_nc[i])) {
455  offenders.push_back("dim " + str(i) + " (zero-based): "
456  "(n_knots - 4) = " + str(ext_nc[i]));
457  }
458  }
459  scan_prefix_pow2(offenders, ext_nc, "");
460 
461  if (precompute_coeff_ && diff_order_ >= 1) {
462  for (casadi_int k = 0; k < n_dims; ++k) {
463  casadi_int n_k = knots_offset_[k+1] - knots_offset_[k];
464  casadi_int f5 = n_k - 5;
465  if (is_pow2_ge8(f5)) {
466  offenders.push_back("dim " + str(k) + " (zero-based): "
467  "(n_knots - 5) = " + str(f5) + " (diff order 1)");
468  }
469  std::vector<casadi_int> ext = ext_nc;
470  ext[k] -= 1;
471  scan_prefix_pow2(offenders, ext,
472  " (diff order 1, d/dx_" + str(k) + ")");
473  }
474  }
475  if (!precompute_coeff_ && diff_order_ >= 2) {
476  for (casadi_int k = 0; k < n_dims; ++k) {
477  for (casadi_int kk = k; kk < n_dims; ++kk) {
478  if (k == kk) {
479  casadi_int n_k = knots_offset_[k+1] - knots_offset_[k];
480  casadi_int f6 = n_k - 6;
481  if (is_pow2_ge8(f6)) {
482  offenders.push_back("dim " + str(k) + " (zero-based): "
483  "(n_knots - 6) = " + str(f6) + " (diff order 2)");
484  }
485  }
486  std::vector<casadi_int> ext = ext_nc;
487  ext[k] -= 1;
488  ext[kk] -= 1;
489  scan_prefix_pow2(offenders, ext,
490  " (diff order 2, d2/dx_" + str(k) + "dx_" + str(kk) + ")");
491  }
492  }
493  }
494  if (!offenders.empty()) {
495  std::string msg = "blazing_spline '" + name_ + "': internal "
496  "coefficient-tensor extents or cumulative products are powers of 2 "
497  "(8, 16, 32, ...). Such extents cause cache-set aliasing on "
498  "power-of-2 strides / cache eviction and will incur costs. These "
499  "costs can vary from 30% to 400% runtime. Adjust the number of "
500  "knots in the affected dimension(s). Offending:";
501  for (const auto& s : offenders) msg += "\n - " + s;
502  handle_pedantic(pedantic_mode_size_, "pedantic_mode_size", msg);
503  }
504 
505  // Arrays for holding inputs and outputs
506  alloc_iw(4*n_dims+2);
507  alloc_w(n_dims+1);
508  }
509 
511  clear_mem();
512  }
513 
515  casadi_int nd = ndim();
516  switch (nd) {
522  default: casadi_assert_dev(false);
523  }
524  g.add_include("simde/x86/avx2.h");
525  g.add_include("simde/x86/fma.h");
526 
527  std::string knots_offset = g.constant(knots_offset_);
528  std::string knots_stacked = has_parametric_knots() ?
530  std::string knots_inv;
531  if (inv_input_) {
532  knots_inv = g.arg(arg_inv());
533  } else if (!has_parametric_knots() && precompute_grid_) {
534  knots_inv = g.constant(knots_inv_);
535  } else {
536  knots_inv = "0";
537  }
538 
539  std::vector<casadi_int> degree(nd, 3);
540  std::vector<casadi_int> mode =
542  lookup_modes_, knots_stacked_, knots_offset_, degree, degree);
543 
544  std::string fun_name = "casadi_blazing_" + str(nd) + "d_boor_eval";
545  std::string f_ptr = "res[0]";
546  std::string J_ptr = (diff_order_>=1) ? "res[1]" : "0";
547  std::string H_ptr = (diff_order_>=2) ? "res[2]" : "0";
548 
549  std::string dc_ptr = "0", ddc_ptr = "0";
550  if (precompute_coeff_) {
551  casadi_int dc_idx = 2 + has_parametric_knots() + inv_input_;
552  if (diff_order_>=1) dc_ptr = g.arg(dc_idx);
553  if (diff_order_>=2) ddc_ptr = g.arg(dc_idx+1);
554  }
555 
556  g << fun_name + "(" + f_ptr + ", " + J_ptr + ", " + H_ptr + ", " +
557  knots_stacked + ", " +
558  knots_inv + ", " +
559  knots_offset + ", " +
560  "arg[1], " + dc_ptr + ", " + ddc_ptr + ", " +
561  "arg[0], " +
562  g.constant(mode) + ", " +
563  "iw, w);\n";
564  }
565 
567  return diff_order_<2;
568  }
569 
571  const std::vector<std::string>& inames,
572  const std::vector<std::string>& onames,
573  const Dict& opts) const {
574  casadi_int N = ndim();
575  bool parametric = has_parametric_knots();
576  casadi_int nk = parametric ? knots_offset_.back() : 0;
577  // Per-dim cache slice = 2 (intercept,slope) + 3*n_k. See compute_knots_cache.
578  casadi_int n_inv = 2 * N + 3 * nk;
579 
580  MX x = MX::sym("x", N);
581  MX C = MX::sym("C", nc_);
582  MX knots_sym;
583  if (parametric) knots_sym = MX::sym("knots", nk);
584  MX inv_sym;
585  if (inv_input_) inv_sym = MX::sym("inv", n_inv);
586 
588  Jopts = combine(opts, Jopts);
589  Jopts = combine(Jopts, generate_options("jacobian"));
590  Jopts["derivative_of"] = self();
591 
592  // Propagate pedantic_mode_* to the child unless explicitly overridden.
593  // combine() takes the first dict's value when keys collide, so this only
594  // fills in when no caller- or jacobian_options-supplied value exists.
595  Dict pedantic_defaults;
596  pedantic_defaults["pedantic_mode_order"] = pedantic_mode_order_;
597  pedantic_defaults["pedantic_mode_size"] = pedantic_mode_size_;
598  Jopts = combine(Jopts, pedantic_defaults);
599 
600  std::string fJname = name_ + "_der";
601 
602  // --- Synthesize dC/ddC tensors (coeff mode only) ---
603  std::vector<casadi_int> coeffs_dims(N+1);
604  coeffs_dims[0] = 1;
605  for (casadi_int i=0; i<N; ++i) {
606  coeffs_dims[i+1] = knots_offset_[i+1]-knots_offset_[i]-4;
607  }
608  std::vector<casadi_int> degree(N, 3);
609 
610  std::vector<MX> dCv;
611  MX dC, ddC;
612  // Per-dim degree after one derivative
613  std::vector< std::vector<casadi_int> > degree_d(N);
614  // Numeric derivative knots (non-parametric path); filled by derivative_coeff
615  std::vector< std::vector< std::vector<double> > > knots_d_num(N);
616  // Parametric per-dim knot vectors
617  std::vector<MX> K_per_dim;
618 
619  if (precompute_coeff_) {
620  if (parametric) {
621  K_per_dim.resize(N);
622  for (casadi_int i=0; i<N; ++i) {
623  casadi_int off = knots_offset_[i];
624  K_per_dim[i] = knots_sym(Slice(off, off + (knots_offset_[i+1]-off)));
625  }
626  }
627  for (casadi_int i=0; i<N; ++i) {
628  if (parametric) {
629  dCv.push_back(derivative_coeff_mx(i, K_per_dim, degree, coeffs_dims, C));
630  } else {
631  dCv.push_back(BSplineCommon::derivative_coeff(
632  i, knots_, degree, coeffs_dims, C, knots_d_num[i], degree_d[i]));
633  }
634  degree_d[i].assign(N, 3);
635  degree_d[i][i] = 2;
636  }
637  dC = vertcat(dCv);
638 
639  if (diff_order_>=1) {
640  // ddC ordering (must match runtime layout expected by 2d/3d_boor_eval):
641  // diagonals (i,i) for i in [0,N),
642  // off-diags (0,1) for N==2; (0,1), (1,2), (2,0) for N==3.
643  std::vector<std::pair<casadi_int, casadi_int>> dd_pairs;
644  for (casadi_int i=0; i<N; ++i) dd_pairs.emplace_back(i, i);
645  if (N==2) {
646  dd_pairs.emplace_back(0, 1);
647  } else if (N==3) {
648  dd_pairs.emplace_back(0, 1);
649  dd_pairs.emplace_back(1, 2);
650  dd_pairs.emplace_back(2, 0);
651  }
652 
653  std::vector<MX> parts;
654  parts.reserve(dd_pairs.size());
655  std::vector< std::vector<double> > knots_dummy;
656  std::vector<casadi_int> degree_dummy;
657  for (auto& p : dd_pairs) {
658  casadi_int di = p.first, dj = p.second;
659  std::vector<casadi_int> cd = coeffs_dims;
660  cd[di+1] -= 1;
661  if (parametric) {
662  std::vector<MX> Kd(N);
663  for (casadi_int k=0; k<N; ++k) {
664  casadi_int n_ki = knots_offset_[k+1]-knots_offset_[k];
665  Kd[k] = (k==di) ? K_per_dim[k](Slice(1, n_ki-1)) : K_per_dim[k];
666  }
667  parts.push_back(derivative_coeff_mx(dj, Kd, degree_d[di], cd, dCv[di]));
668  } else {
669  parts.push_back(BSplineCommon::derivative_coeff(
670  dj, knots_d_num[di], degree_d[di], cd, dCv[di],
671  knots_dummy, degree_dummy));
672  }
673  }
674  ddC = vertcat(parts);
675  }
676  }
677 
678  // --- Create child function fJ (diff_order_+1) ---
679  Function fJ;
680  if (!incache(fJname, fJ)) {
681  if (parametric) {
682  std::vector<casadi_int> kdims(N);
683  for (casadi_int i=0; i<N; ++i)
684  kdims[i] = knots_offset_[i+1]-knots_offset_[i];
685  fJ = Function::create(
686  new BlazingSplineFunction(fJname, kdims, diff_order_+1,
687  precompute_coeff_, precompute_grid_, /*inv_input=*/precompute_grid_), Jopts);
688  } else {
689  fJ = Function::create(
692  }
693  tocache(fJ);
694  }
695 
696  // --- Child inputs: [x, C, [knots], [inv], [dC], [ddC]] ---
697  std::vector<MX> in_child = {x, C};
698  if (parametric) in_child.push_back(knots_sym);
699  if (precompute_grid_ && parametric) {
700  MX inv_mx = inv_input_ ? inv_sym : compute_knots_cache(knots_sym, knots_offset_);
701  in_child.push_back(inv_mx);
702  }
703  if (precompute_coeff_) {
704  in_child.push_back(dC);
705  if (diff_order_ >= 1) in_child.push_back(ddC);
706  }
707 
708  std::vector<MX> ret = fJ(in_child);
709 
710  // --- User-facing jacobian inputs (mirror original function inputs) ---
711  std::vector<MX> jac_in = {x, C};
712  std::vector<casadi_int> in_sizes = {N, nc_};
713  if (parametric) { jac_in.push_back(knots_sym); in_sizes.push_back(nk); }
714  if (inv_input_) { jac_in.push_back(inv_sym); in_sizes.push_back(n_inv); }
715  if (precompute_coeff_ && diff_order_>=1) {
716  jac_in.push_back(MX(1, ndc_)); in_sizes.push_back(ndc_);
717  }
718 
719  // --- Jacobian outputs: for each orig output k, for each in_user, a block ---
720  std::vector<MX> jac_out;
721  for (casadi_int k=0; k<=diff_order_; ++k) {
722  casadi_int nrows = 1;
723  for (casadi_int j=0; j<k; ++j) nrows *= N;
724  for (size_t j=0; j<jac_in.size(); ++j) {
725  jac_out.push_back(j==0 ? ret[k+1] : MX(nrows, in_sizes[j]));
726  }
727  }
728 
729  // --- Append adjoint seeds (one per original output) ---
730  for (casadi_int k=0; k<=diff_order_; ++k) {
731  if (k==0) jac_in.push_back(MX(1, 1));
732  else if (k==1) jac_in.push_back(MX(1, N));
733  else if (k==2) jac_in.push_back(MX(N, N));
734  }
735 
736  return Function(name, jac_in, jac_out, inames, onames, {{"always_inline", true}});
737  }
738 
741 
742  s.version("BlazingSplineFunction", 2);
743  s.pack("BlazingSplineFunction::diff_order", diff_order_);
744  s.pack("BlazingSplineFunction::precompute_coeff", precompute_coeff_);
745  s.pack("BlazingSplineFunction::precompute_grid", precompute_grid_);
746  s.pack("BlazingSplineFunction::knots", knots_);
747  s.pack("BlazingSplineFunction::lookup_modes", lookup_modes_);
748  s.pack("BlazingSplineFunction::parametric_knots", has_parametric_knots());
749  if (has_parametric_knots()) {
750  s.pack("BlazingSplineFunction::knots_offset", knots_offset_);
751  s.pack("BlazingSplineFunction::inv_input", inv_input_);
752  }
753  s.pack("BlazingSplineFunction::pedantic_mode_order", pedantic_mode_order_);
754  s.pack("BlazingSplineFunction::pedantic_mode_size", pedantic_mode_size_);
755  }
756 
758  int v = s.version("BlazingSplineFunction", 1, 2);
759  s.unpack("BlazingSplineFunction::diff_order", diff_order_);
760  if (v>=2) {
761  s.unpack("BlazingSplineFunction::precompute_coeff", precompute_coeff_);
762  s.unpack("BlazingSplineFunction::precompute_grid", precompute_grid_);
763  } else {
764  precompute_coeff_ = true;
765  precompute_grid_ = false;
766  }
767  s.unpack("BlazingSplineFunction::knots", knots_);
768  if (v>=2) {
769  s.unpack("BlazingSplineFunction::lookup_modes", lookup_modes_);
770  bool parametric;
771  s.unpack("BlazingSplineFunction::parametric_knots", parametric);
772  if (parametric) {
773  s.unpack("BlazingSplineFunction::knots_offset", knots_offset_);
774  s.unpack("BlazingSplineFunction::inv_input", inv_input_);
775  }
776  s.unpack("BlazingSplineFunction::pedantic_mode_order", pedantic_mode_order_);
777  s.unpack("BlazingSplineFunction::pedantic_mode_size", pedantic_mode_size_);
778  }
780  }
781 
783  return new BlazingSplineFunction(s);
784  }
785 
786  class BlazingSplineIncrementalSerializer {
787  public:
788 
789  BlazingSplineIncrementalSerializer() : serializer(ss) {
790  }
791 
792  std::string generate_id(const std::vector<MX>& a) {
793  ref.insert(ref.end(), a.begin(), a.end());
794  if (a.empty()) return "";
795 
796  std::vector<MX> ordered = Function::order(a);
797  // First serialize may introduce unknown dependencies (e.g. sparsity)
798  // and hence definitions
799  // Subsequent serialization will have references instead.
800  // In order to still get a match with a later common subexpression,
801  // make sure that all dependencies are already defined.
802  serializer.pack(ordered);
803  ss.str("");
804  ss.clear();
805  serializer.pack(ordered);
806  std::string ret = ss.str();
807  ss.str("");
808  ss.clear();
809  return ret;
810  }
811 
812  private:
813  std::stringstream ss;
814  // List of references to keep alive
815  std::vector<MX> ref;
816  SerializingStream serializer;
817  };
818 
819  void BlazingSplineFunction::merge(const std::vector<MX>& arg,
820  std::vector<MX>& subs_from,
821  std::vector<MX>& subs_to) const {
822 
823  Function base = self();
824  for (casadi_int i=0;i<diff_order_;++i) {
825  base = base->derivative_of_;
826  }
827 
828  // Sort graph
829  Function f("f", {}, arg, {{"allow_free", true}, {"max_io", 0}});
830 
831 
832  std::unordered_map<std::string, std::vector<MX> > targets0;
833  std::unordered_map<std::string, std::vector<MX> > targets1;
834  std::vector<MX> targets2;
835 
836  BlazingSplineIncrementalSerializer ss;
837  std::string key;
838 
839  // Loop over instructions
840  for (int k=0; k<f.n_instructions(); ++k) {
841  MX e = f.instruction_MX(k);
842  if (e.is_call()) {
843  Function fun = e.which_function();
844 
845  // Check if the function is a BlazingSplineFunction
846  if (fun.class_name()=="BlazingSplineFunction") {
847  key = ss.generate_id(e->dep_);
848  // Which derivative level?
849  if (fun==base) {
850  targets0[key].push_back(e);
851  } else if (!fun->derivative_of_.is_null() &&
852  fun->derivative_of_==base) {
853  targets1[key].push_back(e);
854  } else if (!fun->derivative_of_.is_null() &&
856  fun->derivative_of_->derivative_of_==base) {
857  targets2.push_back(e);
858  }
859  }
860  }
861  }
862 
863  // Loop over second order targets, targets2
864  for (const auto& e : targets2) {
865 
866  // Compute key that matches targets1
867  // Precompute: strip last arg (ddC) to match targets1's (x, C, dC)
868  // NPC: all levels share the same deps (x, C), use directly
869  key = precompute_coeff_ ?
870  ss.generate_id(vector_init(e->dep_)) :
871  ss.generate_id(e->dep_);
872 
873  // Loop over all matching target1 entries
874  for (const auto& ee : targets1[key]) {
875  // Mark all matches for substitution
876  subs_from.push_back(ee);
877  // Substitute with self
878  subs_to.push_back(e);
879  }
880 
881  // Compute key that matches targets0
882  // Precompute coeff: strip two args (ddC, dC) to match targets0's (x, C)
883  // Parametric grid: strip inv to match targets0's (x, C, K)
884  // NPC: same deps already match
885  if (precompute_coeff_) {
886  key = ss.generate_id(vector_init(vector_init(e->dep_)));
887  }
888 
889  // Loop over all matching target0 entries
890  for (const auto& ee : targets0[key]) {
891  // Mark all matches for substitution
892  subs_from.push_back(ee);
893  // Substitute with self
894  subs_to.push_back(e);
895  }
896  }
897 
898  // Loop over first order targets, targets1
899  for (const auto& ee : targets1) {
900  for (const auto& e : ee.second) {
901  // Compute key that matches targets0
902  // Precompute coeff: strip last arg (dC) to match targets0's (x, C)
903  // NPC/grid: all levels share the same deps, use directly
904  key = precompute_coeff_ ?
905  ss.generate_id(vector_init(e->dep_)) :
906  ss.generate_id(e->dep_);
907 
908  // Loop over all matching target0 entries
909  for (const auto& ee : targets0[key]) {
910  // Mark all matches for substitution
911  subs_from.push_back(ee);
912  // Substitute with self
913  subs_to.push_back(e);
914  }
915  }
916  }
917 
918  }
919 
920 
921 } // namespace casadi
static M derivative_coeff(casadi_int i, const std::vector< std::vector< double > > &knots, const std::vector< casadi_int > &degree, const std::vector< casadi_int > &coeffs_dims, const M &coeffs, std::vector< std::vector< double > > &new_knots, std::vector< casadi_int > &new_degree)
static ProtoFunction * deserialize(DeserializingStream &s)
Deserialize into MX.
std::string get_name_out(casadi_int i) override
Names of function input and outputs.
std::vector< std::string > lookup_modes_
std::vector< casadi_int > knots_offset_
std::vector< double > knots_stacked_
~BlazingSplineFunction() override
Destructor.
Sparsity get_sparsity_in(casadi_int i) override
Sparsities of function inputs and outputs.
void serialize_body(SerializingStream &s) const override
Serialize an object without type information.
BlazingSplineFunction(const std::string &name, const std::vector< std::vector< double > > &knots, casadi_int diff_order, bool precompute_coeff=true, bool precompute_grid=false)
Constructor (fixed knots)
bool get_diff_in(casadi_int i) override
Which inputs are differentiable?
std::vector< std::vector< double > > knots_
bool has_parametric_knots() const
Are knots parametric (provided at runtime)?
bool has_jacobian() const override
Jacobian of all outputs with respect to all inputs.
size_t get_n_out() override
Number of function inputs and outputs.
Sparsity get_sparsity_out(casadi_int i) override
Sparsities of function inputs and outputs.
casadi_int arg_knots() const
Index of the knots input (only valid when parametric)
Function get_jacobian(const std::string &name, const std::vector< std::string > &inames, const std::vector< std::string > &onames, const Dict &opts) const override
Jacobian of all outputs with respect to all inputs.
static const Options options_
Options.
void init(const Dict &opts) override
Initialize.
casadi_int arg_inv() const
Index of the inv input (only valid when inv_input_)
void codegen_body(CodeGenerator &g) const override
Generate code for the function body.
casadi_int ndim() const
Number of dimensions.
size_t get_n_in() override
Number of function inputs and outputs.
std::string get_name_in(casadi_int i) override
Names of function input and outputs.
void merge(const std::vector< MX > &arg, std::vector< MX > &subs_from, std::vector< MX > &subs_to) const override
List merge opportunitities.
Helper class for C code generation.
std::string arg(casadi_int i) const
Refer to argument.
std::string constant(const std::vector< casadi_int > &v)
Represent an array constant; adding it when new.
void add_include(const std::string &new_include, bool relative_path=false, const std::string &use_ifdef=std::string())
Add an include file optionally using a relative path "..." instead of an absolute path <....
void add_auxiliary(Auxiliary f, const std::vector< std::string > &inst={"casadi_real"})
Add a built-in auxiliary function.
Helper class for Serialization.
void unpack(Sparsity &e)
Reconstruct an object from the input stream.
void version(const std::string &name, int v)
Internal class for Function.
void alloc_iw(size_t sz_iw, bool persistent=false)
Ensure required length of iw field.
void init(const Dict &opts) override
Initialize.
void serialize_body(SerializingStream &s) const override
Serialize an object without type information.
virtual void find(std::map< FunctionInternal *, std::pair< Function, size_t > > &all_fun, casadi_int max_depth) const
bool incache(const std::string &fname, Function &f, const std::string &suffix="") const
Get function in cache.
static const Options options_
Options.
void alloc_w(size_t sz_w, bool persistent=false)
Ensure required length of w field.
void tocache(const Function &f, const std::string &suffix="") const
Save function to cache.
Function derivative_of_
If the function is the derivative of another function.
Dict generate_options(const std::string &target) const override
Reconstruct options dict.
Function object.
Definition: function.hpp:60
static Function create(FunctionInternal *node)
Create from node.
Definition: function.cpp:488
static std::vector< SX > order(const std::vector< SX > &expr)
Definition: function.cpp:2142
std::pair< casadi_int, casadi_int > size_in(casadi_int ind) const
Get input dimension.
Definition: function.cpp:995
const std::vector< std::string > & name_out() const
Get output scheme.
Definition: function.cpp:1117
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.
bool is_null() const
Is a null pointer?
static void stack_grid(const std::vector< std::vector< double > > &grid, std::vector< casadi_int > &offset, std::vector< double > &stacked)
Definition: interpolant.cpp:46
static std::vector< casadi_int > interpret_lookup_mode(const std::vector< std::string > &modes, const std::vector< double > &grid, const std::vector< casadi_int > &offset, const std::vector< casadi_int > &margin_left=std::vector< casadi_int >(), const std::vector< casadi_int > &margin_right=std::vector< casadi_int >())
Convert from (optional) lookup modes labels to enum.
std::vector< MX > dep_
dependencies - functions that have to be evaluated before this one
Definition: mx_node.hpp:824
MX - Matrix expression.
Definition: mx.hpp:92
static MX einstein(const MX &A, const MX &B, const MX &C, const std::vector< casadi_int > &dim_a, const std::vector< casadi_int > &dim_b, const std::vector< casadi_int > &dim_c, const std::vector< casadi_int > &a, const std::vector< casadi_int > &b, const std::vector< casadi_int > &c)
Computes an einstein dense tensor contraction.
Definition: mx.cpp:682
bool is_call() const
Check if evaluation.
Definition: mx.cpp:803
Function which_function() const
Get function - only valid when is_call() is true.
Definition: mx.cpp:807
std::vector< Scalar > & nonzeros()
Base class for FunctionInternal and LinsolInternal.
void clear_mem()
Clear all memory (called from destructor)
Helper class for Serialization.
void version(const std::string &name, int v)
void pack(const Sparsity &e)
Serializes an object to the output stream.
std::string class_name() const
Get class name.
Class representing a Slice.
Definition: slice.hpp:48
General sparsity class.
Definition: sparsity.hpp:106
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
std::vector< casadi_int > range(casadi_int start, casadi_int stop, casadi_int step, casadi_int len)
Range function.
Function blazing_spline(const std::string &name, const std::vector< std::vector< double > > &knots, const Dict &opts)
Construct a specialized parametric BSpline.
static std::vector< casadi_int > knot_offsets(const std::vector< casadi_int > &knot_dims)
double if_else_zero(double x, double y)
Conditional assignment.
Definition: calculus.hpp:295
static M compute_knots_cache(const M &K, const std::vector< casadi_int > &offsets)
static bool is_pow2_ge8(casadi_int n)
Dict combine(const Dict &first, const Dict &second, bool recurse)
Combine two dicts. First has priority.
static void scan_prefix_pow2(std::vector< std::string > &offenders, const std::vector< casadi_int > &ext, const std::string &tag)
@ OT_STRINGVECTOR
std::string str(const T &v)
String representation, any type.
GenericType::Dict Dict
C++ equivalent of Python's dict or MATLAB's struct.
static MX derivative_coeff_mx(casadi_int i, const std::vector< MX > &knots_per_dim, const std::vector< casadi_int > &degree, const std::vector< casadi_int > &coeffs_dims, const MX &coeffs)
std::vector< T > diff(const std::vector< T > &values)
diff
static void handle_pedantic(const std::string &mode, const std::string &opt_name, const std::string &msg)
std::vector< T > vector_init(const std::vector< T > &v)
Return all but the last element of a vector.
Matrix< double > DM
Definition: dm_fwd.hpp:33
bool is_nondecreasing(const std::vector< T > &v)
Check if the vector is non-decreasing.
Options metadata for a class.
Definition: options.hpp:40