conopt_interface.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 #include "conopt_interface.hpp"
26 #include "casadi/core/casadi_misc.hpp"
27 #include "casadi/core/casadi_interrupt.hpp"
28 #include <cmath>
29 #include <cstring>
30 #include <algorithm>
31 #include <limits>
32 
33 namespace casadi {
34 
35  // CONOPT 4.39.2 accepts option names up to 20 characters.
36  static const size_t conopt_max_option_name = 20;
37 
38  extern "C" int CASADI_NLPSOL_CONOPT_EXPORT casadi_register_nlpsol_conopt(Nlpsol::Plugin* plugin) {
39  plugin->creator = ConoptInterface::creator;
40  plugin->name = "conopt";
41  plugin->doc = ConoptInterface::meta_doc.c_str();
42  plugin->version = CASADI_VERSION;
43  plugin->options = &ConoptInterface::options_;
44  plugin->deserialize = &ConoptInterface::deserialize;
45  return 0;
46  }
47 
48  extern "C" void CASADI_NLPSOL_CONOPT_EXPORT casadi_load_nlpsol_conopt() {
50  }
51 
52  const Options ConoptInterface::options_ =
53  {{&Nlpsol::options_}, {
54  {"exact_hessian", {OT_BOOL, "Provide exact Hessian to CONOPT"}},
55  {"warm_start", {OT_BOOL,
56  "Warm-start CONOPT using multipliers from a prior solve to infer "
57  "basis status (IniStat=2)"}},
58  {"conopt", {OT_DICT, "Options to be passed to CONOPT"}},
59  {"optfile", {OT_STRING,
60  "Path to a CONOPT option file (for string-valued CR-cells such as Algorithm)"}},
61  {"debug", {OT_BOOL,
62  "Print debug output: constraint values at each FDEval, solution vector, "
63  "and option echo"}}
64  }};
65 
66  ConoptInterface::ConoptInterface(const std::string& name, const Function& nlp)
67  : Nlpsol(name, nlp) {}
68 
70 
71  void ConoptInterface::init(const Dict& opts) {
72  Nlpsol::init(opts);
73 
74  // Extract native options
75  warm_start_ = false;
76  debug_ = false;
77  for (auto&& op : opts) {
78  if (op.first == "conopt") opts_ = op.second;
79  else if (op.first == "optfile") optfile_ = op.second.to_string();
80  else if (op.first == "warm_start") warm_start_ = op.second.to_bool();
81  else if (op.first == "debug") debug_ = op.second.to_bool();
82  }
83 
84  for (auto&& op : opts_) {
85  casadi_assert(op.first.size() <= conopt_max_option_name,
86  "CONOPT option name '" + op.first + "' is " + str(op.first.size()) +
87  " characters; CONOPT accepts at most " + str(conopt_max_option_name) + ".");
88  }
89 
90  Function gradf_fcn = create_function("nlp_grad_f", {"x", "p"}, {"f", "grad:f:x"});
91  gradf_sp_ = gradf_fcn.sparsity_out(1);
92 
93  Function jacg_fcn = create_function("nlp_jac_g", {"x", "p"}, {"g", "jac:g:x"});
94  jacg_sp_ = jacg_fcn.sparsity_out(1);
95 
96  // Detect linear (constant) Jacobian entries using second-order sparsity:
97  // d(jac:g:x_compact)/dx has shape (nnz_g, nx_); if row k is empty,
98  // the k-th Jacobian nonzero is constant in x (linear entry).
99  {
100  Sparsity djac_dx = jacg_fcn.sparsity_jac(0, 1, true);
101  jacg_nlflag_.assign(jacg_sp_.nnz(), 0);
102  const casadi_int* dj_row = djac_dx.row();
103  for (casadi_int el = 0; el < djac_dx.nnz(); ++el)
104  jacg_nlflag_[dj_row[el]] = 1;
105  has_linear_jac_ = std::any_of(jacg_nlflag_.begin(), jacg_nlflag_.end(),
106  [](int f) { return f == 0; });
107  }
108 
109  // Setup 2nd Order Info
110  exact_hessian_ = true;
111  if (opts.find("exact_hessian") != opts.end()) exact_hessian_ = opts.at("exact_hessian");
112 
113  if (exact_hessian_) {
114  Function hl_fcn = create_function("nlp_hess_l", {"x", "p", "lam:f", "lam:g"},
115  {"tril:hess:gamma:x:x"}, {{"gamma", {"f", "g"}}});
116  hesslag_sp_ = hl_fcn.sparsity_out(0);
117  }
118 
119  // Per-column objective-gradient flag (used in cb_read_matrix)
120  const casadi_int* f_row = gradf_sp_.row();
121  gradf_col_flag_.assign(nx_, false);
122  gradf_col_to_nz_.assign(nx_, -1);
123  for (casadi_int k = 0; k < gradf_sp_.nnz(); ++k) {
124  gradf_col_flag_[f_row[k]] = true;
125  gradf_col_to_nz_[f_row[k]] = k;
126  }
127 
128  // Detect constant (linear) objective gradient entries using second-order sparsity
129  {
130  Sparsity dgradf_dx = gradf_fcn.sparsity_jac(0, 1, true);
131  gradf_nlflag_.assign(gradf_sp_.nnz(), 0);
132  const casadi_int* dg_row = dgradf_dx.row();
133  for (casadi_int el = 0; el < dgradf_dx.nnz(); ++el) {
134  gradf_nlflag_[dg_row[el]] = 1;
135  }
136  has_linear_gradf_ = std::any_of(gradf_nlflag_.begin(), gradf_nlflag_.end(),
137  [](int f) { return f == 0; });
138  }
139 
140  const casadi_int* g_colind = jacg_sp_.colind();
141  const casadi_int* g_row = jacg_sp_.row();
142 
143  // Build row-indexed CSR structure over nonlinear entries for fast
144  // Jacobian scatter in cb_fd_eval
145  casadi_int nnz_g = jacg_sp_.nnz();
146  jacg_rowstart_.assign(ng_ + 1, 0);
147  for (casadi_int el = 0; el < nnz_g; ++el)
148  if (jacg_nlflag_[el]) jacg_rowstart_[g_row[el] + 1]++;
149  for (int r = 0; r < ng_; ++r)
150  jacg_rowstart_[r + 1] += jacg_rowstart_[r];
151  jacg_nzidx_.resize(jacg_rowstart_[ng_]);
152  jacg_col_.resize(jacg_rowstart_[ng_]);
153  std::vector<int> fill_pos(ng_, 0);
154  for (int c = 0; c < nx_; ++c) {
155  for (casadi_int el = g_colind[c]; el < g_colind[c+1]; ++el) {
156  if (!jacg_nlflag_[el]) continue;
157  int r = static_cast<int>(g_row[el]);
158  casadi_assert(fill_pos[r] < jacg_rowstart_[r + 1] - jacg_rowstart_[r],
159  "CSR fill overflow for row r - count/fill pass mismatch in jacg_rowstart_");
160  int pos = jacg_rowstart_[r] + fill_pos[r]++;
161  jacg_nzidx_[pos] = static_cast<int>(el);
162  jacg_col_[pos] = c;
163  }
164  }
165  }
166 
167  // --- Serialization & Deserialization --- //
169  s.version("ConoptInterface", 1);
170  s.unpack("ConoptInterface::exact_hessian", exact_hessian_);
171  s.unpack("ConoptInterface::opts", opts_);
172  s.unpack("ConoptInterface::gradf_sp", gradf_sp_);
173  s.unpack("ConoptInterface::jacg_sp", jacg_sp_);
174  s.unpack("ConoptInterface::hesslag_sp", hesslag_sp_);
175  s.unpack("ConoptInterface::optfile", optfile_);
176  s.unpack("ConoptInterface::warm_start", warm_start_);
177  s.unpack("ConoptInterface::debug", debug_);
178 
179  // Recompute linearity flags first (needed for CSR construction below)
180  {
181  Sparsity djac_dx = get_function("nlp_jac_g").sparsity_jac(0, 1, true);
182  jacg_nlflag_.assign(jacg_sp_.nnz(), 0);
183  const casadi_int* dj_row = djac_dx.row();
184  for (casadi_int el = 0; el < djac_dx.nnz(); ++el)
185  jacg_nlflag_[dj_row[el]] = 1;
186  has_linear_jac_ = std::any_of(jacg_nlflag_.begin(), jacg_nlflag_.end(),
187  [](int f) { return f == 0; });
188  }
189 
190  // Rebuild derived arrays from the serialized sparsities
191  const casadi_int* f_row = gradf_sp_.row();
192  gradf_col_flag_.assign(nx_, false);
193  gradf_col_to_nz_.assign(nx_, -1);
194  for (casadi_int k = 0; k < gradf_sp_.nnz(); ++k) {
195  gradf_col_flag_[f_row[k]] = true;
196  gradf_col_to_nz_[f_row[k]] = k;
197  }
198 
199  {
200  Sparsity dgradf_dx = get_function("nlp_grad_f").sparsity_jac(0, 1, true);
201  gradf_nlflag_.assign(gradf_sp_.nnz(), 0);
202  const casadi_int* dg_row = dgradf_dx.row();
203  for (casadi_int el = 0; el < dgradf_dx.nnz(); ++el)
204  gradf_nlflag_[dg_row[el]] = 1;
205  has_linear_gradf_ = std::any_of(gradf_nlflag_.begin(), gradf_nlflag_.end(),
206  [](int f) { return f == 0; });
207  }
208 
209  const casadi_int* g_colind = jacg_sp_.colind();
210  const casadi_int* g_row = jacg_sp_.row();
211  casadi_int nnz_g = jacg_sp_.nnz();
212  jacg_rowstart_.assign(ng_ + 1, 0);
213  for (casadi_int el = 0; el < nnz_g; ++el)
214  if (jacg_nlflag_[el]) jacg_rowstart_[g_row[el] + 1]++;
215  for (int r = 0; r < ng_; ++r)
216  jacg_rowstart_[r + 1] += jacg_rowstart_[r];
217  jacg_nzidx_.resize(jacg_rowstart_[ng_]);
218  jacg_col_.resize(jacg_rowstart_[ng_]);
219  std::vector<int> fill_pos(ng_, 0);
220  for (int c = 0; c < nx_; ++c)
221  for (casadi_int el = g_colind[c]; el < g_colind[c+1]; ++el) {
222  if (!jacg_nlflag_[el]) continue;
223  int r = static_cast<int>(g_row[el]);
224  casadi_assert(fill_pos[r] < jacg_rowstart_[r + 1] - jacg_rowstart_[r],
225  "CSR fill overflow for row r - count/fill pass mismatch in jacg_rowstart_");
226  int pos = jacg_rowstart_[r] + fill_pos[r]++;
227  jacg_nzidx_[pos] = static_cast<int>(el);
228  jacg_col_[pos] = c;
229  }
230  }
231 
234  s.version("ConoptInterface", 1);
235  s.pack("ConoptInterface::exact_hessian", exact_hessian_);
236  s.pack("ConoptInterface::opts", opts_);
237  s.pack("ConoptInterface::gradf_sp", gradf_sp_);
238  s.pack("ConoptInterface::jacg_sp", jacg_sp_);
239  s.pack("ConoptInterface::hesslag_sp", hesslag_sp_);
240  s.pack("ConoptInterface::optfile", optfile_);
241  s.pack("ConoptInterface::warm_start", warm_start_);
242  s.pack("ConoptInterface::debug", debug_);
243  // Derived arrays (gradf_col_flag_, CSR) are rebuilt on deserialization
244  }
245 
247  : self(interface), NlpsolMemory(), cntvect(nullptr),
248  modsta(ConoptModelStatus::Unset), solsta(ConoptSolverStatus::Unset),
249  iter(0), return_status("Unset"),
250  cache_valid(false), cache_valid_jac(false), nan_encountered(false),
251  ng_expanded(0), numnz_expanded(0) {}
252 
254  if (cntvect) COI_Free(&cntvect);
255  }
256 
257  void ConoptInterface::free_mem(void* mem) const { delete static_cast<ConoptMemory*>(mem); }
258 
259  int ConoptInterface::init_mem(void* mem) const {
260  if (Nlpsol::init_mem(mem)) return 1;
261  auto m = static_cast<ConoptMemory*>(mem);
262 
263  m->cached_x.resize(nx_, 0.0);
264  m->cached_grad_f.resize(gradf_sp_.nnz(), 0.0);
265  m->cached_g.resize(ng_, 0.0);
266  m->cached_jac_g.resize(jacg_sp_.nnz(), 0.0);
267  m->casadi_to_conopt_lb_row.resize(ng_);
268  m->casadi_to_conopt_ub_row.assign(ng_, -1);
269  m->hess_lam_g_.resize(ng_, 0.0);
270  m->row_const_.assign(ng_, 0.0);
271  m->row_nnz.assign(ng_, 0);
272  // Every CasADi row contributes at least one entry (range constraints add a
273  // second), so ng_ is a guaranteed lower bound on the final size — reserving
274  // less would force a reallocation on essentially every solve. solve() grows
275  // these vectors further on demand only for the range-constraint rows.
276  casadi_int initial_row_reserve = ng_;
277  m->conopt_to_casadi.reserve(initial_row_reserve);
278  m->conopt_type.reserve(initial_row_reserve);
279  m->conopt_rhs.reserve(initial_row_reserve);
280  if (has_linear_jac_) {
281  m->const_jac_vals.resize(jacg_sp_.nnz(), 0.0);
282  }
283  m->linear_at_x0.resize(ng_, 0.0);
284  if (has_linear_gradf_)
285  m->gradf_const_vals.resize(gradf_sp_.nnz(), 0.0);
286 
287  if (COI_Create(&m->cntvect) != 0 || m->cntvect == nullptr) {
288  casadi::uerr() << "CONOPT: COI_Create failed" << std::endl;
289  return 1;
290  }
291 
292  if (warm_start_) COIDEF_IniStat(m->cntvect, 2);
293 
294  COIDEF_NumVar(m->cntvect, nx_);
295  // NumCon, NumNz, NumNlNz are set in solve() because range-constraint expansion
296  // can change them between calls.
297 
298  COIDEF_ObjCon(m->cntvect, 0);
299  COIDEF_OptDir(m->cntvect, -1);
300 
301  // Handle Options
302  m->custom_options.clear();
303  for (auto&& op : opts_) {
304  // Explictly catch C API options defined in conopt.h
305  if (op.first == "itlim") COIDEF_ItLim(m->cntvect, op.second.to_int());
306  else if (op.first == "errlim") COIDEF_ErrLim(m->cntvect, op.second.to_int());
307  else if (op.first == "reslim" || op.first == "timelim")
308  COIDEF_ResLim(m->cntvect, op.second.to_double());
309  else if (op.first == "maxheap") COIDEF_MaxHeap(m->cntvect, op.second.to_double());
310  else if (op.second.is_string()) {
311  casadi_warning("CONOPT option '" + op.first + "' is a string; string options cannot be "
312  "passed via the CONOPT option callback (no SVAL parameter). "
313  "Use the 'optfile' option instead.");
314  } else {
315  m->custom_options.push_back(op);
316  }
317  }
318  if (!optfile_.empty()) COIDEF_Optfile(m->cntvect, optfile_.c_str());
319  COIDEF_Option(m->cntvect, &ConoptInterface::cb_option);
320  COIDEF_Progress(m->cntvect, &ConoptInterface::cb_progress);
321 
322  // Register Callbacks
323  COIDEF_ReadMatrix(m->cntvect, &ConoptInterface::cb_read_matrix);
324  COIDEF_FDEvalIni(m->cntvect, &ConoptInterface::cb_fdevalini);
325  COIDEF_FDEval(m->cntvect, &ConoptInterface::cb_fd_eval);
326  COIDEF_FDEvalEnd(m->cntvect, &ConoptInterface::cb_fdevalend);
327 
328  if (exact_hessian_ && hesslag_sp_.nnz() > 0) {
329  COIDEF_NumHess(m->cntvect, hesslag_sp_.nnz());
330  COIDEF_2DLagrStr(m->cntvect, &ConoptInterface::cb_2dlagrstr);
331  COIDEF_2DLagrVal(m->cntvect, &ConoptInterface::cb_2dlagrval);
332  }
333 
334  COIDEF_FVincLin(m->cntvect, 1);
335 
336  COIDEF_Status(m->cntvect, &ConoptInterface::cb_status);
337  COIDEF_Solution(m->cntvect, &ConoptInterface::cb_solution);
338  COIDEF_Message(m->cntvect, &ConoptInterface::cb_message);
339  COIDEF_ErrMsg(m->cntvect, &ConoptInterface::cb_errmsg);
340  COIDEF_UsrMem(m->cntvect, m);
341 
342  return 0;
343  }
344 
345  void ConoptInterface::set_work(void* mem, const double**& arg, double**& res,
346  casadi_int*& iw, double*& w) const {
347  Nlpsol::set_work(mem, arg, res, iw, w);
348  }
349 
350  // conopt_to_casadi/conopt_type/conopt_rhs always grow in lockstep, so a single
351  // capacity check (on conopt_to_casadi) is enough to decide whether to grow all
352  // three. Growth is by 0.25 of the CasADi rows not yet processed, rather than
353  // jumping straight to the worst-case (all-range) size.
354  void ConoptInterface::ensure_row_capacity(ConoptMemory* m, casadi_int remaining_rows) const {
355  if (m->conopt_to_casadi.size() == m->conopt_to_casadi.capacity()) {
356  casadi_int growth = std::max<casadi_int>(
357  std::min<casadi_int>(remaining_rows, 10), remaining_rows / 4);
358  casadi_int new_cap = m->conopt_to_casadi.capacity() + growth;
359  m->conopt_to_casadi.reserve(new_cap);
360  m->conopt_type.reserve(new_cap);
361  m->conopt_rhs.reserve(new_cap);
362  }
363  }
364 
365  int ConoptInterface::solve(void* mem) const {
366  auto m = static_cast<ConoptMemory*>(mem);
367  m->cache_valid = false;
368  m->cache_valid_jac = false;
369  m->cached_f = 0.0;
370  m->nan_encountered = false;
371  m->modsta = ConoptModelStatus::Unset;
372  m->solsta = ConoptSolverStatus::Unset;
373  m->iter = 0;
374  m->return_status = "Unset";
375 
376  // Build the per-solve constraint expansion (splits range constraints into two rows)
377  std::fill(m->row_const_.begin(), m->row_const_.end(), 0.0);
378  m->obj_const_lin_ = 0.0;
379  m->conopt_to_casadi.clear();
380  m->casadi_to_conopt_ub_row.assign(ng_, -1);
381  m->conopt_type.clear();
382  m->conopt_rhs.clear();
383 
384  // Compute total nnz per CasADi row (needed for range-constraint NZ duplication)
385  std::fill(m->row_nnz.begin(), m->row_nnz.end(), 0);
386  {
387  const casadi_int* g_row_s = jacg_sp_.row();
388  for (casadi_int el = 0; el < (casadi_int)jacg_sp_.nnz(); ++el)
389  m->row_nnz[g_row_s[el]]++;
390  }
391 
392  casadi_int ng_expanded = 0;
393  // Objective gradient NZs are added to numnz after nlp_grad_f is evaluated
394  // (so that gradf_const_vals is populated before we check for non-zero linear entries).
395  casadi_int numnz = (casadi_int)jacg_sp_.nnz();
396 
397  for (casadi_int i = 0; i < ng_; ++i) {
398  double lbg = m->d_nlp.lbz[nx_ + i];
399  double ubg = m->d_nlp.ubz[nx_ + i];
400  bool is_range = !std::isinf(lbg) && !std::isinf(ubg) && lbg != ubg;
401 
402  // CONOPT row 0 is reserved for the objective, so constraint rows start at 1
403  // (arrays are still plain 0-based C arrays; only the row *numbering* is offset).
404  m->casadi_to_conopt_lb_row[i] = static_cast<int>(ng_expanded + 1);
405  ensure_row_capacity(m, ng_ - i);
406  m->conopt_to_casadi.push_back(static_cast<int>(i));
407  if (lbg == ubg) {
408  m->conopt_type.push_back(ConoptRowType::Equality); m->conopt_rhs.push_back(lbg);
409  } else if (!std::isinf(lbg) && std::isinf(ubg)) {
410  m->conopt_type.push_back(ConoptRowType::GreaterEqual); m->conopt_rhs.push_back(lbg);
411  } else if (std::isinf(lbg) && !std::isinf(ubg)) {
412  m->conopt_type.push_back(ConoptRowType::LessEqual); m->conopt_rhs.push_back(ubg);
413  } else if (std::isinf(lbg) && std::isinf(ubg)) {
414  m->conopt_type.push_back(ConoptRowType::Free); m->conopt_rhs.push_back(0.0);
415  } else {
416  // range: >= row
417  m->conopt_type.push_back(ConoptRowType::GreaterEqual);
418  m->conopt_rhs.push_back(lbg);
419  }
420  ng_expanded++;
421 
422  if (is_range) {
423  m->casadi_to_conopt_ub_row[i] = static_cast<int>(ng_expanded + 1);
424  ensure_row_capacity(m, ng_ - i);
425  m->conopt_to_casadi.push_back(static_cast<int>(i)); // <= row
426  m->conopt_type.push_back(ConoptRowType::LessEqual);
427  m->conopt_rhs.push_back(ubg);
428  ng_expanded++;
429  numnz += m->row_nnz[i];
430  }
431  }
432  casadi_assert(ng_expanded <= std::numeric_limits<int>::max(), "ng_expanded overflows int");
433  m->ng_expanded = static_cast<int>(ng_expanded);
434 
435  // Empty Jacobian rows also need their constant terms moved into the RHS.
436  bool has_affine_g = has_linear_jac_ ||
437  std::any_of(m->row_nnz.begin(), m->row_nnz.end(), [](int n) { return n == 0; });
438  if (has_affine_g) {
439  m->arg[0] = m->d_nlp.z;
440  m->arg[1] = m->d_nlp.p;
441  m->res[0] = m->cached_g.data();
442  m->res[1] = has_linear_jac_ ? m->const_jac_vals.data() : nullptr;
443  try {
444  if (calc_function(m, "nlp_jac_g")) {
445  m->success = false;
446  m->unified_return_status = SOLVER_RET_NAN;
447  m->return_status = "Initial evaluation failed";
448  return 0;
449  }
450  } catch (std::exception& ex) {
451  casadi::uerr() << "CONOPT: initial evaluation failed: " << ex.what() << std::endl;
452  return 1;
453  } catch (...) {
454  casadi::uerr() << "CONOPT: initial evaluation failed (unknown exception)" << std::endl;
455  return 1;
456  }
457  }
458 
459  // CONOPT evaluates affine rows internally; absorb their constants into the RHS.
460  if (has_affine_g) {
461  const casadi_int* g_colind_c = jacg_sp_.colind();
462  const casadi_int* g_row_c = jacg_sp_.row();
463 
464  // Accumulate the linear part of G at x0 per row: sum_j a_j * x0_j
465  std::fill(m->linear_at_x0.begin(), m->linear_at_x0.end(), 0.0);
466  for (int c = 0; c < nx_; ++c) {
467  for (casadi_int el = g_colind_c[c]; el < g_colind_c[c + 1]; ++el) {
468  if (jacg_nlflag_[el] == 0)
469  m->linear_at_x0[g_row_c[el]] += m->const_jac_vals[el] * m->d_nlp.z[c];
470  }
471  }
472 
473  for (int ci = 0; ci < ng_; ++ci) {
474  // Only adjust fully linear rows (no nonlinear Jacobian entries)
475  if (jacg_rowstart_[ci + 1] != jacg_rowstart_[ci]) continue;
476  double constant = m->cached_g[ci] - m->linear_at_x0[ci];
477  if (std::abs(constant) < 1e-14) continue;
478  m->row_const_[ci] = constant;
479  int lb_row = m->casadi_to_conopt_lb_row[ci];
480  m->conopt_rhs[lb_row - 1] -= constant;
481  int ub_row = m->casadi_to_conopt_ub_row[ci];
482  if (ub_row >= 0) m->conopt_rhs[ub_row - 1] -= constant;
483  }
484  }
485 
486  // Evaluate objective gradient at initial point for constant (linear) entries,
487  // or to capture the function value when the gradient is structurally empty.
488  m->obj_const_ = std::numeric_limits<double>::quiet_NaN();
489  if (has_linear_gradf_ || gradf_sp_.nnz() == 0) {
490  m->arg[0] = m->d_nlp.z;
491  m->arg[1] = m->d_nlp.p;
492  m->res[0] = &m->cached_f;
493  m->res[1] = has_linear_gradf_ ? m->gradf_const_vals.data() : nullptr;
494  try {
495  if (calc_function(m, "nlp_grad_f")) {
496  m->success = false;
497  m->unified_return_status = SOLVER_RET_NAN;
498  m->return_status = "Initial evaluation failed";
499  return 0;
500  }
501  } catch (std::exception& ex) {
502  casadi::uerr() << "CONOPT: initial evaluation failed: " << ex.what() << std::endl;
503  return 1;
504  } catch (...) {
505  casadi::uerr() << "CONOPT: initial evaluation failed (unknown exception)" << std::endl;
506  return 1;
507  }
508  }
509 
510  // Detect constant objective at solve time: no nonlinear gradient entries and
511  // all linear-gradient values are zero (objective has no x-dependence).
512  // Switch to feasibility mode so CONOPT doesn't report OBJVAL=0 for an empty row.
513  {
514  bool has_nl_gradf = std::any_of(gradf_nlflag_.begin(), gradf_nlflag_.end(),
515  [](int f) { return f == 1; });
516  bool all_const_zero = has_linear_gradf_ &&
517  std::all_of(m->gradf_const_vals.begin(), m->gradf_const_vals.end(),
518  [](double v) { return v == 0.0; });
519  if (!has_nl_gradf && (gradf_sp_.nnz() == 0 || all_const_zero)) {
520  m->obj_const_ = m->cached_f;
521  COIDEF_OptDir(m->cntvect, 0);
522  } else {
523  if (!has_nl_gradf) {
524  // CONOPT omits the affine objective's constant term; recover it at x0.
525  const casadi_int* f_row_c = gradf_sp_.row();
526  double lin_at_x0 = 0.0;
527  for (casadi_int k = 0; k < gradf_sp_.nnz(); ++k)
528  lin_at_x0 += m->gradf_const_vals[k] * m->d_nlp.z[f_row_c[k]];
529  m->obj_const_lin_ = m->cached_f - lin_at_x0;
530  }
531  // cntvect persists across solves, so explicitly reset OptDir in case a
532  // prior solve on this instance (e.g. with different parameters) hit the
533  // constant-objective branch above and left it at 0.
534  COIDEF_OptDir(m->cntvect, -1);
535  }
536  }
537 
538  // Count nonlinear NZ: nonlinear objective gradient entries + nonlinear constraint entries
539  const casadi_int* g_colind_s = jacg_sp_.colind();
540  const casadi_int* g_row_s = jacg_sp_.row();
541  casadi_int num_nl_nz = 0;
542  for (casadi_int k = 0; k < (casadi_int)gradf_sp_.nnz(); ++k)
543  if (gradf_nlflag_[k]) num_nl_nz++;
544  for (casadi_int c = 0; c < nx_; ++c) {
545  for (casadi_int el = g_colind_s[c]; el < g_colind_s[c+1]; ++el) {
546  if (jacg_nlflag_[el]) {
547  int ci = static_cast<int>(g_row_s[el]);
548  num_nl_nz += (m->casadi_to_conopt_ub_row[ci] >= 0) ? 2 : 1;
549  }
550  }
551  }
552 
553  casadi_assert(num_nl_nz <= std::numeric_limits<int>::max(), "num_nl_nz overflows int");
554 
555  // Count objective gradient NZs now that gradf_const_vals has been populated.
556  // Nonlinear entries are always included; linear (constant) entries only when
557  // non-zero — a zero constant gradient contributes nothing to the objective row
558  // and must not occupy a slot in the CONOPT matrix structure.
559  {
560  casadi_int numnz_f = 0;
561  for (casadi_int k = 0; k < (casadi_int)gradf_sp_.nnz(); ++k) {
562  if (gradf_nlflag_[k] == 1 ||
563  (has_linear_gradf_ && m->gradf_const_vals[k] != 0.0))
564  numnz_f++;
565  }
566  numnz += numnz_f;
567  }
568  casadi_assert(numnz <= std::numeric_limits<int>::max(), "numnz overflows int");
569  m->numnz_expanded = static_cast<int>(numnz);
570 
571  COIDEF_NumCon(m->cntvect, static_cast<int>(ng_expanded + 1));
572  COIDEF_NumNz(m->cntvect, static_cast<int>(numnz));
573  COIDEF_NumNlNz(m->cntvect, static_cast<int>(num_nl_nz));
574 
575  int ret = COI_Solve(m->cntvect);
576 
577  // Restore the affine objective's constant term (cb_status only saw grad'x).
578  m->d_nlp.objective += m->obj_const_lin_;
579 
580  // Restore constant objective value when CONOPT ran in feasibility mode.
581  if (!std::isnan(m->obj_const_)) m->d_nlp.objective = m->obj_const_;
582 
583  if (ret != 0) {
584  m->success = false;
585  m->unified_return_status = m->nan_encountered ? SOLVER_RET_NAN : SOLVER_RET_UNKNOWN;
586  return 0;
587  }
588 
589  m->success = !m->nan_encountered &&
590  (m->modsta == ConoptModelStatus::Optimal ||
591  m->modsta == ConoptModelStatus::LocallyOptimal) &&
593 
594  if (m->nan_encountered || m->solsta == ConoptSolverStatus::EvalErrorLimit) {
595  m->unified_return_status = SOLVER_RET_NAN;
596  } else if (m->success) {
597  m->unified_return_status = SOLVER_RET_SUCCESS;
598  } else {
599  if (m->solsta == ConoptSolverStatus::IterationLimit ||
600  m->solsta == ConoptSolverStatus::TimeLimit ||
601  m->solsta == ConoptSolverStatus::UserInterrupt ||
603  m->unified_return_status = SOLVER_RET_LIMITED;
604  } else if (m->modsta == ConoptModelStatus::Infeasible ||
606  m->unified_return_status = SOLVER_RET_INFEASIBLE;
607  }
608  }
609  return 0;
610  }
611 
612  Dict ConoptInterface::get_stats(void* mem) const {
613  Dict stats = Nlpsol::get_stats(mem);
614  auto m = static_cast<ConoptMemory*>(mem);
615  stats["return_status"] = m->return_status;
616  stats["modsta"] = static_cast<int>(m->modsta);
617  stats["solsta"] = static_cast<int>(m->solsta);
618  stats["iter_count"] = m->iter;
619  return stats;
620  }
621 
622  // --- Dynamic Option Callback --- //
623  int COI_CALLCONV ConoptInterface::cb_option(int NCALL, double* RVAL, int* IVAL,
624  int* LVAL, char* NAME, void* USRMEM) {
625  auto m = static_cast<ConoptMemory*>(USRMEM);
626 
627  if (NCALL >= static_cast<int>(m->custom_options.size())) {
628  NAME[0] = '\0';
629  return 0;
630  }
631 
632  auto& opt = m->custom_options[NCALL];
633  // NAME has a fixed-size buffer; init() validates the option-name length.
634  size_t name_len = std::min(opt.first.size(), conopt_max_option_name);
635  std::memcpy(NAME, opt.first.c_str(), name_len);
636  NAME[name_len] = '\0';
637 
638  if (opt.second.is_double()) {
639  *RVAL = opt.second.to_double();
640  if (m->self.debug_)
641  casadi::uout() << "CONOPT option: " << opt.first << " = " << *RVAL << std::endl;
642  } else if (opt.second.is_int()) {
643  *IVAL = opt.second.to_int();
644  if (m->self.debug_)
645  casadi::uout() << "CONOPT option: " << opt.first << " = " << *IVAL << std::endl;
646  } else if (opt.second.is_bool()) {
647  *LVAL = opt.second.to_bool() ? 1 : 0;
648  if (m->self.debug_) {
649  casadi::uout() << "CONOPT option: " << opt.first << " = "
650  << (opt.second.to_bool() ? "true" : "false") << std::endl;
651  }
652  } else if (opt.second.is_string()) {
653  // init_mem filters out all string options before they enter custom_options
654  // (with a casadi_warning directing the user to 'optfile'). Reaching this
655  // branch means custom_options was populated externally in a way that
656  // bypasses that filter, which is a programming error. Setting NAME[0]='\0'
657  // here would terminate the entire option enumeration, silently dropping
658  // all subsequent entries — so we assert rather than pretend to skip.
659  casadi_error("CONOPT option '" + opt.first + "' is a string type in cb_option. "
660  "String options cannot be passed via the CONOPT option callback "
661  "(COI_OPTION_t has no SVAL parameter). Use the 'optfile' option "
662  "instead. The init_mem filter should have removed this option "
663  "before it reached custom_options; reaching this branch is a "
664  "programming error.");
665  } else {
666  // Similarly, an option of unknown type must never reach this point.
667  casadi_error("CONOPT option '" + opt.first + "' has an unknown GenericType in "
668  "cb_option. Only double, int, and bool options are valid here. "
669  "Reaching this branch is a programming error.");
670  }
671 
672  return 0;
673  }
674 
675  // --- Progress / Interrupt Callback --- //
676  int COI_CALLCONV ConoptInterface::cb_progress(int LEN_INT, const int INTX[], int LEN_RL,
677  const double RL[], const double X[],
678  void* USRMEM) {
679  auto m = static_cast<ConoptMemory*>(USRMEM);
680  const ConoptInterface& self = m->self;
681 
682  if (!self.fcallback_.is_null()) {
683  int phase = (LEN_INT > 1) ? INTX[1] : -1;
684 
685  double obj_val = m->cached_f; // best available approximation for early phases
686  if (LEN_RL > 1 && phase >= 3) {
687  obj_val = RL[1]; // CONOPT-reported value once available
688  }
689 
690  std::fill_n(m->arg, self.fcallback_.n_in(), nullptr);
691  m->arg[NLPSOL_X] = X;
692  m->arg[NLPSOL_F] = &obj_val;
693 
694  std::fill_n(m->res, self.fcallback_.n_out(), nullptr);
695  double ret_double = 0;
696  m->res[0] = &ret_double;
697 
698  try {
699  self.fcallback_(m->arg, m->res, m->iw, m->w, 0);
700  if (ret_double != 0.0) return 1;
701  } catch (KeyboardInterruptException& ex) {
702  return 1;
703  } catch (std::exception& ex) {
704  casadi_warning(std::string("intermediate_callback: ") + ex.what());
705  if (!self.iteration_callback_ignore_errors_) return 1;
706  }
707  }
708  return 0;
709  }
710 
711  int COI_CALLCONV ConoptInterface::cb_read_matrix(double LOWER[], double CURR[],
712  double UPPER[], int VSTA[], int TYPEX[],
713  double RHS[], int ESTA[], int COLSTA[],
714  int ROWNO[], double VALUE[],
715  int NLFLAG[], int NUMVAR, int NUMCON,
716  int NUMNZ, void* USRMEM) {
717  auto m = static_cast<ConoptMemory*>(USRMEM);
718  const ConoptInterface& self = m->self;
719 
720  // CONOPT is expected to echo back exactly what we told it via COIDEF_NumVar/
721  // COIDEF_NumCon in init_mem()/solve() — a mismatch means the interface's own
722  // bookkeeping (nx_, ng_expanded) is desynced from what CONOPT is using.
723  casadi_assert(NUMVAR == self.nx_, "cb_read_matrix: NUMVAR != nx_");
724  casadi_assert(NUMCON == m->ng_expanded + 1, "cb_read_matrix: NUMCON != ng_expanded + 1");
725 
726  // Variable bounds and initial point (clamped to [lb, ub])
727  for (int i = 0; i < NUMVAR; ++i) {
728  double lb = m->d_nlp.lbz[i];
729  double ub = m->d_nlp.ubz[i];
730  if (!std::isinf(lb)) LOWER[i] = lb;
731  if (!std::isinf(ub)) UPPER[i] = ub;
732  double x0 = m->d_nlp.z[i];
733  if (!std::isinf(ub)) x0 = std::min(x0, ub);
734  if (!std::isinf(lb)) x0 = std::max(x0, lb);
735  CURR[i] = x0;
736  }
737 
738  // Constraint types and RHS (row 0 = objective, rows 1..ng_expanded = constraints).
739  // conopt_type/conopt_rhs come from solve()'s range-constraint expansion —
740  // they depend on this call's numeric lbg/ubg, not just problem structure.
741  TYPEX[0] = static_cast<int>(ConoptRowType::Free);
742  RHS[0] = 0.0;
743  for (int r = 0; r < m->ng_expanded; ++r) {
744  TYPEX[r + 1] = static_cast<int>(m->conopt_type[r]);
745  RHS[r + 1] = m->conopt_rhs[r];
746  }
747 
748  if (self.warm_start_) {
749  // conopt_to_casadi/casadi_to_conopt_lb_row/ub_row (from solve()) map each
750  // expanded CONOPT row back to its CasADi constraint, to look up lam there.
751  ESTA[0] = static_cast<int>(ConoptBasisStatus::SuperBasic); // objective row always superbasic
752 
753  // lam is the prior solve's dual solution, length nx_+ng_. lam[0..nx_-1] are
754  // variable-bound multipliers, lam[nx_..] are constraint multipliers.
755  const double* lam = m->d_nlp.lam;
756  bool all_zero = std::all_of(lam, lam + self.nx_ + self.ng_,
757  [](double v) { return v == 0.0; });
758  if (!all_zero) {
759  for (int i = 0; i < NUMVAR; ++i) {
760  double lbi = m->d_nlp.lbz[i];
761  double ubi = m->d_nlp.ubz[i];
762  double xi = m->d_nlp.z[i];
763  double li = lam[i]; // nonzero multiplier => that bound is active, so nonbasic
764  if (!std::isinf(lbi) && std::fabs(xi - lbi) < 1e-8 && li <= 0.0)
765  VSTA[i] = static_cast<int>(ConoptBasisStatus::AtLower);
766  else if (!std::isinf(ubi) && std::fabs(xi - ubi) < 1e-8 && li >= 0.0)
767  VSTA[i] = static_cast<int>(ConoptBasisStatus::AtUpper);
768  else
769  VSTA[i] = static_cast<int>(ConoptBasisStatus::Basic);
770  }
771 
772  for (int r = 0; r < m->ng_expanded; ++r) {
773  int ci = m->conopt_to_casadi[r];
774  // this constraint's multiplier, same sign logic as above
775  double lam_ci = lam[NUMVAR + ci];
776  int row1 = m->casadi_to_conopt_lb_row[ci];
777  int row2 = m->casadi_to_conopt_ub_row[ci];
778  if (r + 1 == row1) {
779  ConoptRowType ctype = m->conopt_type[r]; // type of this CONOPT expanded row
780  if (ctype == ConoptRowType::Equality) { // equality: both sides, just mark basic
781  ESTA[r + 1] = static_cast<int>(ConoptBasisStatus::Basic);
782  } else if (ctype == ConoptRowType::GreaterEqual) { // >= row: active when lam_ci < 0
783  ESTA[r + 1] = static_cast<int>(
785  } else if (ctype == ConoptRowType::LessEqual) {
786  // <= row (pure <= stored as lb_row): active when lam_ci > 0
787  ESTA[r + 1] = static_cast<int>(
789  } else { // free row: superbasic
790  ESTA[r + 1] = static_cast<int>(ConoptBasisStatus::SuperBasic);
791  }
792  } else if (r + 1 == row2) {
793  // range ub side is active when lam_ci > 0
794  ESTA[r + 1] = static_cast<int>(
796  } else {
797  ESTA[r + 1] = static_cast<int>(ConoptBasisStatus::Basic);
798  }
799  }
800  }
801  }
802 
803  // Jacobian structure — built live from jacg_sp_ with range-row duplication.
804  // Column/sparsity/linearity data (jacg_sp_, gradf_col_flag_, gradf_col_to_nz_,
805  // gradf_nlflag_, jacg_nlflag_) is structural, fixed since init(). Row mapping
806  // (casadi_to_conopt_lb_row/ub_row) and numeric values (gradf_const_vals,
807  // const_jac_vals) are this solve()'s numeric data.
808  const casadi_int* g_colind = self.jacg_sp_.colind();
809  const casadi_int* g_row = self.jacg_sp_.row();
810  int nz = 0;
811  for (int c = 0; c < NUMVAR; ++c) {
812  COLSTA[c] = nz;
813  if (self.gradf_col_flag_[c]) {
814  // Objective-gradient entry in column c, if any (row 0 = objective).
815  casadi_int k = self.gradf_col_to_nz_[c];
816  if (self.gradf_nlflag_[k] == 1) {
817  ROWNO[nz] = 0;
818  NLFLAG[nz] = 1;
819  nz++;
820  } else if (m->gradf_const_vals[k] != 0.0) {
821  ROWNO[nz] = 0;
822  NLFLAG[nz] = 0;
823  VALUE[nz] = m->gradf_const_vals[k];
824  nz++;
825  }
826  }
827  // Constraint-Jacobian entries in column c; duplicated onto the ub row too
828  // when this CasADi row was split into a range constraint by solve().
829  for (casadi_int el = g_colind[c]; el < g_colind[c+1]; ++el) {
830  int ci = static_cast<int>(g_row[el]);
831  int nlflag = self.jacg_nlflag_[el];
832  ROWNO[nz] = m->casadi_to_conopt_lb_row[ci];
833  NLFLAG[nz] = nlflag;
834  if (nlflag == 0) VALUE[nz] = m->const_jac_vals[el];
835  nz++;
836  if (m->casadi_to_conopt_ub_row[ci] >= 0) {
837  ROWNO[nz] = m->casadi_to_conopt_ub_row[ci];
838  NLFLAG[nz] = nlflag;
839  if (nlflag == 0) VALUE[nz] = m->const_jac_vals[el];
840  nz++;
841  }
842  }
843  }
844  COLSTA[NUMVAR] = nz;
845 
846  casadi_assert(nz == NUMNZ,
847  "cb_read_matrix: nz != NUMNZ - Jacobian nonzero count mismatch "
848  "between solve()'s numnz computation and this callback's writes");
849 
850  return 0;
851  }
852 
853  int COI_CALLCONV ConoptInterface::cb_fdevalini(const double X[], const int ROWLIST[],
854  int MODE, int LISTSIZE, int NUMTHREAD,
855  int IGNERR, int* ERRCNT, int NUMVAR,
856  void* USRMEM) {
857  auto m = static_cast<ConoptMemory*>(USRMEM);
858  const ConoptInterface& self = m->self;
859 
860  casadi_assert(NUMVAR == self.nx_, "cb_fdevalini: NUMVAR != nx_");
861  casadi_assert(static_cast<size_t>(NUMVAR) == m->cached_x.size(),
862  "cb_fdevalini: NUMVAR != cached_x size");
863 
864  std::memcpy(m->cached_x.data(), X, NUMVAR * sizeof(double));
865 
866  if (self.debug_) {
867  casadi::uout() << "FDEvalIni x:";
868  for (int i = 0; i < NUMVAR; ++i)
869  casadi::uout() << " " << X[i];
870  casadi::uout() << "\n";
871  }
872 
873  const bool need_jac = (MODE != 1);
874 
875  m->cache_valid_jac.store(false, std::memory_order_relaxed);
876  m->cache_valid.store(false, std::memory_order_relaxed);
877  try {
878  m->arg[0] = m->cached_x.data();
879  m->arg[1] = m->d_nlp.p;
880 
881  m->res[0] = &m->cached_f;
882  m->res[1] = need_jac ? m->cached_grad_f.data() : nullptr;
883  int ret = self.calc_function(m, "nlp_grad_f");
884  if (!ret) {
885  m->res[0] = m->cached_g.data();
886  m->res[1] = need_jac ? m->cached_jac_g.data() : nullptr;
887  ret = self.calc_function(m, "nlp_jac_g");
888  }
889  if (!ret) {
890  // Publish the cache only after both evaluations succeed.
891  m->cache_valid_jac.store(need_jac, std::memory_order_relaxed);
892  m->cache_valid.store(true, std::memory_order_release);
893  }
894  } catch (std::exception& ex) {
895  casadi::uerr() << ex.what() << std::endl;
896  } catch (...) {
897  }
898  if (!m->cache_valid.load(std::memory_order_relaxed)) {
899  *ERRCNT = 1;
900  m->nan_encountered = true;
901  }
902  return 0;
903  }
904 
905  int COI_CALLCONV ConoptInterface::cb_fd_eval(const double X[], double* G, double JAC[],
906  int ROWNO, const int JACNUM[], int MODE,
907  int IGNERR, int* ERRCNT, int NUMVAR,
908  int NUMJAC, int THREAD, void* USRMEM) {
909  auto m = static_cast<ConoptMemory*>(USRMEM);
910  const ConoptInterface& self = m->self;
911 
912  // Acquire load: establishes happens-before with the release store in cb_fdevalini,
913  // making all cache writes (including cached_jac_g and cache_valid_jac) visible here.
914  if (!m->cache_valid.load(std::memory_order_acquire)) {
915  *ERRCNT = 1;
916  return 0;
917  }
918 
919  if ((MODE == 2 || MODE == 3) &&
920  !m->cache_valid_jac.load(std::memory_order_relaxed)) {
921  *ERRCNT = 1;
922  return 0;
923  }
924 
925 #ifndef NDEBUG
926  casadi_assert(
927  std::memcmp(X, m->cached_x.data(), NUMVAR * sizeof(double)) == 0,
928  "cb_fd_eval: X does not match cached_x — CONOPT API contract violated");
929 #endif
930 
931  if (ROWNO == 0) {
932  if (MODE == 1 || MODE == 3) *G = m->cached_f;
933  if (MODE == 2 || MODE == 3) {
934  const casadi_int* f_row = self.gradf_sp_.row();
935  for (casadi_int k = 0; k < self.gradf_sp_.nnz(); ++k) {
936  if (self.gradf_nlflag_[k]) {
937  JAC[f_row[k]] = m->cached_grad_f[k];
938  if (self.debug_)
939  casadi::uout() << " df/dx[" << f_row[k] << "] = "
940  << m->cached_grad_f[k] << "\n";
941  }
942  }
943  }
944  } else {
945  casadi_assert(ROWNO >= 1 && ROWNO <= m->ng_expanded,
946  "cb_fd_eval: ROWNO out of range");
947  int ci = m->conopt_to_casadi[ROWNO - 1];
948  if (MODE == 1 || MODE == 3) {
949  *G = m->cached_g[ci];
950  if (self.debug_) {
951  ConoptRowType ctype = m->conopt_type[ROWNO - 1];
952  double rhs = m->conopt_rhs[ROWNO - 1];
953  const char* rel = (ctype == ConoptRowType::Equality) ? "=" :
954  (ctype == ConoptRowType::GreaterEqual) ? ">=" :
955  (ctype == ConoptRowType::LessEqual) ? "<=" : "free";
956  if (ctype == ConoptRowType::Free)
957  casadi::uout() << " g[" << ci << "](x) = " << *G << " (free)\n";
958  else
959  casadi::uout() << " g[" << ci << "](x) = " << *G << " "
960  << rel << " " << rhs << "\n";
961  }
962  }
963  if (MODE == 2 || MODE == 3) {
964  int base = self.jacg_rowstart_[ci];
965  int count = self.jacg_rowstart_[ci + 1] - self.jacg_rowstart_[ci];
966  for (int k = 0; k < count; ++k) {
967  int col = self.jacg_col_[base + k];
968  double val = m->cached_jac_g[self.jacg_nzidx_[base + k]];
969  JAC[col] = val;
970  if (self.debug_)
971  casadi::uout() << " dg[" << ci << "]/dx[" << col << "] = " << val << "\n";
972  }
973  }
974  }
975  return 0;
976  }
977 
978  int COI_CALLCONV ConoptInterface::cb_fdevalend(int IGNERR, int* ERRCNT, void* USRMEM) {
979  auto m = static_cast<ConoptMemory*>(USRMEM);
980  m->cache_valid_jac.store(false, std::memory_order_relaxed);
981  m->cache_valid.store(false, std::memory_order_relaxed);
982  return 0;
983  }
984 
985  int COI_CALLCONV ConoptInterface::cb_2dlagrstr(int HSRW[], int HSCL[], int* NODRV,
986  int NUMVAR, int NUMCON, int NHESS,
987  void* USRMEM) {
988  auto m = static_cast<ConoptMemory*>(USRMEM);
989  const ConoptInterface& self = m->self;
990  const casadi_int* colind = self.hesslag_sp_.colind();
991  const casadi_int* row = self.hesslag_sp_.row();
992 
993  int idx = 0;
994  for (int c = 0; c < NUMVAR; ++c) {
995  for (casadi_int el = colind[c]; el < colind[c+1]; ++el) {
996  HSRW[idx] = row[el];
997  HSCL[idx] = c;
998  idx++;
999  }
1000  }
1001  casadi_assert(idx == NHESS,
1002  "cb_2dlagrstr: idx != NHESS - Hessian nonzero count mismatch");
1003  return 0;
1004  }
1005 
1006  int COI_CALLCONV ConoptInterface::cb_2dlagrval(const double X[], const double U[],
1007  const int HSRW[], const int HSCL[],
1008  double HSVL[], int* NODRV, int NUMVAR,
1009  int NUMCON, int NHESS, void* USRMEM) {
1010  auto m = static_cast<ConoptMemory*>(USRMEM);
1011  const ConoptInterface& self = m->self;
1012 
1013  // CONOPT's Lagrangian: L = SUM(r) U(r) * F(r), so d²L/dx² = SUM(r) U(r) * d²F(r)/dx².
1014  // CasADi computes lam_f*d²f/dx² + lam_g^T*d²g/dx², so lam_f = U[0], lam_g[ci] = U[row_ci].
1015  double obj_factor = U[0];
1016 
1017  for (int ci = 0; ci < self.ng_; ++ci) {
1018  int row1 = m->casadi_to_conopt_lb_row[ci];
1019  int row2 = m->casadi_to_conopt_ub_row[ci];
1020  m->hess_lam_g_[ci] = U[row1] + (row2 >= 0 ? U[row2] : 0.0);
1021  }
1022 
1023  if (self.debug_) {
1024  casadi::uout() << "Hessian lam_f=" << obj_factor << " lam_g:";
1025  for (int ci = 0; ci < self.ng_; ++ci)
1026  casadi::uout() << " " << m->hess_lam_g_[ci];
1027  casadi::uout() << "\n";
1028  }
1029 
1030  m->arg[0] = X;
1031  m->arg[1] = m->d_nlp.p;
1032  m->arg[2] = &obj_factor;
1033  m->arg[3] = m->hess_lam_g_.data();
1034  m->res[0] = HSVL;
1035 
1036  try {
1037  if (self.calc_function(m, "nlp_hess_l")) {
1038  *NODRV = 1;
1039  return 0;
1040  }
1041  if (self.debug_) {
1042  casadi::uout() << "Hessian values (HSVL):";
1043  for (int i = 0; i < NHESS; ++i)
1044  casadi::uout() << " " << HSVL[i];
1045  casadi::uout() << "\n";
1046  }
1047  } catch (std::exception& ex) {
1048  casadi::uerr() << "CONOPT: nlp_hess_l failed: " << ex.what() << std::endl;
1049  *NODRV = 1;
1050  } catch (...) {
1051  casadi::uerr() << "CONOPT: nlp_hess_l failed (unknown exception)" << std::endl;
1052  *NODRV = 1;
1053  }
1054  return 0;
1055  }
1056 
1057  int COI_CALLCONV ConoptInterface::cb_status(int MODSTA, int SOLSTA, int ITER,
1058  double OBJVAL, void* USRMEM) {
1059  auto m = static_cast<ConoptMemory*>(USRMEM);
1060  m->modsta = static_cast<ConoptModelStatus>(MODSTA);
1061  m->solsta = static_cast<ConoptSolverStatus>(SOLSTA);
1062  m->iter = ITER;
1063  m->d_nlp.objective = OBJVAL;
1064 
1065  const char* modsta_str;
1066  switch (m->modsta) {
1067  case ConoptModelStatus::Optimal: modsta_str = "Optimal"; break;
1068  case ConoptModelStatus::LocallyOptimal: modsta_str = "Locally optimal"; break;
1069  case ConoptModelStatus::Unbounded: modsta_str = "Unbounded"; break;
1070  case ConoptModelStatus::Infeasible: modsta_str = "Infeasible"; break;
1071  case ConoptModelStatus::LocallyInfeasible: modsta_str = "Locally infeasible"; break;
1072  case ConoptModelStatus::IntermediateInfeas: modsta_str = "Intermediate infeasible"; break;
1073  case ConoptModelStatus::IntermediateNonOpt: modsta_str = "Intermediate non-optimal"; break;
1074  case ConoptModelStatus::UnknownError: modsta_str = "Unknown error"; break;
1075  case ConoptModelStatus::ErrorNoSolution: modsta_str = "Error: no solution"; break;
1076  default: modsta_str = "Unknown model status"; break;
1077  }
1078 
1079  const char* solsta_str;
1080  switch (m->solsta) {
1082  solsta_str = "Normal completion"; break;
1084  solsta_str = "Iteration limit"; break;
1086  solsta_str = "Time limit"; break;
1088  solsta_str = "Terminated by solver"; break;
1090  solsta_str = "Evaluation error limit"; break;
1092  solsta_str = "User interrupt"; break;
1094  solsta_str = "Setup failure"; break;
1096  solsta_str = "Major solver error"; break;
1098  solsta_str = "Major solver error (feasible point)"; break;
1100  solsta_str = "System error"; break;
1102  solsta_str = "Quick Mode termination"; break;
1103  default:
1104  solsta_str = "Unknown solver status"; break;
1105  }
1106 
1107  m->return_status = std::string(modsta_str) + " / " + std::string(solsta_str);
1108  return 0;
1109  }
1110 
1111  int COI_CALLCONV ConoptInterface::cb_solution(const double XVAL[], const double XMAR[],
1112  const int XBAS[], const int XSTA[],
1113  const double YVAL[], const double YMAR[],
1114  const int YBAS[], const int YSTA[],
1115  int NUMVAR, int NUMCON, void* USRMEM) {
1116  auto m = static_cast<ConoptMemory*>(USRMEM);
1117 
1118  casadi_copy(XVAL, NUMVAR, m->d_nlp.z);
1119 
1120  if (m->self.debug_) {
1121  casadi::uout() << "Solution x:";
1122  for (int i = 0; i < NUMVAR; ++i)
1123  casadi::uout() << " " << XVAL[i];
1124  casadi::uout() << "\n";
1125  }
1126 
1127  // Use the first row of each constraint and restore constants absorbed into the RHS.
1128  for (casadi_int ci = 0; ci < m->self.ng_; ++ci)
1129  m->d_nlp.z[NUMVAR + ci] = YVAL[m->casadi_to_conopt_lb_row[ci]] + m->row_const_[ci];
1130 
1131  // Variable marginals: CONOPT shadow prices = -CasADi lam_x
1132  for (int i = 0; i < NUMVAR; ++i)
1133  m->d_nlp.lam[i] = -XMAR[i];
1134 
1135  // Constraint marginals: for range constraints sum both rows' shadow prices
1136  // (only the active bound has a non-zero YMAR; summing is always safe)
1137  for (casadi_int ci = 0; ci < m->self.ng_; ++ci) {
1138  int row1 = m->casadi_to_conopt_lb_row[ci];
1139  int row2 = m->casadi_to_conopt_ub_row[ci];
1140  double ymar = YMAR[row1] + (row2 >= 0 ? YMAR[row2] : 0.0);
1141  m->d_nlp.lam[NUMVAR + ci] = -ymar;
1142  }
1143 
1144  return 0;
1145  }
1146 
1147  int COI_CALLCONV ConoptInterface::cb_message(int SMSG, int DMSG, int NMSG, char* MSGV[],
1148  void* USRMEM) {
1149  auto m = static_cast<ConoptMemory*>(USRMEM);
1150 
1151  int message_length = SMSG;
1152  if (m->self.debug_) message_length = std::max(message_length, std::max(DMSG, NMSG));
1153 
1154  for (int i = 0; i < message_length; ++i) {
1155  if (MSGV[i] != nullptr) {
1156  casadi::uout() << MSGV[i] << std::endl;
1157  }
1158  }
1159  return 0;
1160  }
1161 
1162  int COI_CALLCONV ConoptInterface::cb_errmsg(int ROWNO, int COLNO, int POSNO,
1163  const char* MSG, void* USRMEM) {
1164  if (MSG == nullptr) return 0;
1165 
1166  std::string prefix = "CONOPT Error: ";
1167  if (COLNO == -1 && ROWNO >= 0) {
1168  prefix += "Row " + std::to_string(ROWNO) + " - ";
1169  } else if (ROWNO == -1 && COLNO >= 0) {
1170  prefix += "Column " + std::to_string(COLNO) + " - ";
1171  } else if (ROWNO >= 0 && COLNO >= 0) {
1172  if (POSNO >= 0) {
1173  prefix += "Jacobian Pos " + std::to_string(POSNO) + " (Row " +
1174  std::to_string(ROWNO) + ", Col " + std::to_string(COLNO) + ") - ";
1175  } else if (POSNO == -1) {
1176  prefix += "Pair (Row " + std::to_string(ROWNO) + ", Col " +
1177  std::to_string(COLNO) + ") - ";
1178  }
1179  }
1180 
1181  casadi::uerr() << prefix << MSG << std::endl;
1182  return 0;
1183  }
1184 } // namespace casadi
const char * what() const override
Display error.
Definition: exception.hpp:90
static int COI_CALLCONV cb_fdevalini(const double X[], const int ROWLIST[], int MODE, int LISTSIZE, int NUMTHREAD, int IGNERR, int *ERRCNT, int NUMVAR, void *USRMEM)
static int COI_CALLCONV cb_read_matrix(double LOWER[], double CURR[], double UPPER[], int VSTA[], int TYPEX[], double RHS[], int ESTA[], int COLSTA[], int ROWNO[], double VALUE[], int NLFLAG[], int NUMVAR, int NUMCON, int NUMNZ, void *USRMEM)
std::vector< int > jacg_nlflag_
int init_mem(void *mem) const override
Initalize memory block.
void ensure_row_capacity(ConoptMemory *m, casadi_int remaining_rows) const
void serialize_body(SerializingStream &s) const override
Serialize an object without type information.
static int COI_CALLCONV cb_solution(const double XVAL[], const double XMAR[], const int XBAS[], const int XSTA[], const double YVAL[], const double YMAR[], const int YBAS[], const int YSTA[], int NUMVAR, int NUMCON, void *USRMEM)
static int COI_CALLCONV cb_fdevalend(int IGNERR, int *ERRCNT, void *USRMEM)
static int COI_CALLCONV cb_status(int MODSTA, int SOLSTA, int ITER, double OBJVAL, void *USRMEM)
std::vector< bool > gradf_col_flag_
std::vector< int > jacg_rowstart_
Dict get_stats(void *mem) const override
Get all statistics.
static int COI_CALLCONV cb_progress(int LEN_INT, const int INTX[], int LEN_RL, const double RL[], const double X[], void *USRMEM)
std::vector< casadi_int > gradf_col_to_nz_
static int COI_CALLCONV cb_2dlagrstr(int HSRW[], int HSCL[], int *NODRV, int NUMVAR, int NUMCON, int NHESS, void *USRMEM)
ConoptInterface(const std::string &name, const Function &nlp)
static int COI_CALLCONV cb_option(int NCALL, double *RVAL, int *IVAL, int *LVAL, char *NAME, void *USRMEM)
std::vector< int > gradf_nlflag_
static int COI_CALLCONV cb_2dlagrval(const double X[], const double U[], const int HSRW[], const int HSCL[], double HSVL[], int *NODRV, int NUMVAR, int NUMCON, int NHESS, void *USRMEM)
static int COI_CALLCONV cb_message(int SMSG, int DMSG, int NMSG, char *MSGV[], void *USRMEM)
static const Options options_
std::vector< int > jacg_nzidx_
int solve(void *mem) const override
void set_work(void *mem, const double **&arg, double **&res, casadi_int *&iw, double *&w) const override
Set the (persistent) work vectors.
static int COI_CALLCONV cb_fd_eval(const double X[], double *G, double JAC[], int ROWNO, const int JACNUM[], int MODE, int IGNERR, int *ERRCNT, int NUMVAR, int NUMJAC, int THREAD, void *USRMEM)
static Nlpsol * creator(const std::string &name, const Function &nlp)
void free_mem(void *mem) const override
Free memory block.
static const std::string meta_doc
A documentation string.
static int COI_CALLCONV cb_errmsg(int ROWNO, int COLNO, int POSNO, const char *MSG, void *USRMEM)
static ProtoFunction * deserialize(DeserializingStream &s)
void init(const Dict &opts) override
Initialize.
std::vector< int > jacg_col_
Helper class for Serialization.
void unpack(Sparsity &e)
Reconstruct an object from the input stream.
void version(const std::string &name, int v)
Function self() const
Get a public class instance.
Function object.
Definition: function.hpp:60
const Sparsity & sparsity_out(casadi_int ind) const
Get sparsity of a given output.
Definition: function.cpp:1183
const Sparsity sparsity_jac(casadi_int iind, casadi_int oind, bool compact=false, bool symmetric=false) const
Definition: function.cpp:1059
bool is_null() const
Is a null pointer?
NLP solver storage class.
Definition: nlpsol_impl.hpp:59
bool iteration_callback_ignore_errors_
Options.
Definition: nlpsol_impl.hpp:95
Dict get_stats(void *mem) const override
Get all statistics.
Definition: nlpsol.cpp:1251
static const Options options_
Options.
void init(const Dict &opts) override
Initialize.
Definition: nlpsol.cpp:499
casadi_int ng_
Number of constraints.
Definition: nlpsol_impl.hpp:69
int init_mem(void *mem) const override
Initalize memory block.
Definition: nlpsol.cpp:692
void serialize_body(SerializingStream &s) const override
Serialize an object without type information.
Definition: nlpsol.cpp:1409
casadi_int nx_
Number of variables.
Definition: nlpsol_impl.hpp:66
void set_work(void *mem, const double **&arg, double **&res, casadi_int *&iw, double *&w) const override
Set the (persistent) work vectors.
Definition: nlpsol.cpp:884
Function fcallback_
callback function, executed at each iteration
Definition: nlpsol_impl.hpp:75
Function create_function(const Function &oracle, const std::string &fname, const std::vector< std::string > &s_in, const std::vector< std::string > &s_out, const Function::AuxOut &aux=Function::AuxOut(), const Dict &opts=Dict())
int calc_function(OracleMemory *m, const std::string &fcn, const double *const *arg=nullptr, int thread_id=0) const
std::vector< std::string > get_function() const override
Get list of dependency functions.
static void registerPlugin(const Plugin &plugin, bool needs_lock=true)
Register an integrator in the factory.
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.
General sparsity class.
Definition: sparsity.hpp:106
casadi_int nnz() const
Get the number of (structural) non-zeros.
Definition: sparsity.cpp:148
const casadi_int * row() const
Get a reference to row-vector,.
Definition: sparsity.cpp:164
const casadi_int * colind() const
Get a reference to the colindex of all column element (see class description)
Definition: sparsity.cpp:168
The casadi namespace.
Definition: archiver.cpp:28
void CASADI_NLPSOL_CONOPT_EXPORT casadi_load_nlpsol_conopt()
@ NLPSOL_X
Decision variables at the optimal solution (nx x 1)
Definition: nlpsol.hpp:217
@ NLPSOL_F
Cost function value at the optimal solution (1 x 1)
Definition: nlpsol.hpp:219
static const size_t conopt_max_option_name
int CASADI_NLPSOL_CONOPT_EXPORT casadi_register_nlpsol_conopt(Nlpsol::Plugin *plugin)
std::ostream & uerr()
bool is_range(const std::vector< casadi_int > &v, casadi_int start, casadi_int stop, casadi_int step)
Check if a vector matches a range.
Definition: casadi_misc.cpp:95
void casadi_copy(const T1 *x, casadi_int n, T1 *y)
COPY: y <-x.
std::string str(const T &v)
String representation, any type.
GenericType::Dict Dict
C++ equivalent of Python's dict or MATLAB's struct.
std::ostream & uout()
@ SOLVER_RET_NAN
@ SOLVER_RET_INFEASIBLE
@ SOLVER_RET_LIMITED
@ SOLVER_RET_SUCCESS
@ SOLVER_RET_UNKNOWN
std::atomic< bool > cache_valid_jac
std::atomic< bool > cache_valid
std::vector< std::pair< std::string, GenericType > > custom_options
std::vector< double > conopt_rhs
ConoptModelStatus modsta
std::vector< ConoptRowType > conopt_type
std::vector< int > conopt_to_casadi
std::vector< double > cached_x
ConoptMemory(const ConoptInterface &interface)
Integrator memory.
Definition: nlpsol_impl.hpp:40