1#![allow(clippy::too_many_lines, clippy::approx_constant)]
13
14use pounce_common::exception::SolverException;
15use pounce_common::reg_options::RegisteredOptions;
16
17pub fn register_all_upstream_options(r: &RegisteredOptions) -> Result<(), SolverException> {
18 r.set_registering_category("Output");
20 r.add_bounded_integer_option("print_level", "Output verbosity level.", 0, 12, 5, "Sets the default verbosity level for console output. The larger this value the more detailed is the output.")?;
21 r.add_string_option("output_file", "File name of desired output file (leave unset for no file output).", "", &[("*", "Any acceptable standard file name")], "NOTE: This option only works when read from the ipopt.opt options file! An output file with this name will be written (leave unset for no file output). The verbosity level is by default set to \"print_level\", but can be overridden with \"file_print_level\". The file name is changed to use only small letters.")?;
22 r.add_bounded_integer_option("file_print_level", "Verbosity level for output file.", 0, 12, 5, "NOTE: This option only works when read from the ipopt.opt options file! Determines the verbosity level for the file specified by \"output_file\". By default it is the same as \"print_level\".")?;
23 r.add_bool_option(
24 "file_append",
25 "Whether to append to output file, if set, instead of truncating.",
26 false,
27 "NOTE: This option only works when read from the ipopt.opt options file!",
28 )?;
29 r.add_bool_option("print_user_options", "Print all options set by the user.", false, "If selected, the algorithm will print the list of all options set by the user including their values and whether they have been used. In some cases this information might be incorrect, due to the internal program flow.")?;
30 r.add_bool_option("print_options_documentation", "Switch to print all algorithmic options with some documentation before solving the optimization problem.", false, "")?;
31 r.add_bounded_integer_option("debug_print_level", "Verbosity level for debug file.", 0, 12, 5, "This Ipopt library has been compiled in debug mode, and a file \"debug.out\" is produced for every run. This option determines the verbosity level for this file. By default it is the same as \"print_level\".")?;
32 r.add_bool_option("print_timing_statistics", "Switch to print timing statistics.", false, "If selected, the program will print the time spend for selected tasks. This implies timing_statistics=yes.")?;
33 r.set_registering_category("Miscellaneous");
34 r.add_string_option("option_file_name", "File name of options file.", "ipopt.opt", &[("*", "Any acceptable standard file name")], "By default, the name of the Ipopt options file is \"ipopt.opt\" - or something else if specified in the IpoptApplication::Initialize call. If this option is set by SetStringValue BEFORE the options file is read, it specifies the name of the options file. It does not make any sense to specify this option within the options file. Setting this option to an empty string disables reading of an options file.")?;
35 r.add_bool_option(
36 "replace_bounds",
37 "Whether all variable bounds should be replaced by inequality constraints",
38 false,
39 "This option must be set for the inexact algorithm.",
40 )?;
41 r.add_bool_option("skip_finalize_solution_call", "Whether a call to NLP::FinalizeSolution after optimization should be suppressed", false, "In some Ipopt applications, the user might want to call the FinalizeSolution method separately. Setting this option to \"yes\" will cause the IpoptApplication object to suppress the default call to that method.")?;
42 r.set_registering_category("Undocumented");
43 r.add_bool_option("suppress_all_output", "", false, "")?;
44 r.add_bool_option(
45 "inexact_algorithm",
46 "Whether to activate the version of Ipopt that allows iterative linear solvers.",
47 false,
48 "EXPERIMENTAL",
49 )?;
50 r.set_registering_category("");
51
52 r.set_registering_category("Output");
54 r.add_string_option(
55 "print_options_mode",
56 "format in which to print options documentation",
57 "text",
58 &[
59 ("text", "Ordinary text"),
60 ("latex", "LaTeX formatted"),
61 ("doxygen", "Doxygen (markdown) formatted"),
62 ],
63 "",
64 )?;
65 r.add_bool_option(
66 "print_advanced_options",
67 "whether to print also advanced options",
68 false,
69 "",
70 )?;
71
72 r.set_registering_category("NLP");
74 r.add_number_option(
75 "nlp_lower_bound_inf",
76 "any bound less or equal this value will be considered -inf (i.e. not lower bounded).",
77 -1e19,
78 "",
79 )?;
80 r.add_number_option(
81 "nlp_upper_bound_inf",
82 "any bound greater or this value will be considered +inf (i.e. not upper bounded).",
83 1e19,
84 "",
85 )?;
86 r.add_string_option("fixed_variable_treatment", "Determines how fixed variables should be handled.", "make_parameter", &[("make_parameter", "Remove fixed variable from optimization variables"), ("make_parameter_nodual", "Remove fixed variable from optimization variables and do not compute bound multipliers for fixed variables"), ("make_constraint", "Add equality constraints fixing variables"), ("relax_bounds", "Relax fixing bound constraints")], "The main difference between those options is that the starting point in the \"make_constraint\" case still has the fixed variables at their given values, whereas in the case \"make_parameter(_nodual)\" the functions are always evaluated with the fixed values for those variables. Also, for \"relax_bounds\", the fixing bound constraints are relaxed (according to\" bound_relax_factor\"). For all but \"make_parameter_nodual\", bound multipliers are computed for the fixed variables.")?;
87 r.add_bool_option("dependency_detection_with_rhs", "Indicates if the right hand sides of the constraints should be considered in addition to gradients during dependency detection", false, "")?;
89 r.add_lower_bounded_integer_option("num_linear_variables", "Number of linear variables", 0, 0, "When the Hessian is approximated, it is assumed that the first num_linear_variables variables are linear. The Hessian is then not approximated in this space. If the get_number_of_nonlinear_variables method in the TNLP is implemented, this option is ignored.")?;
90 r.add_string_option(
91 "jacobian_approximation",
92 "Specifies technique to compute constraint Jacobian",
93 "exact",
94 &[
95 ("exact", "user-provided derivatives"),
96 (
97 "finite-difference-values",
98 "user-provided structure, values by finite differences",
99 ),
100 ],
101 "",
102 )?;
103 r.add_string_option(
104 "gradient_approximation",
105 "Specifies technique to compute objective Gradient",
106 "exact",
107 &[
108 ("exact", "user-provided gradient"),
109 ("finite-difference-values", "values by finite differences"),
110 ],
111 "",
112 )?;
113 r.add_lower_bounded_number_option(
114 "findiff_perturbation",
115 "Size of the finite difference perturbation for derivative approximation.",
116 0.0,
117 true,
118 1e-7,
119 "This determines the relative perturbation of the variable entries.",
120 )?;
121 r.set_registering_category("Derivative Checker");
122 r.add_string_option("derivative_test", "Enable derivative checker", "none", &[("none", "do not perform derivative test"), ("first-order", "perform test of first derivatives at starting point"), ("second-order", "perform test of first and second derivatives at starting point"), ("only-second-order", "perform test of second derivatives at starting point")], "If this option is enabled, a (slow!) derivative test will be performed before the optimization. The test is performed at the user provided starting point and marks derivative values that seem suspicious")?;
123 r.add_lower_bounded_integer_option("derivative_test_first_index", "Index of first quantity to be checked by derivative checker", -2, -2, "If this is set to -2, then all derivatives are checked. Otherwise, for the first derivative test it specifies the first variable for which the test is done (counting starts at 0). For second derivatives, it specifies the first constraint for which the test is done; counting of constraint indices starts at 0, and -1 refers to the objective function Hessian.")?;
124 r.add_lower_bounded_number_option(
125 "derivative_test_perturbation",
126 "Size of the finite difference perturbation in derivative test.",
127 0.0,
128 true,
129 1e-8,
130 "This determines the relative perturbation of the variable entries.",
131 )?;
132 r.add_lower_bounded_number_option("derivative_test_tol", "Threshold for indicating wrong derivative.", 0.0, true, 1e-4, "If the relative deviation of the estimated derivative from the given one is larger than this value, the corresponding derivative is marked as wrong.")?;
133 r.add_bool_option(
134 "derivative_test_print_all",
135 "Indicates whether information for all estimated derivatives should be printed.",
136 false,
137 "Determines verbosity of derivative checker.",
138 )?;
139 r.add_lower_bounded_number_option("point_perturbation_radius", "Maximal perturbation of an evaluation point.", 0.0, false, 10.0, "If a random perturbation of a points is required, this number indicates the maximal perturbation. This is for example used when determining the center point at which the finite difference derivative test is executed.")?;
140 r.add_string_option("dependency_detector", "Indicates which linear solver should be used to detect linearly dependent equality constraints.", "none", &[("none", "don't check; no extra work at beginning"), ("mumps", "use MUMPS"), ("wsmp", "use WSMP"), ("ma28", "use MA28")], "This is experimental and does not work well.")?;
141
142 r.set_registering_category("Barrier Parameter Update");
144 r.add_lower_bounded_number_option("mu_max_fact", "Factor for initialization of maximum value for barrier parameter.", 0.0, true, 1e3, "This option determines the upper bound on the barrier parameter. This upper bound is computed as the average complementarity at the initial point times the value of this option. (Only used if option \"mu_strategy\" is chosen as \"adaptive\".)")?;
145 r.add_lower_bounded_number_option("mu_max", "Maximum value for barrier parameter.", 0.0, true, 1e5, "This option specifies an upper bound on the barrier parameter in the adaptive mu selection mode. If this option is set, it overwrites the effect of mu_max_fact. (Only used if option \"mu_strategy\" is chosen as \"adaptive\".)")?;
146 r.add_lower_bounded_number_option("mu_min", "Minimum value for barrier parameter.", 0.0, true, 1e-11, "This option specifies the lower bound on the barrier parameter in the adaptive mu selection mode. By default, it is set to the minimum of 1e-11 and min(\"tol\",\"compl_inf_tol\")/(\"barrier_tol_factor\"+1), which should be a reasonable value. (Only used if option \"mu_strategy\" is chosen as \"adaptive\".)")?;
147 r.set_registering_category("Undocumented");
148 r.add_lower_bounded_number_option("adaptive_mu_safeguard_factor", "", 0.0, false, 0.0, "")?;
149 r.set_registering_category("");
150 r.add_string_option("adaptive_mu_globalization", "Globalization strategy for the adaptive mu selection mode.", "obj-constr-filter", &[("kkt-error", "nonmonotone decrease of kkt-error"), ("obj-constr-filter", "2-dim filter for objective and constraint violation"), ("never-monotone-mode", "disables globalization")], "To achieve global convergence of the adaptive version, the algorithm has to switch to the monotone mode (Fiacco-McCormick approach) when convergence does not seem to appear. This option sets the criterion used to decide when to do this switch. (Only used if option \"mu_strategy\" is chosen as \"adaptive\".)")?;
151 r.add_lower_bounded_integer_option("adaptive_mu_kkterror_red_iters", "Maximum number of iterations requiring sufficient progress.", 0, 4, "For the \"kkt-error\" based globalization strategy, sufficient progress must be made for \"adaptive_mu_kkterror_red_iters\" iterations. If this number of iterations is exceeded, the globalization strategy switches to the monotone mode.")?;
152 r.add_bounded_number_option("adaptive_mu_kkterror_red_fact", "Sufficient decrease factor for \"kkt-error\" globalization strategy.", 0.0, true, 1.0, true, 0.9999, "For the \"kkt-error\" based globalization strategy, the error must decrease by this factor to be deemed sufficient decrease.")?;
153 r.add_bounded_number_option("filter_margin_fact", "Factor determining width of margin for obj-constr-filter adaptive globalization strategy.", 0.0, true, 1.0, true, 1e-5, "When using the adaptive globalization strategy, \"obj-constr-filter\", sufficient progress for a filter entry is defined as follows: (new obj) < (filter obj) - filter_margin_fact*(new constr-viol) OR (new constr-viol) < (filter constr-viol) - filter_margin_fact*(new constr-viol). For the description of the \"kkt-error-filter\" option see \"filter_max_margin\".")?;
154 r.add_lower_bounded_number_option(
155 "filter_max_margin",
156 "Maximum width of margin in obj-constr-filter adaptive globalization strategy.",
157 0.0,
158 true,
159 1.0,
160 "",
161 )?;
162 r.add_lower_bounded_integer_option("adaptive_mu_max_free_returns", "Maximum number of times the adaptive strategy may return to free-mu mode after entering fixed-mu (monotone) mode.", -1, -1, "Once the adaptive barrier strategy has switched into the monotone mode this many times, it stays there for the remainder of the solve instead of switching back to the oracle-driven free mode. This keeps the cheap, well-behaved monotone endgame while preserving the adaptive strategy's early exploration. A value of -1 places no limit, which reproduces Ipopt's behavior. (Only used if option \"mu_strategy\" is chosen as \"adaptive\".)")?;
169 r.add_bounded_number_option("adaptive_mu_budget_pin_fraction", "Fraction of the time budget after which the adaptive strategy commits to the monotone endgame.", 0.0, true, 1.0, false, 0.75, "Once this fraction of an explicitly requested \"max_cpu_time\" or \"max_wall_time\" has been spent without converging, the adaptive barrier strategy switches into the monotone (fixed-mu) mode and stays there, finishing from the current iterate rather than continuing to pay the mu oracle's extra linear solves. A value of 1 disables the mechanism. This has no effect unless a time budget was set, and none unless option \"mu_strategy\" is chosen as \"adaptive\".")?;
176 r.add_bool_option("adaptive_mu_restore_previous_iterate", "Indicates if the previous accepted iterate should be restored if the monotone mode is entered.", false, "When the globalization strategy for the adaptive barrier algorithm switches to the monotone mode, it can either start from the most recent iterate (no), or from the last iterate that was accepted (yes).")?;
177 r.add_lower_bounded_number_option("adaptive_mu_monotone_init_factor", "Determines the initial value of the barrier parameter when switching to the monotone mode.", 0.0, true, 0.8, "When the globalization strategy for the adaptive barrier algorithm switches to the monotone mode and fixed_mu_oracle is chosen as \"average_compl\", the barrier parameter is set to the current average complementarity times the value of \"adaptive_mu_monotone_init_factor\".")?;
178 r.add_string_option("adaptive_mu_kkt_norm_type", "Norm used for the KKT error in the adaptive mu globalization strategies.", "2-norm-squared", &[("1-norm", "use the 1-norm (abs sum)"), ("2-norm-squared", "use the 2-norm squared (sum of squares)"), ("max-norm", "use the infinity norm (max)"), ("2-norm", "use 2-norm")], "When computing the KKT error for the globalization strategies, the norm to be used is specified with this option. Note, this option is also used in the QualityFunctionMuOracle.")?;
179
180 r.set_registering_category("Initialization");
201 r.add_lower_bounded_number_option("bound_push", "Desired minimum absolute distance from the initial point to bound.", 0.0, true, 1e-2, "Determines how much the initial point might have to be modified in order to be sufficiently inside the bounds (together with \"bound_frac\"). (This is kappa_1 in Section 3.6 of implementation paper.)")?;
202 r.add_bounded_number_option("bound_frac", "Desired minimum relative distance from the initial point to bound.", 0.0, true, 0.5, false, 1e-2, "Determines how much the initial point might have to be modified in order to be sufficiently inside the bounds (together with \"bound_push\"). (This is kappa_2 in Section 3.6 of implementation paper.)")?;
203 r.add_lower_bounded_number_option("slack_bound_push", "Desired minimum absolute distance from the initial slack to bound.", 0.0, true, 1e-2, "Determines how much the initial slack variables might have to be modified in order to be sufficiently inside the inequality bounds (together with \"slack_bound_frac\"). (This is kappa_1 in Section 3.6 of implementation paper.)")?;
204 r.add_bounded_number_option("slack_bound_frac", "Desired minimum relative distance from the initial slack to bound.", 0.0, true, 0.5, false, 1e-2, "Determines how much the initial slack variables might have to be modified in order to be sufficiently inside the inequality bounds (together with \"slack_bound_push\"). (This is kappa_2 in Section 3.6 of implementation paper.)")?;
205 r.add_lower_bounded_number_option("constr_mult_init_max", "Maximum allowed least-square guess of constraint multipliers.", 0.0, false, 1e3, "Determines how large the initial least-square guesses of the constraint multipliers are allowed to be (in max-norm). If the guess is larger than this value, it is discarded and all constraint multipliers are set to zero. This options is also used when initializing the restoration phase. By default, \"resto.constr_mult_init_max\" (the one used in RestoIterateInitializer) is set to zero.")?;
206 r.add_lower_bounded_number_option(
207 "bound_mult_init_val",
208 "Initial value for the bound multipliers.",
209 0.0,
210 true,
211 1.0,
212 "All dual variables corresponding to bound constraints are initialized to this value.",
213 )?;
214 r.add_string_option(
215 "bound_mult_init_method",
216 "Initialization method for bound multipliers",
217 "constant",
218 &[
219 (
220 "constant",
221 "set all bound multipliers to the value of bound_mult_init_val",
222 ),
223 (
224 "mu-based",
225 "initialize to mu_init/(x_i-x_L_i) (NOT IMPLEMENTED in pounce; refused, see gh#604)",
226 ),
227 ],
228 "This option defines how the iterates for the bound multipliers are initialized. If \"constant\" is chosen, then all bound multipliers are initialized to the value of \"bound_mult_init_val\". If \"mu-based\" is chosen, the each value is initialized to the the value of \"mu_init\" divided by the corresponding slack variable. This latter option might be useful if the starting point is close to the optimal solution. pounce implements \"constant\" only; \"mu-based\" is registered so an ipopt.opt written for Ipopt still parses, and is refused with a message rather than silently served as \"constant\" (gh#604).",
229 )?;
230 r.add_string_option(
231 "least_square_init_primal",
232 "Least square initialization of the primal variables",
233 "no",
234 &[
235 ("no", "take user-provided point"),
236 (
237 "yes",
238 "overwrite user-provided point with least-square estimates",
239 ),
240 ],
241 "If set to yes, Ipopt ignores the user provided point and solves a least square problem for the primal variables (x and s) to fit the linearized equality and inequality constraints. This might be useful if the user doesn't know anything about the starting point, or for solving an LP or QP. This option is enabled by the \"mehrotra_algorithm\" cascade.",
242 )?;
243 r.add_string_option(
244 "least_square_init_duals",
245 "Least square initialization of all dual variables",
246 "no",
247 &[
248 ("no", "use bound_mult_init_val and least-square equality constraint multipliers"),
249 ("yes", "overwrite user-provided point with least-square estimates"),
250 ],
251 "If set to yes, Ipopt tries to solve a least square problem for the primal and dual variables to fit the linearized equality and inequality constraints as well as the first-order optimality conditions. NOT IMPLEMENTED in pounce: registered so an ipopt.opt written for Ipopt still parses, and refused when set to \"yes\" rather than silently ignored (gh#604). The equality multipliers are always least-square initialized (subject to constr_mult_init_max), and the bound multipliers always take bound_mult_init_val — i.e. exactly the \"no\" behaviour.",
252 )?;
253 r.set_registering_category("");
254
255 r.set_registering_category("");
257 r.set_registering_category("Linear Solver");
258 r.add_string_option(
261 "hsllib",
262 "Name of library containing HSL routines for load at runtime",
263 "libhsl.so",
264 &[("*", "Any acceptable filename (may contain path, too)")],
265 "",
266 )?;
267 r.add_string_option("pardisolib", "Name of library containing Pardiso routines (from pardiso-project.org) for load at runtime", "libpardiso.so", &[("*", "Any acceptable filename (may contain path, too)")], "")?;
268 r.set_registering_category("NLP Scaling");
269 r.set_registering_category("Barrier Parameter Update");
271 r.add_string_option(
272 "mu_strategy",
273 "Update strategy for barrier parameter.",
274 "monotone",
275 &[
276 ("monotone", "use the monotone (Fiacco-McCormick) strategy"),
277 ("adaptive", "use the adaptive update strategy"),
278 ],
279 "Determines which barrier parameter update strategy is to be used.",
280 )?;
281 r.add_string_option("mu_oracle", "Oracle for a new barrier parameter in the adaptive strategy.", "quality-function", &[("probing", "Mehrotra's probing heuristic"), ("loqo", "LOQO's centrality rule"), ("quality-function", "minimize a quality function")], "Determines how a new barrier parameter is computed in each \"free-mode\" iteration of the adaptive barrier parameter strategy. (Only considered if \"adaptive\" is selected for option \"mu_strategy\").")?;
282 r.add_string_option("fixed_mu_oracle", "Oracle for the barrier parameter when switching to fixed mode.", "average_compl", &[("probing", "Mehrotra's probing heuristic"), ("loqo", "LOQO's centrality rule"), ("quality-function", "minimize a quality function"), ("average_compl", "base on current average complementarity")], "Determines how the first value of the barrier parameter should be computed when switching to the \"monotone mode\" in the adaptive strategy. (Only considered if \"adaptive\" is selected for option \"mu_strategy\".)")?;
283 r.set_registering_category("Hessian Approximation");
284 r.add_string_option(
285 "limited_memory_aug_solver",
286 "Strategy for solving the augmented system for low-rank Hessian.",
287 "sherman-morrison",
288 &[
289 ("sherman-morrison", "use Sherman-Morrison formula"),
290 ("extended", "use an extended augmented system"),
291 ],
292 "",
293 )?;
294 r.set_registering_category("Line Search");
295 r.add_string_option("line_search_method", "Globalization method used in backtracking line search", "filter", &[("filter", "Filter method"), ("cg-penalty", "Chen-Goldfarb penalty function"), ("penalty", "Standard penalty function")], "Only the \"filter\" choice is officially supported. But sometimes, good results might be obtained with the other choices.")?;
296 r.set_registering_category("Undocumented");
297 r.add_bool_option(
298 "wsmp_iterative",
299 "Switches to use iterative instead of direct solver in WSMP.",
300 false,
301 "EXPERIMENTAL!",
302 )?;
303 r.add_string_option("linear_solver", "Linear solver used for step computations.", "feral", &[("ma27", "use the Harwell routine MA27"), ("ma57", "use the Harwell routine MA57"), ("ma77", "use the Harwell routine HSL_MA77"), ("ma86", "use the Harwell routine HSL_MA86"), ("ma97", "use the Harwell routine HSL_MA97"), ("pardiso", "use the Pardiso package from pardiso-project.org"), ("pardisomkl", "use the Pardiso package from Intel MKL"), ("spral", "use the SPRAL package"), ("wsmp", "use WSMP package"), ("mumps", "use MUMPS package"), ("custom", "use custom linear solver (expert use)"), ("feral", "use FERAL pure-Rust sparse symmetric solver (pounce extension)")], "Determines which linear algebra package is to be used for the solution of the augmented linear system (for obtaining the search directions).")?;
311
312 r.add_string_option(
318 "algorithm",
319 "Top-level optimization algorithm.",
320 "interior-point",
321 &[
322 (
323 "interior-point",
324 "primal-dual interior-point method (IPOPT-lineage; pounce default)",
325 ),
326 (
327 "active-set-sqp",
328 "active-set sequential quadratic programming via pounce-qp",
329 ),
330 ],
331 "Selects between the IPM (default) and the active-set SQP driver.",
332 )?;
333
334 r.add_string_option(
337 "solver_selection",
338 "Which solver to route the problem to.",
339 "auto",
340 &[
341 (
342 "auto",
343 "Most specialized solver matching the detected problem class.",
344 ),
345 (
346 "nlp",
347 "Always the filter-IPM NLP solver (current default behavior).",
348 ),
349 (
350 "lp-ipm",
351 "Force IPM-LP; errors if the problem is not an LP.",
352 ),
353 (
354 "qp-ipm",
355 "Force IPM-QP; errors if the problem is not LP/convex-QP.",
356 ),
357 (
358 "qp-active-set",
359 "Force active-set QP; errors if not LP/convex-QP.",
360 ),
361 (
362 "socp",
363 "Force the SOCP conic IPM; errors if not a convex LP/QP/QCQP.",
364 ),
365 ],
366 "Selects the solver by problem class. `auto` routes LP and convex QP to \
367 the specialized convex interior-point solver (pounce-convex), a convex \
368 QCQP to the SOCP conic IPM, and all other classes to the NLP filter-IPM. \
369 `qp-active-set` routes through the active-set SQP engine (pounce-qp QP \
370 subproblems) instead of the IPM; `socp` forces the conic IPM (a convex \
371 QCQP routes there under `auto`). \
372 Behavior differs by surface. Through the pounce CLI on `.nl` input all \
373 six values are honored: the CLI classifies the parsed problem, validates \
374 a forced value against the detected class (e.g. `lp-ipm` errors if the \
375 problem is not an LP), and routes the convex values \
376 (`lp-ipm` / `qp-ipm` / `socp`) to pounce-convex — which is also the only \
377 path where `qp_presolve` applies. A library (`IpoptApplication` / \
378 pounce-rs) solve has no problem-structure extraction, so it honors only \
379 `auto`, `nlp`, and `qp-active-set`; the convex-IPM values \
380 (`lp-ipm` / `qp-ipm` / `socp`) return Invalid_Option. \
381 On the library path `qp-active-set` does NOT class-validate: it simply \
382 selects the active-set SQP algorithm (a general NLP method) and runs it \
383 on whatever TNLP is given, whereas the CLI restricts it to LP / convex \
384 QP. `qp-active-set` and `algorithm=active-set-sqp` are equivalent \
385 selectors for the SQP engine, and either takes precedence over \
386 `algorithm=interior-point`: setting `solver_selection=qp-active-set` \
387 runs the SQP engine even when `algorithm` is left at (or explicitly set \
388 to) its `interior-point` default.",
389 )?;
390 r.add_string_option(
391 "qp_presolve",
392 "Run presolve before the convex LP/QP interior-point solve.",
393 "yes",
394 &[
395 ("yes", "Reduce the problem (and detect trivial infeasibility / unboundedness) before solving."),
396 ("no", "Solve the extracted problem directly, without presolve."),
397 ],
398 "Only affects the convex LP/QP path (`solver_selection` routing to \
399 pounce-convex), which runs only in the pounce CLI on `.nl` input; a \
400 library (`IpoptApplication`) solve cannot route there, and refuses a \
401 non-default value rather than accepting one it would ignore (gh#604). \
402 When on, presolve removes empty / duplicate / redundant rows, fixes and \
403 substitutes structural columns, and may report infeasible / unbounded \
404 without invoking the solver.",
405 )?;
406
407 r.add_bounded_number_option(
419 "qp_tau",
420 "Convex IPM fraction-to-boundary parameter τ ∈ (0,1).",
421 0.0,
422 true,
423 1.0,
424 true,
425 0.95,
426 "Convex LP/QP interior-point only. Caps each Newton step at a fraction \
427 τ of the distance to the cone boundary; nearer 1 is more aggressive. \
428 The floor of the adaptive rule capped by qp_tau_max, and the flat \
429 value on the predictor step and on second-order / PSD cone blocks. \
430 Default 0.95.",
431 )?;
432 r.add_bounded_number_option(
433 "qp_tau_max",
434 "Convex IPM adaptive fraction-to-boundary ceiling τ_max ∈ (0,1).",
435 0.0,
436 true,
437 1.0,
438 true,
439 1.0 - 1e-12,
440 "Convex LP/QP interior-point only. As the solve converges, the \
441 corrector's τ on nonnegative-orthant blocks follows the Mehrotra tail \
442 τ = clamp(1 − μ, qp_tau, qp_tau_max), so a near-optimal iterate can \
443 take a near-full Newton step — worth 35–60% of the iterations when \
444 warm starting a sequence of nearby QPs. Set equal to qp_tau to pin τ \
445 flat (the most conservative setting). Default 1 − 1e-12.",
446 )?;
447 r.add_bounded_integer_option(
448 "qp_gondzio_corr",
449 "Convex IPM Gondzio centrality correctors per iteration.",
450 0,
451 10,
452 3,
453 "Convex LP/QP interior-point only. Maximum Gondzio multiple centrality \
454 corrections computed after the Mehrotra corrector, on \
455 nonnegative-orthant blocks only. Each is one extra back-solve through \
456 the factorization the iteration already paid for — never a \
457 refactorization — and is kept only if it lengthens the \
458 fraction-to-boundary step, so a well-centered solve stops after the \
459 first trial. It is the standard answer to steps that are accepted but \
460 short. Set 0 to disable. Default 3.",
461 )?;
462 r.add_lower_bounded_number_option(
463 "qp_reg",
464 "Convex IPM static KKT regularization δ ≥ 0.",
465 0.0,
466 false,
467 1e-10,
468 "Convex LP/QP interior-point only. Added on the (block) diagonal to \
469 keep the reduced KKT quasi-definite for a stable LDLᵀ inertia. Too \
470 large freezes the primal residual on badly-scaled LPs; the default \
471 1e-10 is centered in the band that converges the LP/QP suites.",
472 )?;
473 r.add_lower_bounded_number_option(
474 "qp_infeas_tol",
475 "Convex IPM infeasibility-certificate value tolerance > 0.",
476 0.0,
477 true,
478 1e-7,
479 "Convex LP/QP interior-point only. Relative tolerance on the value and \
480 cone-membership parts of an infeasibility / unboundedness \
481 certificate. The certificate's defining-equation residual is held to a \
482 far tighter internal tolerance; this only governs when a status is \
483 backed by a verified proof. Default 1e-7.",
484 )?;
485 r.add_string_option(
486 "qp_hsde",
487 "Use the homogeneous self-dual embedding for the convex IPM.",
488 "yes",
489 &[
490 ("yes", "Self-dual embedding: self-starting, native certificates, robust on ill-conditioned data."),
491 ("no", "Infeasible-start primal–dual method (the warm-start / build-once substrate)."),
492 ],
493 "Convex LP/QP interior-point only. HSDE (default) self-starts and \
494 produces infeasibility / unboundedness certificates natively; it is \
495 also the substrate for non-symmetric cones. Default yes.",
496 )?;
497 r.add_string_option(
498 "qp_equilibrate",
499 "Ruiz-equilibrate the data before the direct convex IPM solve.",
500 "yes",
501 &[
502 (
503 "yes",
504 "Apply Ruiz row/column scaling before solving (direct, non-HSDE path).",
505 ),
506 ("no", "Solve the raw data without equilibration."),
507 ],
508 "Convex LP/QP interior-point only, and only when `qp_hsde=no` (the \
509 direct infeasible-start path): a conditioning aid for the raw KKT \
510 factorization. HSDE conditions internally and ignores this. Default \
511 yes.",
512 )?;
513 r.add_string_option(
514 "qp_crossover",
515 "Run LP crossover to purify the IPM iterate to an exact vertex.",
516 "no",
517 &[
518 ("yes", "After the IPM, pivot the interior iterate to an exact optimal vertex (active-set purification)."),
519 ("no", "Return the interior-point iterate directly (default)."),
520 ],
521 "Convex LP path only (pure LP, P=0); a no-op for genuine QPs. Correct \
522 (never-regress) but currently slow on the degenerate / large NETLIB \
523 LPs it targets and does not yet reach an exact `Optimal` vertex on the \
524 GEN family (issue #133), so it is off by default and offered as an \
525 opt-in for small, well-behaved LPs that want exact-vertex refinement. \
526 Default no.",
527 )?;
528
529 r.add_string_option(
532 "sqp_globalization",
533 "Globalization strategy for the active-set SQP outer loop.",
534 "filter",
535 &[
536 (
537 "filter",
538 "Fletcher-Leyffer filter line search (default; design note §4.1)",
539 ),
540 (
541 "l1-elastic",
542 "l1-merit (Han-Powell) backtracking line search",
543 ),
544 ],
545 "Selects how the SQP outer loop accepts or rejects a trial step. The filter strategy maintains a Pareto-frontier list of (constraint-violation, objective) pairs (Fletcher-Leyffer 2002); the l1-elastic strategy uses a weighted-sum merit (Han-Powell with fixed weight \"sqp_l1_penalty\"). Only consulted when \"algorithm\" is \"active-set-sqp\".",
546 )?;
547 r.add_string_option(
548 "sqp_hessian",
549 "Hessian source for the SQP QP subproblem.",
550 "exact",
551 &[
552 ("exact", "use the NLP-supplied Lagrangian Hessian"),
553 (
554 "damped-bfgs",
555 "Powell-damped BFGS rank-2 update (guaranteed PSD)",
556 ),
557 (
558 "lbfgs",
559 "limited-memory BFGS approximation (Phase 5b.1)",
560 ),
561 ],
562 "Determines which Hessian feeds the QP subproblem. \"exact\" requires the NLP's eval_h and may be indefinite (pounce-qp handles inertia control). \"damped-bfgs\" maintains a dense PSD approximation via Powell damping (Powell 1978). \"lbfgs\" uses limited-memory storage suitable for large n. Only consulted when \"algorithm\" is \"active-set-sqp\".",
563 )?;
564 r.add_lower_bounded_integer_option(
565 "sqp_max_iter",
566 "Maximum number of SQP outer iterations.",
567 0,
568 200,
569 "Outer-iteration cap for the active-set SQP driver. Only consulted when \"algorithm\" is \"active-set-sqp\".",
570 )?;
571 r.add_lower_bounded_number_option(
572 "sqp_tol",
573 "KKT stationarity tolerance (max-norm).",
574 0.0,
575 true,
576 1e-8,
577 "Maximum-norm tolerance on the SQP KKT stationarity residual. Only consulted when \"algorithm\" is \"active-set-sqp\".",
578 )?;
579 r.add_lower_bounded_number_option(
580 "sqp_constr_viol_tol",
581 "Constraint-violation tolerance (max-norm).",
582 0.0,
583 true,
584 1e-6,
585 "Maximum-norm tolerance on the SQP constraint violation. Only consulted when \"algorithm\" is \"active-set-sqp\".",
586 )?;
587 r.add_lower_bounded_number_option(
588 "sqp_dual_inf_tol",
589 "Dual-infeasibility tolerance (max-norm).",
590 0.0,
591 true,
592 1e-4,
593 "Maximum-norm tolerance on the SQP dual-infeasibility residual. Only consulted when \"algorithm\" is \"active-set-sqp\".",
594 )?;
595 r.add_lower_bounded_number_option(
596 "sqp_l1_penalty",
597 "Initial l1-merit penalty weight.",
598 0.0,
599 true,
600 1.0,
601 "Penalty weight ν for the l1-merit line search (Han-Powell). Ignored when \"sqp_globalization\" is \"filter\". Only consulted when \"algorithm\" is \"active-set-sqp\".",
602 )?;
603 r.add_lower_bounded_number_option(
604 "sqp_l1_penalty_safety",
605 "Additive safety margin in the Han-Powell ν update.",
606 0.0,
607 false,
608 0.1,
609 "Each iteration the SQP driver sets `ν ← max(ν, ‖λ_qp‖_∞ + sqp_l1_penalty_safety)`. The default 0.1 follows Nocedal-Wright §18.4. Ignored when \"sqp_globalization\" is \"filter\".",
610 )?;
611 r.add_lower_bounded_number_option(
612 "sqp_l1_penalty_max",
613 "Upper clamp on the l1-merit penalty ν.",
614 0.0,
615 true,
616 1e10,
617 "Prevents catastrophic Armijo failure if `‖λ_qp‖` spikes. Ignored when \"sqp_globalization\" is \"filter\".",
618 )?;
619 r.add_bounded_number_option(
620 "sqp_bt_reduction",
621 "Backtracking step-reduction factor for the SQP line search.",
622 0.0,
623 true,
624 1.0,
625 true,
626 0.5,
627 "Multiplicative factor applied to the trial step at each backtracking-line-search iteration. Only consulted when \"algorithm\" is \"active-set-sqp\".",
628 )?;
629 r.add_lower_bounded_number_option(
630 "sqp_bt_min_alpha",
631 "Smallest line-search step before declaring failure.",
632 0.0,
633 true,
634 1e-12,
635 "Minimum step length below which the SQP line search reports failure. Only consulted when \"algorithm\" is \"active-set-sqp\".",
636 )?;
637 r.add_bounded_integer_option(
638 "sqp_print_level",
639 "Print-level for the SQP outer loop.",
640 0,
641 12,
642 0,
643 "0 silences the SQP driver; 1 prints per-iteration summaries; 2+ enables trace output (planned). Only consulted when \"algorithm\" is \"active-set-sqp\".",
644 )?;
645 r.add_lower_bounded_integer_option(
646 "sqp_lbfgs_max_history",
647 "Maximum number of (s, y) pairs stored by the SQP L-BFGS Hessian.",
648 1,
649 6,
650 "Limited-memory BFGS keeps a circular buffer of the most-recent (s, y) curvature pairs; this option caps that buffer length. Mirrors upstream's \"limited_memory_max_history\". Only consulted when \"algorithm\" is \"active-set-sqp\" and \"sqp_hessian\" is \"lbfgs\".",
651 )?;
652 r.add_string_option(
662 "crossover",
663 "Purify the converged interior-point iterate onto an exact active set.",
664 "no",
665 &[
666 (
667 "yes",
668 "After the NLP interior-point solve converges, pivot to the active-set path to identify an exact active set.",
669 ),
670 ("no", "Return the interior-point iterate directly (default)."),
671 ],
672 "NLP interior-point path only. An interior method never places an \
673 iterate ON a constraint, so at termination \"which constraints are \
674 active\" is a tolerance inference, not an established fact; where \
675 strict complementarity fails that inference cannot be made at all. \
676 Crossover takes the converged iterate to the active-set path and \
677 returns a point at which a linearly independent set of constraints \
678 is satisfied to equality with multipliers certifying stationarity \
679 against that set. Never-regress: the crossed-over point replaces the \
680 interior one only when it is at least as good a KKT point, so this \
681 cannot turn a converged solve into a failed one. Costs about one \
682 extra iteration on a nondegenerate problem. Off by default.",
683 )?;
684 r.add_lower_bounded_integer_option(
685 "crossover_max_iter",
686 "Outer-iteration budget for the crossover fallback phase.",
687 0,
688 30,
689 "When the single EQP-equivalent step of KNITRO §7 step 3 does not \
690 reach the stopping tolerances, crossover falls back to a full \
691 active-set SQP run from the interior iterate (§7 step 4); this caps \
692 that run. 0 disables the fallback, leaving crossover as the \
693 one-step refinement. Only consulted when \"crossover\" is \"yes\".",
694 )?;
695 r.add_lower_bounded_number_option(
696 "crossover_mult_tol",
697 "Multiplier magnitude above which a row is taken active by crossover.",
698 0.0,
699 true,
700 1e-8,
701 "The dual half of the KNITRO §7 step-2 active-set tolerance test. \
702 Raising it makes the initial estimate more conservative (fewer rows \
703 guessed active), which the active-set phase can still correct by \
704 pivoting. Only consulted when \"crossover\" is \"yes\".",
705 )?;
706 r.add_lower_bounded_number_option(
707 "crossover_primal_tol",
708 "Primal distance below which a row is taken binding by crossover.",
709 0.0,
710 true,
711 1e-6,
712 "The primal half of the KNITRO §7 step-2 active-set tolerance test. \
713 Should exceed the barrier parameter at termination, since the \
714 fraction-to-boundary rule leaves an active constraint slack by \
715 O(mu). Only consulted when \"crossover\" is \"yes\".",
716 )?;
717 r.add_lower_bounded_number_option(
739 "sqp_qp_feas_tol",
740 "Active-set QP-subproblem feasibility tolerance > 0.",
741 0.0,
742 true,
743 1e-9,
744 "Active-set SQP only (solver_selection=qp-active-set). Constraint \
745 feasibility tolerance for the pounce-qp subproblem solve. \
746 Default 1e-9.",
747 )?;
748 r.add_lower_bounded_number_option(
749 "sqp_qp_opt_tol",
750 "Active-set QP-subproblem optimality (KKT) tolerance > 0.",
751 0.0,
752 true,
753 1e-9,
754 "Active-set SQP only. Optimality / KKT tolerance for the pounce-qp \
755 subproblem solve. Default 1e-9.",
756 )?;
757 r.add_lower_bounded_integer_option(
758 "sqp_qp_max_iter",
759 "Active-set QP-subproblem iteration cap.",
760 1,
761 200,
762 "Active-set SQP only. Maximum active-set pivots per QP subproblem \
763 solve. Default 200.",
764 )?;
765 r.add_lower_bounded_number_option(
766 "sqp_qp_elastic_gamma",
767 "Active-set QP-subproblem elastic-mode penalty γ > 0.",
768 0.0,
769 true,
770 1e6,
771 "Active-set SQP only. Penalty on the elastic (phase-1) slacks used \
772 to recover from an infeasible QP subproblem. Large enough that the \
773 slacks vanish at the solution of a feasible QP, small enough not to \
774 dominate the Hessian conditioning. Default 1e6.",
775 )?;
776 r.add_string_option(
777 "sqp_qp_anti_cycling",
778 "Active-set QP-subproblem anti-cycling rule.",
779 "expand",
780 &[
781 ("expand", "EXPAND tolerance-growth + Harris two-pass (Gill-Murray-Saunders-Wright 1989). Default."),
782 ("bland", "Bland's rule: slower but guaranteed finite; mainly for tests."),
783 ("none", "No anti-cycling — benchmarking only; may cycle on degenerate QPs."),
784 ],
785 "Active-set SQP only. Anti-cycling strategy for the pounce-qp \
786 subproblem ratio test. Default expand.",
787 )?;
788
789 r.add_bool_option(
790 "sqp_qp_certify_second_order",
791 "Check second-order optimality before certifying the SQP's nonconvex QP subproblem.",
792 false,
793 "SQP subproblem only (algorithm=active-set-sqp). Every `Optimal` the \
794 active-set engine returns is a *first-order* verdict — vanishing \
795 projected gradient, sign-admissible working-set multipliers — which \
796 a saddle point of an indefinite Hessian satisfies exactly. When true \
797 the engine also produces a direction `d` with `A_W d = 0` and \
798 `d'Hd < 0` before certifying, and follows it to the next blocking \
799 row (gh #848). Standalone QP solves \
800 (solver_selection=qp-active-set, pounce.qp.solve_qp) do that by \
801 *default*: there the QP is the question, so the default is yes rather \
802 than the no below. Setting this option explicitly still reaches them \
803 and turns the certification off, which is a way to get a saddle \
804 certified as Optimal — say so rather than claiming the path is \
805 unaffected (gh #872). Here the QP is a *local model*, whose \
806 second-order verdict \
807 is not the NLP's — at iteration 0 the multipliers are still zero, so \
808 HS071 started at its own solution reports negative curvature and \
809 needs five iterations instead of one. Yes fixes real wrong answers \
810 on this path (a constrained maximum reported as Solve_Succeeded) and \
811 costs that; making it the default needs Hessian modification first \
812 (gh #856). The check is skipped outright when the Hessian is known \
813 positive semidefinite, so quasi-Newton runs pay nothing either way. \
814 Default no.",
815 )?;
816
817 r.add_bool_option(
818 "sqp_qp_use_homotopy",
819 "Trace the parametric homotopy on a cold convex-QP solve.",
820 false,
821 "Cold-start path for the pounce-qp active-set engine. When true, the \
822 solve traces the section 4.2 parametric homotopy: start from the \
823 box-only relaxation (all general rows dropped, which the box fast path \
824 solves directly), then tighten the row bounds toward their targets \
825 along t in [0,1], jumping the working set at each t where a row \
826 becomes binding or an active multiplier reaches zero. The iterate is \
827 feasible for the t-problem at every point on the path, so there is no \
828 phase-1 to stall in -- which is the failure mode the conventional path \
829 hits on degenerate netlib-derived QPs. \
830 \
831 This is the algorithm the crate is named for and the one its design \
832 note assumes; it was previously unimplemented (solve_parametric was a \
833 stub). Default false while it is evaluated against the conventional \
834 path.",
835 )?;
836
837 r.add_bool_option(
838 "sqp_qp_use_schur_updates",
839 "Absorb active-set changes as Schur-complement rank-2 updates.",
840 false,
841 "Active-set SQP only. When true, the QP subproblem keeps a cached \
842 factor of the fixed-dimension K_max matrix and absorbs each \
843 working-set change as a Sherman-Morrison-Woodbury rank-2 update, \
844 refactoring only every \"sqp_qp_max_schur_updates_before_refactor\" \
845 updates (Kirches 2011; qpOASES-extended). When false each iteration \
846 assembles a fresh active-set KKT and factors it from scratch -- \
847 algorithmically identical but far more expensive, since every \
848 working-set change repeats the full symbolic analysis (fill-reducing \
849 ordering and MC64 matching), which measured 32% of runtime on \
850 Q25FV47. \
851 \
852 Default false, and that is a measured choice rather than an \
853 oversight. On Maros-Meszaros instances the update path is 28-88x \
854 faster where it works (Q25FV47: 19.7s -> 0.5s), but it is currently \
855 less robust: of the 46 instances the default path solves correctly, \
856 enabling updates breaks 9 (InternalError, TimeOut, or a wrong \
857 objective), and total wall over that set rises 107s -> 251s because \
858 of the new timeouts. Treat it as opt-in for warm-started workloads \
859 where the speedup dominates, not as a general accelerator.",
860 )?;
861 r.add_lower_bounded_integer_option(
862 "sqp_qp_max_schur_updates_before_refactor",
863 "Schur updates to absorb before refactoring from scratch.",
864 1,
865 50,
866 "Active-set SQP only, and only consulted when \
867 \"sqp_qp_use_schur_updates\" is true. The dense Schur block grows by \
868 2 per working-set change, so its solve cost grows quadratically; \
869 refactoring periodically bounds that. Default 50.",
870 )?;
871
872 r.add_string_option("linear_system_scaling", "Method for scaling the linear system.", "none", &[("none", "no scaling will be performed"), ("mc19", "use the Harwell routine MC19 (Curtis-Reid; minimizes sum of log^2 |a_ij|)"), ("ruiz", "use iterative symmetric infinity-norm equilibration (Ruiz, 2001)"), ("slack-based", "use the slack values")], "Determines the method used to compute symmetric scaling factors for the augmented system (see also the \"linear_scaling_on_demand\" option). This scaling is independent of the NLP problem scaling.")?;
873 r.add_string_option("nlp_scaling_method", "Select the technique used for scaling the NLP.", "gradient-based", &[("none", "no problem scaling will be performed"), ("user-scaling", "scaling parameters will come from the user"), ("gradient-based", "scale the problem so the maximum gradient at the starting point is nlp_scaling_max_gradient"), ("equilibration-based", "scale the problem so that first derivatives are of order 1 at random points (uses Harwell routine MC19)"), ("curvature-based", "DEVIATION FROM UPSTREAM (pounce#703): derive the scaling from the model's quadratic coefficients rather than from a derivative sample at the starting point")], "Selects the technique used for scaling the problem internally before it is solved. For user-scaling, the parameters come from the NLP. DELIBERATE DEVIATION FROM UPSTREAM IPOPT (pounce#703): the extra value \"curvature-based\" has no upstream counterpart. gradient-based samples the Jacobian once at x0, so a row written 0.5*x'Qx <= b about the origin -- which has a zero gradient there -- is left at factor 1.0 however far Q and b disagree in magnitude, at every value of nlp_scaling_max_gradient. Measured across the pounce fixture corpus that is every quadratic row in every model. curvature-based instead equilibrates one joint variable scaling D across the whole family Q_0 + sum_i lambda_i Q_i (via the lambda-independent magnitude envelope of that pencil, Ruiz-swept as an augmented matrix) and then sets each row scale to 1/max(||D Q_i D||_inf, ||D a_i||_inf, |b_i|), so a row's scale depends on the model rather than on where the modeller happened to start. It requires every row and the objective to be degree <= 2 and refuses otherwise, and it leaves the objective unscaled for the reason pounce-convex's equilibrate documents. Off by default; see dev-notes/quadratic-structure-exploitation.md section 8.")?;
874
875 r.set_registering_category("Line Search");
877 r.add_bounded_number_option("alpha_red_factor", "Fractional reduction of the trial step size in the backtracking line search.", 0.0, true, 1.0, true, 0.5, "At every step of the backtracking line search, the trial step size is reduced by this factor.")?;
878 r.add_bounded_number_option("alpha_red_factor_min", "Floor on one backtracking reduction, enabling safeguarded interpolation.", 0.0, true, 1.0, true, 0.05, "THE DEFAULT SHOWN HERE IS THE LIMITED-MEMORY ONE. Left unset, this option resolves against the Hessian mode: 0.05 under \"hessian_approximation\" = \"limited-memory\", and equal to \"alpha_red_factor\" -- i.e. interpolation off, upstream's fixed sequence -- under an exact Hessian. The registry carries one number per option, so the exact-path default cannot be shown here; see docs/src/options.md. An explicit value is honoured on both paths. The next trial step size is the minimizer of the quadratic fitted through the barrier objective's value and slope at the current iterate and its value at the rejected trial, clamped to [alpha_red_factor_min, alpha_red_factor] times the rejected step. This is a pounce addition (gh#818): upstream reduces by the fixed \"alpha_red_factor\", which needs log(1/alpha) trial points to reach a small step and dominates the cost of a solve whose Hessian model is badly scaled. Acceptance is unchanged -- only which step is tried next. Set this equal to \"alpha_red_factor\" to restore upstream's fixed geometric sequence; setting it ABOVE \"alpha_red_factor\" does the same thing rather than erroring, since the cap wins when the pair is inverted.")?;
879 r.set_registering_category("Undocumented");
880 r.add_bool_option(
881 "magic_steps",
882 "Enables magic steps.",
883 false,
884 "DOESN'T REALLY WORK YET!",
885 )?;
886 r.set_registering_category("");
887 r.add_bool_option("accept_every_trial_step", "Always accept the first trial step.", false, "Setting this option to \"yes\" essentially disables the line search and makes the algorithm take aggressive steps, without global convergence guarantees.")?;
888 r.add_lower_bounded_integer_option("accept_after_max_steps", "Accept a trial point after maximal this number of steps even if it does not satisfy line search conditions.", -1, -1, "Setting this to -1 disables this option.")?;
889 r.add_string_option(
890 "alpha_for_y",
891 "Method to determine the step size for constraint multipliers (alpha_y) .",
892 "primal",
893 &[
894 ("primal", "use primal step size"),
895 (
896 "bound-mult",
897 "use step size for the bound multipliers (good for LPs)",
898 ),
899 ("min", "use the min of primal and bound multipliers"),
900 ("max", "use the max of primal and bound multipliers"),
901 ("full", "take a full step of size one"),
902 (
903 "min-dual-infeas",
904 "choose step size minimizing new dual infeasibility",
905 ),
906 (
907 "safer-min-dual-infeas",
908 "like \"min_dual_infeas\", but safeguarded by \"min\" and \"max\"",
909 ),
910 (
911 "primal-and-full",
912 "use the primal step size, and full step if delta_x <= alpha_for_y_tol",
913 ),
914 (
915 "dual-and-full",
916 "use the dual step size, and full step if delta_x <= alpha_for_y_tol",
917 ),
918 ("acceptor", "Call LSAcceptor to get step size for y"),
919 ],
920 "",
921 )?;
922 r.add_lower_bounded_number_option("alpha_for_y_tol", "Tolerance for switching to full equality multiplier steps.", 0.0, false, 10.0, "This is only relevant if \"alpha_for_y\" is chosen \"primal-and-full\" or \"dual-and-full\". The step size for the equality constraint multipliers is taken to be one if the max-norm of the primal step is less than this tolerance.")?;
923 r.add_lower_bounded_number_option("tiny_step_tol", "Tolerance for detecting numerically insignificant steps.", 0.0, false, 10.0 * f64::EPSILON, "If the search direction in the primal variables (x and s) is, in relative terms for each component, less than this value, the algorithm accepts the full step without line search. If this happens repeatedly, the algorithm will terminate with a corresponding exit message. The default value is 10 times machine precision.")?;
924 r.add_lower_bounded_number_option("tiny_step_y_tol", "Tolerance for quitting because of numerically insignificant steps.", 0.0, false, 1e-2, "If the search direction in the primal variables (x and s) is, in relative terms for each component, repeatedly less than tiny_step_tol, and the step in the y variables is smaller than this threshold, the algorithm will terminate.")?;
925 r.add_lower_bounded_integer_option("watchdog_shortened_iter_trigger", "Number of shortened iterations that trigger the watchdog.", 0, 10, "If the number of successive iterations in which the backtracking line search did not accept the first trial point exceeds this number, the watchdog procedure is activated. Choosing \"0\" here disables the watchdog procedure.")?;
926 r.add_lower_bounded_integer_option("watchdog_trial_iter_max", "Maximum number of watchdog iterations.", 1, 3, "This option determines the number of trial iterations allowed before the watchdog procedure is aborted and the algorithm returns to the stored point.")?;
927 r.set_registering_category("Restoration Phase");
928 r.add_bool_option("expect_infeasible_problem", "Enable heuristics to quickly detect an infeasible problem.", false, "This options is meant to activate heuristics that may speed up the infeasibility determination if you expect that there is a good chance for the problem to be infeasible. In the filter line search procedure, the restoration phase is called more quickly than usually, and more reduction in the constraint violation is enforced before the restoration phase is left. If the problem is square, this option is enabled automatically.")?;
929 r.add_lower_bounded_number_option("expect_infeasible_problem_ctol", "Threshold for disabling \"expect_infeasible_problem\" option.", 0.0, false, 1e-3, "If the constraint violation becomes smaller than this threshold, the \"expect_infeasible_problem\" heuristics in the filter line search are disabled. If the problem is square, this options is set to 0.")?;
930 r.add_lower_bounded_number_option("expect_infeasible_problem_ytol", "Multiplier threshold for activating \"expect_infeasible_problem\" option.", 0.0, true, 1e8, "If the max norm of the constraint multipliers becomes larger than this value and \"expect_infeasible_problem\" is chosen, then the restoration phase is entered.")?;
931 r.add_bool_option("start_with_resto", "Whether to switch to restoration phase in first iteration.", false, "Setting this option to \"yes\" forces the algorithm to switch to the feasibility restoration phase in the first iteration. If the initial point is feasible, the algorithm will abort with a failure.")?;
932 r.add_lower_bounded_number_option("soft_resto_pderror_reduction_factor", "Required reduction in primal-dual error in the soft restoration phase.", 0.0, false, 1.0 - 1e-4, "The soft restoration phase attempts to reduce the primal-dual error with regular steps. If the damped primal-dual step (damped only to satisfy the fraction-to-the-boundary rule) is not decreasing the primal-dual error by at least this factor, then the regular restoration phase is called. Choosing \"0\" here disables the soft restoration phase.")?;
933 r.add_lower_bounded_integer_option("max_soft_resto_iters", "Maximum number of iterations performed successively in soft restoration phase.", 0, 10, "If the soft restoration phase is performed for more than so many iterations in a row, the regular restoration phase is called.")?;
934
935 r.set_registering_category("Line Search");
937 r.add_lower_bounded_number_option("theta_max_fact", "Determines upper bound for constraint violation in the filter.", 0.0, true, 1e4, "The algorithmic parameter theta_max is determined as theta_max_fact times the maximum of 1 and the constraint violation at initial point. Any point with a constraint violation larger than theta_max is unacceptable to the filter (see Eqn. (21) in the implementation paper).")?;
938 r.add_lower_bounded_number_option("theta_max_row_scale_kappa", "Multiplier on the constraint-row count used as the floor of the theta_max reference (0 = off, upstream behaviour).", 0.0, false, 0.0, "DELIBERATE DEVIATION FROM UPSTREAM IPOPT (pounce#476), OFF BY DEFAULT -- an opt-in rescue for large models that stall from a feasible start. theta_max = theta_max_fact * max(1, theta_0) (Eqn. (21)), and any trial whose constraint violation exceeds theta_max is rejected by the filter outright. The 1 in that max is dimensionally wrong, because theta is a 1-NORM OVER CONSTRAINT ROWS -- it is a SUM of m residuals, so the same numerical ceiling means a mean per-row violation of theta_max/m, which shrinks as the model grows. On a model started at a FEASIBLE point (theta_0 = 0) the max collapses entirely and the ceiling becomes the bare constant 1e4 no matter how many rows there are. Measured: robot_a (Vanderbei, m = 52013) starts feasible, so theta_max locks at 1e4 -- a mean per-row allowance of 0.19 -- while the path to the optimum passes through theta ~ 9.4e7. Every step toward the solution is refused at the gate and the solve grinds to max_iter at objective 8.173304 (3000 iterations). Setting this option to 1 floors the theta_max reference at kappa times the number of constraint rows, so the ceiling means a mean per-row violation of theta_max_fact independent of m; robot_a then reaches Optimal at 1.0431952 in 112 iterations, robot_b in 252 and robot_c in 109. Ipopt is affected identically (robot_a/b/c all hit max_iter under its defaults and all solve under theta_max_fact = 1e8), and upstream papers over the one case it noticed by hard-coding resto.theta_max_fact = 1e8 for the restoration sub-IPM (IpRestoMinC_1Nrm.cpp:91) -- the same degeneracy, since the resto NLP is also initialised feasible. WHY THIS IS NOT THE DEFAULT: raising the ceiling relaxes a global-convergence safeguard, and a model that was not blocked by it can wander instead. On the Vanderbei corpus brainpc1/3/5/7 (m = 6900, theta_0 = 1e-2) regress -- brainpc1 from Optimal in 64 iterations to divergent, objective 3.7e3 against 4.4e-04. A kappa scan showed the damage is a STEP FUNCTION, not a gradient: brainpc3 and brainpc7 land on the identical worse answer at every kappa in {0.01, 0.05, 0.2, 1.0} while robot_a improves monotonically (287, 153, 127, 112 iterations), so no single multiplier separates the two families. A static floor cannot decide this -- the question is whether a model's route to the optimum NEEDS the headroom, which the row count does not answer. Try kappa = 1 when a model with many constraint rows and a feasible or near-feasible start stalls with the line search taking tiny steps; leave it at 0 otherwise. The restoration sub-IPM always runs with kappa = 0, since upstream already covers its instance of this with theta_max_fact = 1e8.")?;
939 r.add_lower_bounded_integer_option("theta_max_adaptive_trigger", "Consecutive line searches refused entirely at the theta_max gate before the ceiling is raised (0 = off, upstream behaviour).", 0, 3, "DELIBERATE DEVIATION FROM UPSTREAM IPOPT (pounce#476, #546). theta_max = theta_max_fact * max(1, theta_0) (Eqn. (21)) is a ceiling on the constraint violation of any trial iterate, and a trial above it is refused outright before the filter's own tests run. The ceiling is a fixed number while theta is a 1-NORM OVER CONSTRAINT ROWS, so on a large model started at a feasible point (theta_0 = 0, the max collapses) it degenerates to the bare constant 1e4 no matter how many rows are being summed. robot_a (Vanderbei, m = 52013) needs to pass through theta ~ 9.4e7 to reach its optimum, so every productive step is refused at the gate and the solve grinds to max_iter at objective 8.173304. WHAT THIS OPTION DOES: it measures whether the gate is what is refusing the line search, rather than guessing from problem size. A trial refused because theta_trial > theta_max takes a distinct early return, so pounce counts those and compares against the number of trials attempted. When EVERY trial of a line search was refused at the gate, for this many CONSECUTIVE line searches, the ceiling is provably the binding constraint and is raised by theta_max_adaptive_factor, at most theta_max_adaptive_max_raises times per solve. Set to 0 to disable, which restores upstream's fixed ceiling exactly. WHY IT IS SAFE TO HAVE ON: the rule cannot fire on a model that is converging, because such a model is accepting steps and therefore not being refused at the gate. This is what the earlier static alternative (theta_max_row_scale_kappa) could not offer: flooring the reference at kappa * rows asks 'does this model have many rows?', which is only a PROXY for needing the headroom, and a kappa scan showed it is the wrong proxy -- brainpc1/3/5/7 (m = 6900) regress at every kappa tried, including 0.01, while robot_a improves monotonically, so no separating value exists. Under the adaptive rule brainpc is untouched by construction: it converges at the upstream ceiling and never trips the trigger. WHY A STREAK RATHER THAN ONE LINE SEARCH: a single overshooting Newton direction can legitimately have all of its trials refused at the gate, and backtracking is the correct response to that. Only a model that cannot get past the gate repeatedly is one whose route to the optimum needs the headroom. The restoration sub-IPM always runs with this disabled, since upstream already hard-codes resto.theta_max_fact = 1e8 (IpRestoMinC_1Nrm.cpp:91) -- its own fix for its own instance of this degeneracy.")?;
940 r.add_lower_bounded_number_option("theta_max_adaptive_factor", "Factor applied to theta_max on each adaptive raise.", 1.0, false, 100.0, "See theta_max_adaptive_trigger. The ladder is geometric so a model needing many orders of headroom reaches it in a few raises, while no single raise is large enough to discard the safeguard outright. A value of 1 or below disables raising. robot_a needs to go from 1e4 to above 9.4e7, which the default reaches in two raises.")?;
941 r.add_lower_bounded_integer_option("theta_max_adaptive_max_raises", "Maximum number of adaptive theta_max raises per solve.", 0, 4, "See theta_max_adaptive_trigger. Wachter-Biegler's global-convergence argument (Thm. 2) requires theta_max to be FINITE, not fixed, so a bounded number of bounded increases preserves it -- the ceiling still exists and a solve cannot ratchet it away one line search at a time. Set to 0 to disable raising while leaving the trigger's instrumentation in place.")?;
942 r.add_lower_bounded_number_option("theta_min_fact", "Determines constraint violation threshold in the switching rule.", 0.0, true, 1e-4, "The algorithmic parameter theta_min is determined as theta_min_fact times the maximum of 1 and the constraint violation at initial point. The switching rule treats an iteration as an h-type iteration whenever the current constraint violation is larger than theta_min (see paragraph before Eqn. (19) in the implementation paper).")?;
943 r.add_bounded_number_option(
944 "eta_phi",
945 "Relaxation factor in the Armijo condition.",
946 0.0,
947 true,
948 0.5,
949 true,
950 1e-8,
951 "See Eqn. (20) in the implementation paper.",
952 )?;
953 r.add_lower_bounded_number_option(
954 "delta",
955 "Multiplier for constraint violation in the switching rule.",
956 0.0,
957 true,
958 1.0,
959 "See Eqn. (19) in the implementation paper.",
960 )?;
961 r.add_lower_bounded_number_option(
962 "s_phi",
963 "Exponent for linear barrier function model in the switching rule.",
964 1.0,
965 true,
966 2.3,
967 "See Eqn. (19) in the implementation paper.",
968 )?;
969 r.add_lower_bounded_number_option(
970 "s_theta",
971 "Exponent for current constraint violation in the switching rule.",
972 1.0,
973 true,
974 1.1,
975 "See Eqn. (19) in the implementation paper.",
976 )?;
977 r.add_bounded_number_option(
978 "gamma_phi",
979 "Relaxation factor in the filter margin for the barrier function.",
980 0.0,
981 true,
982 1.0,
983 true,
984 1e-8,
985 "See Eqn. (18a) in the implementation paper.",
986 )?;
987 r.add_bounded_number_option(
988 "gamma_theta",
989 "Relaxation factor in the filter margin for the constraint violation.",
990 0.0,
991 true,
992 1.0,
993 true,
994 1e-5,
995 "See Eqn. (18b) in the implementation paper.",
996 )?;
997 r.add_bounded_number_option(
998 "alpha_min_frac",
999 "Safety factor for the minimal step size (before switching to restoration phase).",
1000 0.0,
1001 true,
1002 1.0,
1003 true,
1004 0.05,
1005 "This is gamma_alpha in Eqn. (23) in the implementation paper.",
1006 )?;
1007 r.add_lower_bounded_integer_option("max_soc", "Maximum number of second order correction trial steps at each iteration.", 0, 4, "Choosing 0 disables the second order corrections. This is p^{max} of Step A-5.9 of Algorithm A in the implementation paper.")?;
1008 r.add_lower_bounded_number_option("kappa_soc", "Factor in the sufficient reduction rule for second order correction.", 0.0, true, 0.99, "This option determines how much a second order correction step must reduce the constraint violation so that further correction steps are attempted. See Step A-5.9 of Algorithm A in the implementation paper.")?;
1009 r.add_lower_bounded_number_option("obj_max_inc", "Determines the upper bound on the acceptable increase of barrier objective function.", 1.0, true, 5.0, "Trial points are rejected if they lead to an increase in the barrier objective function by more than obj_max_inc orders of magnitude.")?;
1010 r.add_lower_bounded_integer_option("max_filter_resets", "Maximal allowed number of filter resets", 0, 5, "A positive number enables a heuristic that resets the filter, whenever in more than \"filter_reset_trigger\" successive iterations the last rejected trial steps size was rejected because of the filter. This option determine the maximal number of resets that are allowed to take place.")?;
1011 r.add_lower_bounded_integer_option("filter_reset_trigger", "Number of iterations that trigger the filter reset.", 1, 5, "If the filter reset heuristic is active and the number of successive iterations in which the last rejected trial step size was rejected because of the filter, the filter is reset.")?;
1012 r.add_string_option("corrector_type", "The type of corrector steps that should be taken.", "none", &[("none", "no corrector"), ("affine", "corrector step towards mu=0"), ("primal-dual", "corrector step towards current mu")], "If \"mu_strategy\" is \"adaptive\", this option determines what kind of corrector steps should be tried. Changing this option is experimental.")?;
1013 r.add_bool_option("skip_corr_if_neg_curv", "Whether to skip the corrector step in negative curvature iteration.", true, "The corrector step is not tried if negative curvature has been encountered during the computation of the search direction in the current iteration. This option is only used if \"mu_strategy\" is \"adaptive\". Changing this option is experimental.")?;
1014 r.add_bool_option("skip_corr_in_monotone_mode", "Whether to skip the corrector step during monotone barrier parameter mode.", true, "The corrector step is not tried if the algorithm is currently in the monotone mode (see also option \"barrier_strategy\"). This option is only used if \"mu_strategy\" is \"adaptive\". Changing this option is experimental.")?;
1015 r.add_lower_bounded_number_option("corrector_compl_avrg_red_fact", "Complementarity tolerance factor for accepting corrector step.", 0.0, true, 1.0, "This option determines the factor by which complementarity is allowed to increase for a corrector step to be accepted. Changing this option is experimental.")?;
1016 r.add_bounded_integer_option("soc_method", "Ways to apply second order correction", 0, 1, 0, "This option determines the way to apply second order correction, 0 is the method described in the implementation paper. 1 is the modified way which adds alpha on the rhs of x and s rows.")?;
1017
1018 r.set_registering_category("Line Search");
1020 r.add_lower_bounded_number_option(
1021 "nu_init",
1022 "Initial value of the penalty parameter.",
1023 0.0,
1024 true,
1025 1e-6,
1026 "",
1027 )?;
1028 r.add_lower_bounded_number_option(
1029 "nu_inc",
1030 "Increment of the penalty parameter.",
1031 0.0,
1032 true,
1033 1e-4,
1034 "",
1035 )?;
1036 r.add_bounded_number_option(
1037 "rho",
1038 "Value in penalty parameter update formula.",
1039 0.0,
1040 true,
1041 1.0,
1042 true,
1043 1e-1,
1044 "",
1045 )?;
1046
1047 r.set_registering_category("NLP Scaling");
1049 r.add_number_option("obj_scaling_factor", "Scaling factor for the objective function.", 1.0, "This option sets a scaling factor for the objective function. The scaling is seen internally by Ipopt but the unscaled objective is reported in the console output. If additional scaling parameters are computed (e.g. user-scaling or gradient-based), both factors are multiplied. If this value is chosen to be negative, Ipopt will maximize the objective function instead of minimizing it.")?;
1050
1051 r.set_registering_category("NLP Scaling");
1053 r.add_lower_bounded_number_option("nlp_scaling_max_gradient", "Maximum gradient after NLP scaling.", 0.0, true, 100.0, "This is the gradient scaling cut-off. If the maximum gradient is above this value, then gradient based scaling will be performed. Scaling parameters are calculated to scale the maximum gradient back to this value. (This is g_max in Section 3.8 of the implementation paper.) Note: This option is only used if \"nlp_scaling_method\" is chosen as \"gradient-based\".")?;
1054 r.add_lower_bounded_number_option("nlp_scaling_obj_target_gradient", "Target value for objective function gradient size.", 0.0, false, 0.0, "If a positive number is chosen, the scaling factor for the objective function is computed so that the gradient has the max norm of the given size at the starting point. This overrides nlp_scaling_max_gradient for the objective function.")?;
1055 r.add_lower_bounded_number_option("nlp_scaling_constr_target_gradient", "Target value for constraint function gradient size.", 0.0, false, 0.0, "If a positive number is chosen, the scaling factors for the constraint functions are computed so that the gradient has the max norm of the given size at the starting point. This overrides nlp_scaling_max_gradient for the constraint functions.")?;
1056 r.add_lower_bounded_number_option("nlp_scaling_min_value", "Minimum value of gradient-based scaling values.", 0.0, false, 1e-8, "This is the lower bound for the scaling factors computed by gradient-based scaling method. If some derivatives of some functions are huge, the scaling factors will otherwise become very small, and the (unscaled) final constraint violation, for example, might then be significant. Note: This option is only used if \"nlp_scaling_method\" is chosen as \"gradient-based\".")?;
1057
1058 r.set_registering_category("");
1060 r.set_registering_category("Line Search");
1061 r.add_lower_bounded_number_option("kappa_sigma", "Factor limiting the deviation of dual variables from primal estimates.", 0.0, true, 1e10, "If the dual variables deviate from their primal estimates, a correction is performed. See Eqn. (16) in the implementation paper. Setting the value to less than 1 disables the correction.")?;
1062 r.add_string_option("recalc_y", "Tells the algorithm to recalculate the equality and inequality multipliers as least square estimates.", "no", &[("no", "use the Newton step to update the multipliers"), ("yes", "use least-square multiplier estimates")], "This asks the algorithm to recompute the multipliers, whenever the current infeasibility is less than recalc_y_feas_tol. Choosing yes might be helpful in the quasi-Newton option. However, each recalculation requires an extra factorization of the linear system. If a limited memory quasi-Newton option is chosen, this is used by default.")?;
1063 r.add_lower_bounded_number_option("recalc_y_feas_tol", "Feasibility threshold for recomputation of multipliers.", 0.0, true, 1e-6, "If recalc_y is chosen and the current infeasibility is less than this value, then the multipliers are recomputed.")?;
1064 r.set_registering_category("Step Calculation");
1065 r.add_bool_option("mehrotra_algorithm", "Indicates whether to do Mehrotra's predictor-corrector algorithm.", false, "If enabled, line search is disabled and the (unglobalized) adaptive mu strategy is chosen with the \"probing\" oracle, and \"corrector_type=affine\" is used without any safeguards; you should not set any of those options explicitly in addition. Also, unless otherwise specified, the values of \"bound_push\", \"bound_frac\", and \"bound_mult_init_val\" are set more aggressive, and sets \"alpha_for_y=bound-mult\". The Mehrotra's predictor-corrector algorithm works usually very well for LPs and convex QPs.")?;
1066 r.set_registering_category("Undocumented");
1067 r.add_bool_option(
1068 "sb",
1069 "whether to skip printing Ipopt copyright banner",
1070 false,
1071 "",
1072 )?;
1073 r.set_registering_category("Miscellaneous");
1074 r.add_bool_option(
1075 "timing_statistics",
1076 "Indicates whether to measure time spend in components of Ipopt and NLP evaluation",
1077 false,
1078 "The overall algorithm time is unaffected by this option.",
1079 )?;
1080
1081 r.set_registering_category("");
1083 r.set_registering_category("Termination");
1084 r.add_lower_bounded_number_option("tol", "Desired convergence tolerance (relative).", 0.0, true, 1e-8, "Determines the convergence tolerance for the algorithm. The algorithm terminates successfully, if the (scaled) NLP error becomes smaller than this value, and if the (absolute) criteria according to \"dual_inf_tol\", \"constr_viol_tol\", and \"compl_inf_tol\" are met. This is epsilon_tol in Eqn. (6) in implementation paper. See also \"acceptable_tol\" as a second termination criterion. Note, some other algorithmic features also use this quantity to determine thresholds etc.")?;
1085
1086 r.set_registering_category("");
1088 r.set_registering_category("Termination");
1089 r.add_lower_bounded_number_option(
1090 "s_max",
1091 "Scaling threshold for the NLP error.",
1092 0.0,
1093 true,
1094 100.0,
1095 "See paragraph after Eqn. (6) in the implementation paper.",
1096 )?;
1097 r.set_registering_category("NLP");
1098 r.add_lower_bounded_number_option(
1099 "kappa_d",
1100 "Weight for linear damping term (to handle one-sided bounds).",
1101 0.0,
1102 false,
1103 1e-5,
1104 "See Section 3.7 in implementation paper.",
1105 )?;
1106 r.set_registering_category("Line Search");
1107 r.add_lower_bounded_number_option("slack_move", "Correction size for very small slacks.", 0.0, false, (f64::EPSILON).powf(0.75), "Due to numerical issues or the lack of an interior, the slack variables might become very small. If a slack becomes very small compared to machine precision, the corresponding bound is moved slightly. This parameter determines how large the move should be. Its default value is mach_eps^{3/4}. See also end of Section 3.5 in implementation paper - but actual implementation might be somewhat different.")?;
1108 r.add_string_option("constraint_violation_norm_type", "Norm to be used for the constraint violation in the line search.", "1-norm", &[("1-norm", "use the 1-norm"), ("2-norm", "use the 2-norm"), ("max-norm", "use the infinity norm")], "Determines which norm should be used when the algorithm computes the constraint violation in the line search.")?;
1109
1110 r.set_registering_category("Hessian Approximation");
1112 r.add_lower_bounded_integer_option("limited_memory_max_history", "Maximum size of the history for the limited quasi-Newton Hessian approximation.", 0, 6, "This option determines the number of most recent iterations that are taken into account for the limited-memory quasi-Newton approximation.")?;
1113 r.add_string_option(
1114 "limited_memory_update_type",
1115 "Quasi-Newton update formula for the limited memory quasi-Newton approximation.",
1116 "bfgs",
1117 &[
1118 ("bfgs", "BFGS update (with skipping)"),
1119 ("sr1", "SR1 (not working well)"),
1120 ],
1121 "",
1122 )?;
1123 r.add_string_option("limited_memory_initialization", "Initialization strategy for the limited memory quasi-Newton approximation.", "scalar1", &[("scalar1", "sigma = s^Ty/s^Ts"), ("scalar2", "sigma = y^Ty/s^Ty"), ("scalar3", "arithmetic average of scalar1 and scalar2"), ("scalar4", "geometric average of scalar1 and scalar2"), ("constant", "sigma = limited_memory_init_val"), ("history-max", "sigma = max over the stored curvature pairs of s^Ty/s^Ts")], "Determines how the diagonal Matrix B_0 as the first term in the limited memory approximation should be computed. The four scalar* values and constant are upstream Ipopt's, and read the newest curvature pair only. history-max is pounce's (gh#818): it applies the scalar1 formula to every pair in the history window and keeps the largest, so B_0 does not understate the curvature of the directions the rank-2 corrections say nothing about.")?;
1124 r.add_lower_bounded_number_option("limited_memory_init_val", "Value for B0 in low-rank update.", 0.0, true, 1.0, "The starting matrix in the low rank update, B0, is chosen to be this multiple of the identity in the first iteration (when no updates have been performed yet), and is constantly chosen as this value, if \"limited_memory_initialization\" is \"constant\".")?;
1125 r.add_lower_bounded_number_option("limited_memory_init_val_max", "Upper bound on value for B0 in low-rank update.", 0.0, true, 1e8, "The starting matrix in the low rank update, B0, is chosen to be this multiple of the identity in the first iteration (when no updates have been performed yet), and is constantly chosen as this value, if \"limited_memory_initialization\" is \"constant\".")?;
1126 r.add_lower_bounded_number_option("limited_memory_init_val_min", "Lower bound on value for B0 in low-rank update.", 0.0, true, 1e-8, "The starting matrix in the low rank update, B0, is chosen to be this multiple of the identity in the first iteration (when no updates have been performed yet), and is constantly chosen as this value, if \"limited_memory_initialization\" is \"constant\".")?;
1127 r.add_lower_bounded_integer_option("limited_memory_max_skipping", "Threshold for successive iterations where update is skipped.", 1, 2, "If the update is skipped more than this number of successive iterations, the quasi-Newton approximation is reset.")?;
1128 r.add_bool_option("limited_memory_special_for_resto", "Determines if the quasi-Newton updates should be special during the restoration phase.", false, "Until Nov 2010, Ipopt used a special update during the restoration phase, but it turned out that this does not work well. The new default uses the regular update procedure and it improves results. If for some reason you want to get back to the original update, set this option to \"yes\".")?;
1129
1130 r.set_registering_category("Barrier Parameter Update");
1132 r.add_lower_bounded_number_option("mu_init", "Initial value for the barrier parameter.", 0.0, true, 0.1, "This option determines the initial value for the barrier parameter (mu). It is only relevant in the monotone, Fiacco-McCormick version of the algorithm. (i.e., if \"mu_strategy\" is chosen as \"monotone\")")?;
1133 r.add_lower_bounded_number_option("barrier_tol_factor", "Factor for mu in barrier stop test.", 0.0, true, 10.0, "The convergence tolerance for each barrier problem in the monotone mode is the value of the barrier parameter times \"barrier_tol_factor\". This option is also used in the adaptive mu strategy during the monotone mode. This is kappa_epsilon in implementation paper.")?;
1134 r.add_bounded_number_option("mu_linear_decrease_factor", "Determines linear decrease rate of barrier parameter.", 0.0, true, 1.0, true, 0.2, "For the Fiacco-McCormick update procedure the new barrier parameter mu is obtained by taking the minimum of mu*\"mu_linear_decrease_factor\" and mu^\"superlinear_decrease_power\". This is kappa_mu in implementation paper. This option is also used in the adaptive mu strategy during the monotone mode.")?;
1135 r.add_bounded_number_option("mu_superlinear_decrease_power", "Determines superlinear decrease rate of barrier parameter.", 1.0, true, 2.0, true, 1.5, "For the Fiacco-McCormick update procedure the new barrier parameter mu is obtained by taking the minimum of mu*\"mu_linear_decrease_factor\" and mu^\"superlinear_decrease_power\". This is theta_mu in implementation paper. This option is also used in the adaptive mu strategy during the monotone mode.")?;
1136 r.add_string_option("mu_allow_fast_monotone_decrease", "Allow skipping of barrier problem if barrier test is already met.", "yes", &[("no", "Take at least one iteration per barrier problem even if the barrier test is already met for the updated barrier parameter"), ("yes", "Allow fast decrease of mu if barrier test it met")], "")?;
1137 r.add_bounded_number_option("tau_min", "Lower bound on fraction-to-the-boundary parameter tau.", 0.0, true, 1.0, true, 0.99, "This is tau_min in the implementation paper. This option is also used in the adaptive mu strategy during the monotone mode.")?;
1138
1139 r.set_registering_category("Termination");
1141 r.add_lower_bounded_integer_option(
1142 "max_iter",
1143 "Maximum number of iterations.",
1144 0,
1145 3000,
1146 "The algorithm terminates with a message if the number of iterations exceeded this number.",
1147 )?;
1148 r.add_lower_bounded_number_option("max_wall_time", "Maximum number of walltime clock seconds.", 0.0, true, 1e20, "A limit on walltime clock seconds that Ipopt can use to solve one problem. If during the convergence check this limit is exceeded, Ipopt will terminate with a corresponding message.")?;
1149 r.add_lower_bounded_number_option("max_cpu_time", "Maximum number of CPU seconds.", 0.0, true, 1e20, "A limit on CPU seconds that Ipopt can use to solve one problem. If during the convergence check this limit is exceeded, Ipopt will terminate with a corresponding message.")?;
1150 r.add_lower_bounded_number_option("dual_inf_tol", "Desired threshold for the dual infeasibility.", 0.0, true, 1.0, "Absolute tolerance on the dual infeasibility. Successful termination requires that the max-norm of the (unscaled) dual infeasibility is less than this threshold.")?;
1151 r.add_lower_bounded_number_option("constr_viol_tol", "Desired threshold for the constraint and variable bound violation.", 0.0, true, 1e-4, "Absolute tolerance on the constraint and variable bound violation. Successful termination requires that the max-norm of the (unscaled) constraint violation is less than this threshold. If option bound_relax_factor is not zero 0, then Ipopt relaxes given variable bounds. The value of constr_viol_tol is used to restrict the absolute amount of this bound relaxation. ")?;
1152 r.add_lower_bounded_number_option("compl_inf_tol", "Desired threshold for the complementarity conditions.", 0.0, true, 1e-4, "Absolute tolerance on the complementarity. Successful termination requires that the max-norm of the (unscaled) complementarity is less than this threshold.")?;
1153 r.add_lower_bounded_number_option("acceptable_tol", "\"Acceptable\" convergence tolerance (relative).", 0.0, true, 1e-6, "Determines which (scaled) overall optimality error is considered to be \"acceptable\". There are two levels of termination criteria. If the usual \"desired\" tolerances (see tol, dual_inf_tol etc) are satisfied at an iteration, the algorithm immediately terminates with a success message. On the other hand, if the algorithm encounters \"acceptable_iter\" many iterations in a row that are considered \"acceptable\", it will terminate before the desired convergence tolerance is met. This is useful in cases where the algorithm might not be able to achieve the \"desired\" level of accuracy.")?;
1154 r.add_lower_bounded_integer_option("acceptable_iter", "Number of \"acceptable\" iterates before triggering termination.", 0, 15, "If the algorithm encounters this many successive \"acceptable\" iterates (see \"acceptable_tol\"), it terminates, assuming that the problem has been solved to best possible accuracy given round-off. If it is set to zero, this heuristic is disabled.")?;
1155 r.add_lower_bounded_number_option("acceptable_dual_inf_tol", "\"Acceptance\" threshold for the dual infeasibility.", 0.0, true, 1e10, "Absolute tolerance on the dual infeasibility. \"Acceptable\" termination requires that the (max-norm of the unscaled) dual infeasibility is less than this threshold; see also acceptable_tol.")?;
1156 r.add_lower_bounded_number_option("acceptable_constr_viol_tol", "\"Acceptance\" threshold for the constraint violation.", 0.0, true, 1e-2, "Absolute tolerance on the constraint violation. \"Acceptable\" termination requires that the max-norm of the (unscaled) constraint violation is less than this threshold; see also acceptable_tol.")?;
1157 r.add_lower_bounded_number_option("acceptable_compl_inf_tol", "\"Acceptance\" threshold for the complementarity conditions.", 0.0, true, 1e-2, "Absolute tolerance on the complementarity. \"Acceptable\" termination requires that the max-norm of the (unscaled) complementarity is less than this threshold; see also acceptable_tol.")?;
1158 r.add_lower_bounded_number_option("acceptable_obj_change_tol", "\"Acceptance\" stopping criterion based on objective function change.", 0.0, false, 1e20, "If the relative change of the objective function (scaled by Max(1,|f(x)|)) is less than this value, this part of the acceptable tolerance termination is satisfied; see also acceptable_tol. This is useful for the quasi-Newton option, which has trouble to bring down the dual infeasibility.")?;
1159 r.add_lower_bounded_number_option("diverging_iterates_tol", "Threshold for maximal value of primal iterates.", 0.0, true, 1e20, "If any component of the primal iterates exceeded this value (in absolute terms), the optimization is aborted with the exit message that the iterates seem to be diverging.")?;
1160 r.add_lower_bounded_integer_option("dual_diverging_streak", "Consecutive growing-dual-infeasibility iterations before the dual-divergence guard fires (0, the default, disables it).", 0, 0, "pounce addition (pounce#246), **off by default**. When the NLP-scaled dual infeasibility grows for this many consecutive iterations in an elevated regime and is large in absolute terms, the outer routes to the restoration phase. It was added to bound a reported bad-warm-start grind on emfl050, but that justification did not hold up: the reported measurement was caller-side JAX compilation, and on the build predating the guard both emfl050 instances solve to the same optimum in the same time (see pounce#246 / pounce#250). Its measured effect over 1284 MINLPLib models is four changed outcomes, and the response is knife-edge and non-monotone in this threshold -- deb7/deb9 reach a better local optimum only at exactly 15, while pooling_rt2stp turns from Solve_Succeeded into Maximum_Iterations_Exceeded only at 10 and 15. That is basin luck on nonconvex problems, not a property, so it is not something to impose by default: the upside is a better local optimum on an already-solved problem, while the downside is a clean solve becoming a failure. Set it to a positive value (15 was the former default) to opt in; when enabled, a diversion that ends worse than a point the solve already had is undone (pounce#250 follow-up).")?;
1161 r.add_lower_bounded_integer_option("resto_decline_deferrals", "How many times the acceptable-point restoration decline may be deferred while the solve is still converging (0 disables the deferral).", 0, 1, "pounce addition (gh #534). When the line search fails at a point that already passes the acceptable-level tolerances, POUNCE declines to enter restoration and reports that point (upstream `IpBacktrackingLineSearch.cpp`'s ACCEPTABLE_POINT_REACHED). That guard reads the point and nothing about the trajectory that reached it, so it stops a contracting endgame and a dead stall with equal confidence -- on CUTE `eigena2` it fires while the dual infeasibility is quartering every iteration on unit steps, three iterations short of a strict certificate. With this at a positive value the decline is deferred, at most this many times per solve, when the overall NLP error has contracted by at least a factor of two on each of the last three iterations; the solve continues for up to ten more iterations, and if no strict certificate arrives the point the guard would have returned is restored and reported. The deferral is therefore bounded in cost and cannot return a worse answer than declining immediately would have. Set to 0 for the pre-#534 behaviour.")?;
1162 r.add_lower_bounded_number_option("resto_decline_progress_ratio", "Per-iteration contraction of the overall NLP error required before a restoration decline is deferred.", 0.0, true, 0.5, "pounce addition (gh #534), companion to `resto_decline_deferrals`. The decline is deferred only when each of the last three outer iterations cut the overall NLP error to at most this fraction of the previous one. The default 0.5 separates the cases the issue measured: `eigena2` quarters every iteration (ratio 0.249) and is deferred, while `eigenb2` (1.88e-7, 2.69e-7, 2.89e-7, 2.93e-7) and `csfi2` (8.468e-8 -> 8.524e-8 on its last step) are flat or rising and are declined exactly as before. Setting it to 1 admits any non-increasing window, and setting it very large (say 1e20) drops the progress requirement entirely, so the decline is deferred on every acceptable entry point until `resto_decline_deferrals` is spent -- the \"bypass the guard and see how far the solve gets\" experiment, which is otherwise only reachable by patching the source. The floor is held either way, so even the bypass cannot return a worse point than declining would have.")?;
1163 r.add_lower_bounded_integer_option("neg_curv_escapes", "How many times a certified stationary point with an indefinite reduced Hessian may be escaped along a direction of negative curvature (0 disables).", 0, 1, "pounce addition (gh #797). The filter line-search interior-point method certifies FIRST-ORDER stationarity, and on a nonconvex model that is strictly weaker than a local minimum: at a point where the reduced Hessian on null(A) is negative definite every KKT residual is zero, so the convergence test has nothing to object to and the point reported as Solve_Succeeded is a constrained MAXIMUM. The reported case is the CLI fixture nonconvex_qp.nl -- min x0*x1 s.t. x0+x1 = 2, 0 <= x <= 4, whose restriction to the feasible segment is the concave x0*(2-x0), maximized at (1,1) with f = 1 and minimized at the endpoints (0,2) and (2,0) with f = 0. From the bound-pushed start (0.01, 0.01) the first Newton step lands exactly on (1,1) and the solve stops there at obj = 1. Inertia correction does not prevent this and never could: delta_x*I is symmetric, so on a model and an iterate that are symmetric under x0 <-> x1 it cannot break the symmetry, and a symmetric correction applied to a zero gradient gives a zero step however indefinite the reduced Hessian is. Regularization makes the STEP well-posed; nothing in the algorithm asks whether the point converged to is a minimum. With this at a positive value, a certified stationary point is first tested for second-order necessity -- one extra factorization of the augmented system with the inertia check on and no perturbation, whose correct inertia is exactly the statement that W + Sigma is positive definite on null(A) -- and only a wrong inertia costs anything more. When the inertia IS wrong, delta_x is escalated until it is right and a few inverse-iteration back-solves against that factor recover the most-negative-curvature direction, which is then MEASURED rather than trusted; the solve steps along it (fraction-to-the-boundary capped, backtracked against the second-order decrease model, refused outright if it raises the constraint violation past constr_viol_tol) and continues for up to 30 more iterations. The stationary point is snapshotted first and is restored and reported unless the continuation comes back with a certificate of its own at a better point, so the escape is bounded in cost and CANNOT return a worse answer than reporting the stationary point immediately would have -- the same floor-and-deadline accounting as resto_decline_deferrals. Above 1 the floor keeps the BEST certificate the escapes have left rather than the most recent one (gh #805), so that guarantee holds at any value; each escape still buys its own continuation 30 iterations, so the cost scales with this option and the guarantee does not. On nonconvex_qp.nl the escape turns Solve_Succeeded at obj = 1 into Solve_Succeeded at obj = 0. Set to 0 for the pre-#797 behaviour: report the first-order certificate whatever its curvature. NOTE that this is a local method either way -- an escape finds a point that is second-order suspect and leaves it, it does not certify global optimality, and a stationary point whose reduced Hessian is positive definite is never touched.")?;
1164 r.add_lower_bounded_integer_option("limited_memory_ls_failure_restarts", "How many times a line-search failure at an already-feasible point may re-anchor the limited-memory Hessian and retry, instead of entering the restoration phase (0, the default, disables).", 0, 0, "pounce addition (gh #818). When the backtracking line search cannot accept any trial step, either the POINT is bad -- infeasible, and the restoration phase is exactly the right tool -- or the DIRECTION is, because W is a quasi-Newton model carrying curvature the iterate has left behind. Upstream has one answer for both, because restoration is the only fallback it has. At an already-feasible point that answer is a no-op: the restoration NLP minimizes the constraint violation and there is none to minimize, so it wanders at theta ~ 1e-13 and reports Restoration_Failed. Measured on the deb7 fixture under limited-memory, the solve stalls at inf_pr ~ 1e-12 with inf_du ~ 1e5, enters restoration at a point feasible to 8e-13, and spends 340 of its 1242 iterations there before failing; on an unconstrained model theta is identically zero, so restoration cannot move at all and the solve can die at iteration 1 with Error_In_Step_Computation and the objective still at its starting value. With this at a positive value, such a failure first drops every curvature pair but the newest -- keeping sigma a real Rayleigh quotient and the secant condition on the step just taken, while discarding the older corrections that made the direction unusable, which is what L-BFGS-B does on a line-search failure (col = 0 in mainlb) -- and retries the iterate. It is a rung and not a refusal: it fires only where restoration has nothing to reduce, it runs AFTER the acceptable-point decline so a reportable point is still reported, and every path that reached restoration before still reaches it once the rung is spent. The bound is structural as well as counted, since the re-anchor gives up once the history is down to one pair, so a second failure at the same iterate falls straight through. Has no effect under an exact Hessian, which has no curvature history to re-anchor. DEFAULTS TO 0, i.e. off: a line-search failure always enters restoration, as it does upstream. The rung is NOT what fixes gh #818 -- the backtracking interpolation is, and every gh #818 regression test passes with the rung compiled out. It defaulted to 1 in the first draft of that work, on a measurement taken before the interpolation was gated to fire only after the fixed sequence has already spent six trials; re-measured on top of that gate the rung takes `pooling_rt2stp` from 716 iterations to 744 and `infeasible_square_scaled_1em4` from 24 to 26 -- both of which are unmoved from `main` with it off -- and changes `deb7`'s verdict from Error_In_Step_Computation at 1010 to Restoration_Failed at 460, which is a different answer rather than a faster one. It is not all cost: at this gate `eigena2` goes from Error_In_Step_Computation at 201 to Solved_To_Acceptable_Level at 174, and `issue_508_infeasible_gap_1em4` from 79 to 76 at the same certificate. Off is the configuration that leaves every fixture where `main` has it; the case for turning it on has improved and needs its own corpus sweep to settle. It is kept because the reasoning is sound and unaddressed elsewhere (a restoration phase entered at a feasible point has no constraint violation to minimize), and some model will want it. Note that setting this at ANY value, 0 included, is not the same as leaving it alone -- it is a `TERMINATION_POLICY_OPTIONS` key, so setting it tells POUNCE the caller has an opinion about when the solve stops, which opts out of the automatic `Solved_To_Acceptable_Level` re-solve (pounce#748). The `pooling_rt2stp` fixture used to be the worked example of this (`Solve_Succeeded` in 295 iterations by default against `Solved_To_Acceptable_Level` in 362 with this set explicitly to 0); it no longer demonstrates the coupling, because that fixture's limited-memory leg now ends in Error_In_Step_Computation at 716 on `main` and so never reaches the acceptable-level ladder at all. The coupling itself is unchanged -- it is a property of the key, not of any fixture. `resto_decline_deferrals` and `neg_curv_escapes` carry the same coupling.")?;
1165 r.add_lower_bounded_number_option("mu_target", "Desired value of complementarity.", 0.0, false, 0.0, "Usually, the barrier parameter is driven to zero and the termination test for complementarity is measured with respect to zero complementarity. However, in some cases it might be desired to have Ipopt solve barrier problem for strictly positive value of the barrier parameter. In this case, the value of \"mu_target\" specifies the final value of the barrier parameter, and the termination tests are then defined with respect to the barrier problem for this value of the barrier parameter.")?;
1166 r.add_lower_bounded_number_option("infeas_stationarity_tol", "Stationarity tolerance for rapid infeasibility detection.", 0.0, false, 1e-8, "The main loop terminates with local infeasibility when the scaled infeasibility stationarity norm(J^T c) / max(1, norm(c)) stays at or below this value, while the constraint violation stays bounded away from zero (see infeas_viol_kappa), for infeas_max_streak successive iterations. Setting this to 0 disables rapid infeasibility detection.")?;
1169 r.add_lower_bounded_number_option("infeas_viol_kappa", "Constraint-violation margin for rapid infeasibility detection.", 0.0, false, 1e2, "An iterate only counts toward the infeasibility streak when its max-norm constraint violation exceeds this multiple of constr_viol_tol, or 1e-2, whichever is larger. Keeps rapid infeasibility detection from firing on nearly-feasible flat spots. The 1e-2 floor (pounce#519) is what keeps that promise when constr_viol_tol is tightened: without it the margin slides with the feasibility tolerance, so asking for stricter feasibility (constr_viol_tol = 1e-6) would let violations as small as 1e-4 count as bounded away from feasible and make the solver MORE likely to report local infeasibility. Raising this option raises the floor as usual; to switch the detector off entirely, set infeas_stationarity_tol or infeas_max_streak to 0.")?;
1170 r.add_lower_bounded_integer_option("infeas_max_streak", "Successive infeasible-stationary iterations before declaring local infeasibility.", 0, 5, "Number of consecutive iterations that must satisfy the rapid infeasibility detection test before the main loop terminates with local infeasibility. Setting this to 0 disables rapid infeasibility detection.")?;
1171 r.add_lower_bounded_number_option("primal_noise_floor_kappa", "Safety factor on the per-row floor below which a constraint residual is treated as floating-point noise by the strict convergence test and the infeasibility verdict.", 0.0, false, 64.0, "DELIBERATE DEVIATION FROM UPSTREAM IPOPT (pounce#528). The primal term of the KKT error is the one term Ipopt leaves as a bare absolute residual (the dual and complementarity terms carry s_d / s_c). But c_i = g_i(x) - b_i and d_i - s_i are each a difference of quantities the row's own size, so they are quantised in units of eps times that magnitude: no iterate can place either residual strictly between 0 and one ulp of the row. Once that quantum exceeds tol -- constraint values past ~4.5e7 at the 1e-8 default -- nlp_err <= tol stops being a statement about the iterate and becomes a bet on the residual landing on a bitwise-exact 0 rather than on one ulp. Feasible, bounded LPs then exited Search_Direction_Becomes_Too_Small holding the correct optimum. The STRICT gate therefore counts a row's residual only where it exceeds max(placement floor, this kappa times eps times the row's magnitude). pounce#590 extended the same floor to the per-component constr_viol test and to the absolute arm of rapid infeasibility detection, in both cases only when NO row rises above its own floor -- one resolvable row anywhere and the raw comparison stands. That case is exactly a point feasible to the limit the model's arithmetic can express: LyoPRONTO's Landau-coordinate lyophilisation OCP has conduction rows near 1e8, so one ulp is ~1e-2, and a solve at a scaled KKT error of 4.3e-10 was reported Infeasible_Problem_Detected on a violation that was pure quantisation. The scale-relative veto still sees the raw relative violation, and the acceptable-level band keeps the raw error. Verdicts are flat across kappa from 8 to 1024 on the measured set. Set to 0 to disable the floor and restore bit-for-bit upstream behaviour.")?;
1172 r.add_lower_bounded_number_option("acceptable_progress_kappa", "Fraction of acceptable_tol the error and objective may drift across the acceptable-level streak and still count as settled.", 0.0, false, 1e-1, "DELIBERATE DEVIATION FROM UPSTREAM IPOPT (pounce#533). Acceptable-level termination fires after acceptable_iter consecutive iterates under acceptable_tol, with no test for whether the solve is still moving -- so it can stop at a point that is near-stationary for the current BARRIER SUBPROBLEM while the NLP solve is still descending, returning a worse answer under a weaker status than continuing would have reached. Measured: kissing (Vanderbei) stopped at iteration 103 with objective 1.00000108 and Solved_To_Acceptable_Level, where continuing reaches 0.84544259 with a strict certificate at 550 -- 18% high, against Ipopt's 0.845442591227744; NARX_CFy (Mittelmann) stopped at 565 with both residuals near 1e-7, where 60 more iterations (25 s, inside the benchmark's 300 s limit) collapse them by five orders and beat both its own answer and Ipopt's. POUNCE therefore also requires the streak to have FLATTENED: across the acceptable_iter iterates that made it, the spread (max - min) of the KKT error must be within this fraction of acceptable_tol, and the spread of the objective within the same fraction of acceptable_tol times max(1, |f|). Spread rather than trend, because kissing's error was an order of magnitude WORSE at the iterate it stopped on than at one it had already reached inside the same streak -- it was wandering across the band, not converging inside it. Either signal alone is enough to keep solving, because kissing's objective was flat to all eight printed figures over those iterates while the continued run moved it by 15%. As with obj_scale_certificate_threshold this is a bet that is TESTED, not predicted: a refused termination is recorded, and if the continued run fails to do better it ends at exactly that point under exactly that status, so the outcome is never worse than without the mechanism -- the cost is bounded extra iterations. Set to 0 to disable the progress test and restore bit-for-bit upstream behaviour.")?;
1173 r.add_lower_bounded_number_option("dual_inf_scale_kappa", "Safety factor on the scale-relative floor under dual_inf_tol used by the strict convergence test.", 0.0, false, 1.0, "DELIBERATE DEVIATION FROM UPSTREAM IPOPT (pounce#532). dual_inf_tol is a bare ABSOLUTE bound on a quantity the aggregate KKT error NORMALISES: that aggregate's dual term is norm(grad L) / s_d, and s_d grows with the mean magnitude of the multipliers. On Vanderbei's orthrds2, s_d reaches 1.6e10 with norm(grad L) = 89.7 -- an aggregate dual term of 5.6e-09, comfortably inside the default tol = 1e-8 -- and the component gate refused it against 1.0, so a solve stationary to nine digits exited Solved_To_Acceptable_Level holding the answer. The same problem with its objective multiplied by a positive constant, which changes no feasible point, no solution and no active set, crosses the bound. The STRICT gate therefore judges the unscaled dual infeasibility against max(dual_inf_tol, this kappa times tol times the dual scale), where the dual scale is the magnitude of the largest term grad L is assembled from (grad f, J^T y, the bound multipliers) -- so what the floor forgives is a residual small RELATIVE to the terms that produced it, never a genuine non-stationarity: min -exp(x) s.t. x >= 0 reaching inf_du = 8.8e+47 with grad f = -8.8e47 has nothing cancelled and stays refused by eight orders. Bounded twice over: the aggregate tol gate must still pass on the same iterate, and at kappa = 1 the floor only rises above dual_inf_tol once the dual scale exceeds dual_inf_tol / tol = 1e8. Only the strict gate is affected; acceptable_dual_inf_tol is untouched. Set to 0 to disable the floor and restore bit-for-bit upstream behaviour -- also what to do if you tighten dual_inf_tol and want that absolute standard honoured unconditionally.")?;
1174
1175 r.add_lower_bounded_number_option("obj_scale_certificate_threshold", "Objective-scale floor below which a termination certificate is re-tested rather than trusted.", 0.0, false, 1e-4, "DELIBERATE DEVIATION FROM UPSTREAM IPOPT (pounce#200). Gradient-based objective scaling can pick a factor so small (floored at nlp_scaling_min_value, 1e-8) that the scaled convergence test passes far from any KKT point: on the flat quartics quartc/dqrtic the solver certified optimality at objective 248.88 / 39.36 when the true minimum is ~0, with an unscaled dual infeasibility of 0.84. When the objective scaling factor is below this threshold AND the max-norm UNSCALED KKT error is still above acceptable_tol, POUNCE does not stop; it keeps iterating. A constant objective scale cancels out of the Newton step and the line-search tests are scale-invariant, so the run continues along the trajectory an unscaled run would take, and on a genuinely false stop it reaches the true minimum (quartc then finishes at 8.8e-07). Whether the stop was really false is NOT predicted in advance -- it is tested: if continuing achieves nothing, the refused point is restored and reported with the status it would originally have had, so the outcome is never worse than without the mechanism. Set to 0 to disable and restore bit-for-bit upstream behaviour (Ipopt itself reports quartc at 248.88 as optimal). Distinct from kkt_fidelity_tol, which is a pure relabel at termination and does not change the iteration path.")?;
1179 r.add_lower_bounded_number_option("kkt_fidelity_tol", "Post-solve max-norm unscaled KKT error above which Solve_Succeeded is downgraded.", 0.0, false, 0.0, "Opt-in status-fidelity check (default 0 = disabled). When positive, after the solve a reported Solve_Succeeded whose max-norm UNSCALED KKT error (max of the unscaled dual infeasibility, constraint violation, and complementarity, reported as final_unscaled_kkt_error) exceeds this threshold is downgraded to Solved_To_Acceptable_Level. This is a pure relabel at termination — it does not change the iteration path or make the solver work harder; to instead drive the solver to a tighter point, tighten dual_inf_tol / constr_viol_tol / compl_inf_tol, which gate on the same unscaled residuals. Useful when the default (scaled) convergence test passes on an ill-conditioned, nlp_scaling-deflated problem but the user-space duals have drifted.")?;
1180
1181 r.set_registering_category("NLP");
1183 r.add_lower_bounded_number_option("bound_relax_factor", "Factor for initial relaxation of the bounds.", 0.0, false, 1e-8, "Before start of the optimization, the bounds given by the user are relaxed. This option sets the factor for this relaxation. Additional, the constraint violation tolerance constr_viol_tol is used to bound the relaxation by an absolute value. If it is set to zero, then then bounds relaxation is disabled. See Eqn.(35) in implementation paper. Note that the constraint violation reported by Ipopt at the end of the solution process does not include violations of the original (non-relaxed) variable bounds. See also option honor_original_bounds.")?;
1184 r.add_bool_option("honor_original_bounds", "Indicates whether final points should be projected into original bounds.", false, "Ipopt might relax the bounds during the optimization (see, e.g., option \"bound_relax_factor\"). This option determines whether the final point should be projected back into the user-provide original bounds after the optimization. Note that violations of constraints and complementarity reported by Ipopt at the end of the solution process are for the non-projected point.")?;
1185 r.set_registering_category("Warm Start");
1186 r.add_bool_option("warm_start_same_structure", "Indicates whether a problem with a structure identical to the previous one is to be solved.", false, "If enabled, then the algorithm assumes that an NLP is now to be solved whose structure is identical to one that already was considered (with the same NLP object).")?;
1187 r.add_bool_option("warm_start_init_point", "Warm-start for initial point", false, "Indicates whether this optimization should use a warm start initialization, where values of primal and dual variables are given (e.g., from a previous optimization of a related problem.)")?;
1196 r.set_registering_category("NLP");
1197 r.add_bool_option("check_derivatives_for_naninf", "Indicates whether it is desired to check for Nan/Inf in derivative matrices", false, "Activating this option will cause an error if an invalid number is detected in the constraint Jacobians or the Lagrangian Hessian. If this is not activated, the test is skipped, and the algorithm might proceed with invalid numbers and fail. If test is activated and an invalid number is detected, the matrix is written to output with print_level corresponding to J_MOREDETAILED (7); so beware of large output!")?;
1198 r.add_bool_option("grad_f_constant", "Indicates whether to assume that the objective function is linear", false, "Activating this option will cause Ipopt to ask for the Gradient of the objective function only once from the NLP and reuse this information later.")?;
1199 r.add_bool_option("jac_c_constant", "Indicates whether to assume that all equality constraints are linear", false, "Activating this option will cause Ipopt to ask for the Jacobian of the equality constraints only once from the NLP and reuse this information later.")?;
1200 r.add_bool_option("jac_d_constant", "Indicates whether to assume that all inequality constraints are linear", false, "Activating this option will cause Ipopt to ask for the Jacobian of the inequality constraints only once from the NLP and reuse this information later.")?;
1201 r.add_bool_option("hessian_constant", "Indicates whether to assume the problem is a QP (quadratic objective, linear constraints)", false, "Activating this option will cause Ipopt to ask for the Hessian of the Lagrangian function only once from the NLP and reuse this information later.")?;
1202 r.set_registering_category("Hessian Approximation");
1203 r.add_string_option("hessian_approximation", "Indicates what Hessian information is to be used.", "exact", &[("exact", "Use second derivatives provided by the NLP."), ("limited-memory", "Perform a limited-memory quasi-Newton approximation"), ("partitioned", "Perform a partitioned quasi-Newton approximation (one dense block per element function)"), ("finite-difference", "Recover the exact Hessian by sparse finite differences of the analytic Jacobian")], "This determines which kind of information for the Hessian of the Lagrangian function is used by the algorithm. \"partitioned\" is a pounce extension with no upstream counterpart: it keeps one small dense quasi-Newton block per element function (the objective and each constraint row, whose support is a row of the constraint Jacobian) and assembles them into a genuine sparse Hessian, so the linear solver sees the model's real block structure instead of the diagonal the limited-memory low-rank path presents. Intended for structured problems -- direct-collocation trajectory optimization above all -- where second derivatives are unavailable but the Jacobian sparsity is declared. See crates/pounce-algorithm/src/hess/partitioned_quasi_newton.rs.")?;
1204 r.add_string_option("partitioned_update_type", "Quasi-Newton update formula applied to each element block.", "sr1", &[("sr1", "Symmetric rank-1; carries indefinite curvature."), ("bfgs", "Powell-damped BFGS; forces each element block positive semidefinite.")], "Only used when \"hessian_approximation\" is \"partitioned\". SR1 is the default because an individual constraint is not convex: damped BFGS would force every element model PSD and the solve would then scale it by a multiplier of either sign, and the indefiniteness would never reach the inertia correction. See dev-notes/issue-131-monotone-lbfgs-stall.md for what that costs on the monolithic path.")?;
1205 r.add_string_option("fd_hessian_coloring", "How finite-difference probe groups are formed.", "cpr", &[("cpr", "Curtis-Powell-Reid; no two columns in a group share a row."), ("star", "Star colouring of the adjacency graph; fewer groups, but unsafe on a dense pattern.")], "Only used when \"hessian_approximation\" is \"finite-difference\". A star colouring lets an entry be read from EITHER endpoint's probe and so needs fewer groups -- 76 to 42 on the Jacobian-derived laptime pattern -- and its recovery is algebraically exact. It is NOT the default anyway, because a forward difference is not an exact Hessian-vector product: it also carries a third-derivative cross term into each row, and CPR's distance-2 property forbids the two columns that term needs from sharing a group, while a star colouring does not. Measured on laptime, star over the Jacobian pattern takes 404 iterations to a wrong objective where CPR takes 38 to the right one; over the sparser declared pattern both take 30. Group size is not the cause -- declared/star has the largest groups of the four and is fine. Use star only on a sparse declared pattern, and measure.")?;
1206 r.add_lower_bounded_number_option("fd_hessian_reuse_tol", "Relative movement below which the previous finite-difference Hessian is reused.", 0.0, false, 0.0, "Only used when \"hessian_approximation\" is \"finite-difference\". A rebuild costs one Jacobian evaluation per probe group, so skipping it on iterations where nothing moved is the cheapest saving available. BOTH the primal iterate and the multipliers are tested, not just x: the Lagrangian Hessian is grad^2 f + sum y_j grad^2 c_j, so a cached Hessian is stale the moment y moves even if x has not -- and the endgame of an interior-point solve is full of short steps with moving duals. 0 (the default) rebuilds every iteration.")?;
1207 r.add_string_option("fd_hessian_pattern", "Where the finite-difference Hessian takes its sparsity pattern from.", "declared", &[("declared", "The TNLP's declared Hessian structure, when it has one."), ("jacobian", "Derived from the Jacobian pattern alone, as the union over rows of supp(grad g) tensor supp(grad g).")], "Only used when \"hessian_approximation\" is \"finite-difference\". \"declared\" needs only the TNLP's Hessian STRUCTURE call, never its values, so it is available to a model that cannot evaluate second derivatives -- every .nl declares one through AMPL's AD. \"jacobian\" needs nothing beyond the Jacobian pattern every TNLP must declare, and is a strict superset of the true pattern: safe, since a superset costs extra probe groups and never a wrong answer, but not free -- on benchmarks/large_scale laptime it is 146267 nonzeros against the true 28000. There is deliberately no mode that guesses a subset, which would silently drop curvature.")?;
1208 r.add_string_option("partitioned_elements", "How the Lagrangian is split into elements for the partitioned Hessian approximation.", "per-constraint", &[("per-constraint", "One element per constraint row, plus the objective."), ("blocks", "One element per contiguous block of primal variables, modelling the Lagrangian's restriction to that block.")], "Only used when \"hessian_approximation\" is \"partitioned\". \"per-constraint\" gives each block a multiplier-independent target and assumes nothing about variable ordering, but produces as many blocks as there are constraints, each approximating a constraint Hessian with no sign structure. \"blocks\" is the partition of Asprion, Chinellato and Guzzella: a direct collocation transcription orders its variables by stage, so the Lagrangian Hessian is close to block diagonal in contiguous blocks, and the block count is the stage count rather than the constraint count. \"blocks\" defaults the update formula to damped BFGS, since the Lagrangian is the object an interior-point method wants a positive definite model of; the ordering assumption is reported rather than trusted -- POUNCE_PARTITIONED_ORACLE prints the fraction of the exact Hessian's Frobenius mass that falls inside the block pattern.")?;
1209 r.add_lower_bounded_integer_option("partitioned_block_size", "Target width of a primal block when \"partitioned_elements\" is \"blocks\".", 1, 64, "Only used when \"hessian_approximation\" is \"partitioned\" and \"partitioned_elements\" is \"blocks\". Should be set to the number of variables one transcription stage contributes (states times collocation points, plus controls); too small and the block misses genuine intra-stage coupling, too large and each block carries more parameters than its one curvature pair per iteration can determine.")?;
1210 r.add_lower_bounded_number_option("partitioned_curvature_cap", "Multiple of an element's own implied curvature that a single quasi-Newton update to its block may reach. Off by default.", 0.0, true, f64::INFINITY, "Only used when \"hessian_approximation\" is \"partitioned\". An element's secant pair implies a curvature of ||y_e||/||s_e||; this caps how far one update may move that element's block, in those same units. DEFAULT IS OFF BECAUSE EVERY FINITE VALUE MEASURED WAS WORSE THAN OFF, and non-monotonically so: on benchmarks/large_scale laptime at N=80 with max_iter=1200 (true optimum 65.462928), cap=1e1 exits ErrorInStepComputation at 1071 iterations and 65.518586, cap=1e2 hits the iteration limit at 67.202124, cap=1e6 hits it at 80.398129, and off converges in 559 iterations at 65.462802. Rejecting an update is selective: it drops exactly the elements whose curvature is moving fastest and leaves those blocks stale while their neighbours update, and the resulting inconsistent Hessian costs more than a uniformly noisy but coherent one. Kept as a knob so the effect can be re-measured against a different element decomposition; do not enable it without measuring.")?;
1211 r.add_lower_bounded_integer_option("partitioned_max_element", "Widest element that keeps a dense block under the partitioned Hessian approximation.", 1, 64, "Only used when \"hessian_approximation\" is \"partitioned\". An element with k nonzeros costs k(k+1)/2 stored reals, so a constraint row (or objective) touching a large share of the variables would dominate the memory. Elements wider than this degrade to a diagonal approximation satisfying the weak secant condition rather than being dropped, so a separable objective is still represented exactly and a coupled one approximately.")?;
1212 r.add_string_option(
1213 "hessian_approximation_space",
1214 "Indicates in which subspace the Hessian information is to be approximated.",
1215 "nonlinear-variables",
1216 &[
1217 (
1218 "nonlinear-variables",
1219 "only in space of nonlinear variables.",
1220 ),
1221 (
1222 "all-variables",
1223 "in space of all variables (without slacks)",
1224 ),
1225 ],
1226 "",
1227 )?;
1228
1229 r.set_registering_category("Output");
1231 r.set_registering_category("Output");
1232 r.add_bool_option("print_info_string", "Enables printing of additional info string at end of iteration output.", false, "This string contains some insider information about the current iteration. For details, look for \"Diagnostic Tags\" in the Ipopt documentation.")?;
1233 r.add_string_option("inf_pr_output", "Determines what value is printed in the \"inf_pr\" output column.", "original", &[("internal", "max-norm of violation of internal equality constraints"), ("original", "maximal constraint violation in original NLP")], "Ipopt works with a reformulation of the original problem, where slacks are introduced and the problem might have been scaled. The choice \"internal\" prints out the constraint violation of this formulation. With \"original\" the true constraint violation in the original NLP is printed.")?;
1234 r.add_lower_bounded_integer_option("print_frequency_iter", "Determines at which iteration frequency the summarizing iteration output line should be printed.", 1, 1, "Summarizing iteration output is printed every print_frequency_iter iterations, if at least print_frequency_time seconds have passed since last output.")?;
1235 r.add_lower_bounded_number_option("print_frequency_time", "Determines at which time frequency the summarizing iteration output line should be printed.", 0.0, false, 0.0, "Summarizing iteration output is printed if at least print_frequency_time seconds have passed since last output and the iteration number is a multiple of print_frequency_iter.")?;
1236 r.set_registering_category("");
1237
1238 r.set_registering_category("Step Calculation");
1240 r.set_registering_category("Step Calculation");
1241 r.add_bool_option("fast_step_computation", "Indicates if the linear system should be solved quickly.", false, "If enabled, the algorithm assumes that the linear system that is solved to obtain the search direction is solved sufficiently well. In that case, no residuals are computed to verify the solution and the computation of the search direction is a little faster.")?;
1242
1243 r.set_registering_category("Step Calculation");
1245 r.add_lower_bounded_integer_option("min_refinement_steps", "Minimum number of iterative refinement steps per linear system solve.", 0, 1, "Iterative refinement (on the full unsymmetric system) is performed for each right hand side. This option determines the minimum number of iterative refinements (i.e. at least \"min_refinement_steps\" iterative refinement steps are enforced per right hand side.)")?;
1246 r.add_lower_bounded_integer_option("max_refinement_steps", "Maximum number of iterative refinement steps per linear system solve.", 0, 10, "Iterative refinement (on the full unsymmetric system) is performed for each right hand side. This option determines the maximum number of iterative refinement steps.")?;
1247 r.add_lower_bounded_number_option("residual_ratio_max", "Iterative refinement tolerance", 0.0, true, 1e-10, "Iterative refinement is performed until the residual test ratio is less than this tolerance (or until \"max_refinement_steps\" refinement steps are performed).")?;
1248 r.add_lower_bounded_number_option("residual_ratio_singular", "Threshold for declaring linear system singular after failed iterative refinement.", 0.0, true, 1e-5, "If the residual test ratio is larger than this value after failed iterative refinement, the algorithm pretends that the linear system is singular.")?;
1249 r.add_lower_bounded_number_option("residual_improvement_factor", "Minimal required reduction of residual test ratio in iterative refinement.", 0.0, true, 0.999999999, "If the improvement of the residual test ratio made by one iterative refinement step is not better than this factor, iterative refinement is aborted.")?;
1250 r.add_lower_bounded_number_option("neg_curv_test_tol", "Tolerance for heuristic to ignore wrong inertia.", 0.0, false, 0.0, "If nonzero, incorrect inertia in the augmented system is ignored, and Ipopt tests if the direction is a direction of positive curvature. This tolerance is alpha_n in the paper by Zavala and Chiang (2014) and it determines when the direction is considered to be sufficiently positive. A value in the range of [1e-12, 1e-11] is recommended.")?;
1251 r.add_string_option("neg_curv_test_reg", "Whether to do the curvature test with the primal regularization (see Zavala and Chiang, 2014).", "yes", &[("yes", "use primal regularization with the inertia-free curvature test"), ("no", "use original IPOPT approach, in which the primal regularization is ignored")], "")?;
1252
1253 r.set_registering_category("Step Calculation");
1255 r.add_lower_bounded_number_option("max_hessian_perturbation", "Maximum value of regularization parameter for handling negative curvature.", 0.0, true, 1e20, "In order to guarantee that the search directions are indeed proper descent directions, Ipopt requires that the inertia of the (augmented) linear system for the step computation has the correct number of negative and positive eigenvalues. The idea is that this guides the algorithm away from maximizers and makes Ipopt more likely converge to first order optimal points that are minimizers. If the inertia is not correct, a multiple of the identity matrix is added to the Hessian of the Lagrangian in the augmented system. This parameter gives the maximum value of the regularization parameter. If a regularization of that size is not enough, the algorithm skips this iteration and goes to the restoration phase. This is delta_w^max in the implementation paper.")?;
1256 r.add_lower_bounded_number_option("min_hessian_perturbation", "Smallest perturbation of the Hessian block.", 0.0, false, 1e-20, "The size of the perturbation of the Hessian block is never selected smaller than this value, unless no perturbation is necessary. This is delta_w^min in implementation paper.")?;
1257 r.add_lower_bounded_number_option("perturb_inc_fact_first", "Increase factor for x-s perturbation for very first perturbation.", 1.0, true, 100.0, "The factor by which the perturbation is increased when a trial value was not sufficient - this value is used for the computation of the very first perturbation and allows a different value for the first perturbation than that used for the remaining perturbations. This is bar_kappa_w^+ in the implementation paper.")?;
1258 r.add_lower_bounded_number_option("perturb_inc_fact", "Increase factor for x-s perturbation.", 1.0, true, 8.0, "The factor by which the perturbation is increased when a trial value was not sufficient - this value is used for the computation of all perturbations except for the first. This is kappa_w^+ in the implementation paper.")?;
1259 r.add_bounded_number_option("perturb_dec_fact", "Decrease factor for x-s perturbation.", 0.0, true, 1.0, true, 1.0 / 3.0, "The factor by which the perturbation is decreased when a trial value is deduced from the size of the most recent successful perturbation. This is kappa_w^- in the implementation paper.")?;
1260 r.add_lower_bounded_number_option("first_hessian_perturbation", "Size of first x-s perturbation tried.", 0.0, true, 1e-4, "The first value tried for the x-s perturbation in the inertia correction scheme. This is delta_0 in the implementation paper.")?;
1261 r.add_lower_bounded_number_option(
1262 "jacobian_regularization_value",
1263 "Size of the regularization for rank-deficient constraint Jacobians.",
1264 0.0,
1265 false,
1266 1e-8,
1267 "This is bar delta_c in the implementation paper.",
1268 )?;
1269 r.add_lower_bounded_number_option(
1270 "jacobian_regularization_exponent",
1271 "Exponent for mu in the regularization for rank-deficient constraint Jacobians.",
1272 0.0,
1273 false,
1274 0.25,
1275 "This is kappa_c in the implementation paper.",
1276 )?;
1277 r.add_bool_option("perturb_always_cd", "Active permanent perturbation of constraint linearization.", false, "Enabling this option leads to using the delta_c and delta_d perturbation for the computation of every search direction. Usually, it is only used when the iteration matrix is singular.")?;
1278 r.add_lower_bounded_integer_option("perturb_delta_c_max_rungs", "Rungs of the delta_w ladder after which delta_c is withdrawn (pounce extension; not in upstream Ipopt).", 0, 3, "delta_c is the perturbation for a rank-deficient constraint Jacobian, and it is reached for when the factorization reports Singular. Since pounce gh#540 a factorization also reports Singular when its inertia is unmeasurable -- the count disagrees and the smallest pivot is at the noise floor -- which is evidence about the measurement, not about the Jacobian's rank. When the Jacobian in fact has full rank, delta_c cannot help, and because it stays switched on for the rest of the augmented system the delta_w ladder then has to climb against a matrix delta_c has made harder to hit the requested inertia on. On the gh#592 model that cost five rungs, ending at delta_w = 1e2 where Ipopt accepted the step at 1e-4; the over-damped step froze the objective for eight iterations and the solver then exited at a point a restart improved by 0.08%. Rather than predict which kind of Singular a report was -- the counts are the very thing gh#540 established are noise -- the ladder answers it: after this many rungs with delta_c on and still no acceptable inertia, delta_c is withdrawn, the delta_w ladder restarts, and delta_c is latched off for the remainder of that augmented system (the next iterate starts clean). Where delta_c is the right remedy this never fires: on eigena2 and eigenb2 it is followed by at most one rung. Lower values withdraw sooner; 0 disables the walk-back and restores the pre-#592 escalation exactly. See crates/pounce-common/src/pd_perturbation.rs (maybe_withdraw_delta_c) and dev-notes/issue-592-restart-non-idempotence.md.")?;
1279
1280 r.set_registering_category("Barrier Parameter Update");
1282 r.add_lower_bounded_number_option("sigma_max", "Maximum value of the centering parameter.", 0.0, true, 1e2, "This is the upper bound for the centering parameter chosen by the quality function based barrier parameter update. Only used if option \"mu_oracle\" is set to \"quality-function\".")?;
1283 r.add_lower_bounded_number_option("sigma_min", "Minimum value of the centering parameter.", 0.0, false, 1e-6, "This is the lower bound for the centering parameter chosen by the quality function based barrier parameter update. Only used if option \"mu_oracle\" is set to \"quality-function\".")?;
1284 r.add_string_option(
1285 "quality_function_norm_type",
1286 "Norm used for components of the quality function.",
1287 "2-norm-squared",
1288 &[
1289 ("1-norm", "use the 1-norm (abs sum)"),
1290 ("2-norm-squared", "use the 2-norm squared (sum of squares)"),
1291 ("max-norm", "use the infinity norm (max)"),
1292 ("2-norm", "use 2-norm"),
1293 ],
1294 "Only used if option \"mu_oracle\" is set to \"quality-function\".",
1295 )?;
1296 r.add_string_option("quality_function_centrality", "The penalty term for centrality that is included in quality function.", "none", &[("none", "no penalty term is added"), ("log", "complementarity * the log of the centrality measure"), ("reciprocal", "complementarity * the reciprocal of the centrality measure"), ("cubed-reciprocal", "complementarity * the reciprocal of the centrality measure cubed")], "This determines whether a term is added to the quality function to penalize deviation from centrality with respect to complementarity. The complementarity measure here is the xi in the Loqo update rule. Only used if option \"mu_oracle\" is set to \"quality-function\".")?;
1297 r.add_string_option("quality_function_balancing_term", "The balancing term included in the quality function for centrality.", "none", &[("none", "no balancing term is added"), ("cubic", "Max(0,Max(dual_inf,primal_inf)-compl)^3")], "This determines whether a term is added to the quality function that penalizes situations where the complementarity is much smaller than dual and primal infeasibilities. Only used if option \"mu_oracle\" is set to \"quality-function\".")?;
1298 r.add_lower_bounded_integer_option("quality_function_max_section_steps", "Maximum number of search steps during direct search procedure determining the optimal centering parameter.", 0, 8, "The golden section search is performed for the quality function based mu oracle. Only used if option \"mu_oracle\" is set to \"quality-function\".")?;
1299 r.add_bounded_number_option("quality_function_section_sigma_tol", "Tolerance for the section search procedure determining the optimal centering parameter (in sigma space).", 0.0, false, 1.0, true, 1e-2, "The golden section search is performed for the quality function based mu oracle. Only used if option \"mu_oracle\" is set to \"quality-function\".")?;
1300 r.add_bounded_number_option("quality_function_section_qf_tol", "Tolerance for the golden section search procedure determining the optimal centering parameter (in the function value space).", 0.0, false, 1.0, true, 0.0, "The golden section search is performed for the quality function based mu oracle. Only used if option \"mu_oracle\" is set to \"quality-function\".")?;
1301 r.add_lower_bounded_number_option("probing_iterate_quality_factor", "Iterate-quality guard for the probing (Mehrotra) mu oracle.", 0.0, false, 1e4, "If curr_avrg_compl / curr_mu exceeds this factor the probing oracle is skipped and a restoration request is signalled, because a 5+ order ratio would otherwise cause the oracle's sigma*mu_curr to throw the iterate out of the convergence neighborhood. Default 1e4. Set to 0 to disable (matches pre-pounce#58 behaviour). Only consumed when mu_oracle=probing (the default under mehrotra_algorithm=yes).")?;
1304
1305 r.set_registering_category("Restoration Phase");
1307 r.add_bounded_number_option("required_infeasibility_reduction", "Required reduction of infeasibility before leaving restoration phase.", 0.0, false, 1.0, true, 0.9, "The restoration phase algorithm is performed, until a point is found that is acceptable to the filter and the infeasibility has been reduced by at least the fraction given by this option.")?;
1308 r.add_lower_bounded_integer_option("max_resto_iter", "Maximum number of successive iterations in restoration phase.", 0, 3000000, "The algorithm terminates with an error message if the number of iterations successively taken in the restoration phase exceeds this number.")?;
1309
1310 r.set_registering_category("Restoration Phase");
1312 r.add_bool_option("evaluate_orig_obj_at_resto_trial", "Determines if the original objective function should be evaluated at restoration phase trial points.", true, "Enabling this option makes the restoration phase algorithm evaluate the objective function of the original problem at every trial point encountered during the restoration phase, even if this value is not required. In this way, it is guaranteed that the original objective function can be evaluated without error at all accepted iterates; otherwise the algorithm might fail at a point where the restoration phase accepts an iterate that is good for the restoration phase problem, but not the original problem. On the other hand, if the evaluation of the original objective is expensive, this might be costly.")?;
1313 r.add_lower_bounded_number_option(
1314 "resto_penalty_parameter",
1315 "Penalty parameter in the restoration phase objective function.",
1316 0.0,
1317 true,
1318 1e3,
1319 "This is the parameter rho in equation (31a) in the Ipopt implementation paper.",
1320 )?;
1321 r.add_lower_bounded_number_option("resto_proximity_weight", "Weighting factor for the proximity term in restoration phase objective.", 0.0, false, 1.0, "This determines how the parameter zeta in equation (29a) in the implementation paper is computed. zeta here is resto_proximity_weight*sqrt(mu), where mu is the current barrier parameter.")?;
1322
1323 r.set_registering_category("Restoration Phase");
1325 r.add_lower_bounded_number_option("bound_mult_reset_threshold", "Threshold for resetting bound multipliers after the restoration phase.", 0.0, false, 1e3, "After returning from the restoration phase, the bound multipliers are updated with a Newton step for complementarity. Here, the change in the primal variables during the entire restoration phase is taken to be the corresponding primal Newton step. However, if after the update the largest bound multiplier exceeds the threshold specified by this option, the multipliers are all reset to 1.")?;
1326 r.add_lower_bounded_number_option("constr_mult_reset_threshold", "Threshold for resetting equality and inequality multipliers after restoration phase.", 0.0, false, 0.0, "After returning from the restoration phase, the constraint multipliers are recomputed by a least square estimate. This option triggers when those least-square estimates should be ignored.")?;
1327 r.add_lower_bounded_number_option("resto_failure_feasibility_threshold", "Threshold for primal infeasibility to declare failure of restoration phase.", 0.0, false, 0.0, "If the restoration phase is terminated because of the \"acceptable\" termination criteria and the primal infeasibility is smaller than this value, the restoration phase is declared to have failed. The default value is actually 1e2*tol, where tol is the general termination tolerance.")?;
1328
1329 r.set_registering_category("Warm Start");
1331 r.add_lower_bounded_number_option(
1332 "warm_start_bound_push",
1333 "same as bound_push for the regular initializer",
1334 0.0,
1335 true,
1336 1e-3,
1337 "",
1338 )?;
1339 r.add_bounded_number_option(
1340 "warm_start_bound_frac",
1341 "same as bound_frac for the regular initializer",
1342 0.0,
1343 true,
1344 0.5,
1345 false,
1346 1e-3,
1347 "",
1348 )?;
1349 r.add_lower_bounded_number_option(
1350 "warm_start_slack_bound_push",
1351 "same as slack_bound_push for the regular initializer",
1352 0.0,
1353 true,
1354 1e-3,
1355 "",
1356 )?;
1357 r.add_bounded_number_option(
1358 "warm_start_slack_bound_frac",
1359 "same as slack_bound_frac for the regular initializer",
1360 0.0,
1361 true,
1362 0.5,
1363 false,
1364 1e-3,
1365 "",
1366 )?;
1367 r.add_lower_bounded_number_option(
1368 "warm_start_mult_bound_push",
1369 "same as mult_bound_push for the regular initializer",
1370 0.0,
1371 true,
1372 1e-3,
1373 "",
1374 )?;
1375 r.add_number_option(
1376 "warm_start_mult_init_max",
1377 "Maximum initial value for the equality multipliers.",
1378 1e6,
1379 "",
1380 )?;
1381 r.add_string_option(
1382 "warm_start_entire_iterate",
1383 "Tells algorithm whether to use the GetWarmStartIterate method in the NLP.",
1384 "no",
1385 &[
1386 ("no", "call GetStartingPoint in the NLP"),
1387 ("yes", "call GetWarmStartIterate in the NLP"),
1388 ],
1389 "",
1390 )?;
1391 r.add_number_option("warm_start_target_mu", "", 0.0, "Experimental!")?;
1392 r.add_string_option(
1393 "warm_start_recentering",
1394 "How the warm-start initializer adapts to the quality of the supplied iterate.",
1395 "residual",
1396 &[
1397 (
1398 "residual",
1399 "measure the supplied point and derive mu, the bound-multiplier fills, and the equality-multiplier reconstruction from it",
1400 ),
1401 (
1402 "none",
1403 "pre-pounce#606 behaviour: universal constants, zero-filled unseeded multipliers, mu untouched",
1404 ),
1405 ],
1406 "DELIBERATE DEVIATION FROM UPSTREAM IPOPT (pounce#606). Ipopt's warm-start initializer applies fixed pushes and floors regardless of what the caller handed it, and fills a missing multiplier block with a constant. That makes the warm start's behaviour a function of the options rather than of the iterate: an at-the-optimum restart and a stale point from a different parameter both start on the same barrier, and a caller who supplies only a primal point gets bound multipliers of warm_start_mult_bound_push -- a number chosen with no reference to the slacks it is paired against. Under `residual` the initializer instead measures the supplied point's primal residual, complementarity and stationarity residual; fills unseeded bound multipliers from mu/slack; re-derives an identically-zero equality-multiplier block from the same regularized stationarity least-squares solve the cold path uses; and raises mu to the measured average complementarity (clamped to [1e-11, 0.1]) when the supplied point cannot support the barrier mu_init asked for. Only complementarity moves mu: a warm point at a moved parameter carries primal and dual residuals of order delta-theta by construction, and raising mu to meet those discards the warm start to pay for a Newton step that was about to happen anyway -- measured at 715 -> 1129 iterations over the 27 parametric paths in benchmarks/warmstart before that term was dropped. `warm_start_target_mu` still overrides mu outright. Set to `none` to restore bit-for-bit pre-pounce#606 warm-start behaviour.",
1407 )?;
1408
1409 r.set_registering_category("CG Penalty");
1411 r.add_lower_bounded_number_option(
1412 "penalty_init_max",
1413 "Maximal value for the initial penalty parameter (for Chen-Goldfarb line search).",
1414 0.0,
1415 true,
1416 1e5,
1417 "",
1418 )?;
1419 r.add_lower_bounded_number_option("penalty_init_min", "Minimal value for the initial penalty parameter for line search (for Chen-Goldfarb line search).", 0.0, true, 1.0, "")?;
1420 r.add_lower_bounded_number_option(
1421 "penalty_max",
1422 "Maximal value for the penalty parameter (for Chen-Goldfarb line search).",
1423 0.0,
1424 true,
1425 1e30,
1426 "",
1427 )?;
1428 r.add_lower_bounded_number_option(
1429 "pen_des_fact",
1430 "a parameter used in penalty parameter computation (for Chen-Goldfarb line search).",
1431 0.0,
1432 true,
1433 2e-1,
1434 "",
1435 )?;
1436 r.add_lower_bounded_number_option("kappa_x_dis", "a parameter used to check if the fast direction can be used as the line search direction (for Chen-Goldfarb line search).", 0.0, true, 1e2, "")?;
1437 r.add_lower_bounded_number_option("kappa_y_dis", "a parameter used to check if the fast direction can be used as the line search direction (for Chen-Goldfarb line search).", 0.0, true, 1e4, "")?;
1438 r.add_lower_bounded_number_option("vartheta", "a parameter used to check if the fast direction can be used as the line search direction (for Chen-Goldfarb line search).", 0.0, true, 0.5, "")?;
1439 r.add_lower_bounded_number_option("delta_y_max", "a parameter used to check if the fast direction can be used as the line search direction (for Chen-Goldfarb line search).", 0.0, true, 1e12, "")?;
1440 r.add_lower_bounded_number_option("fast_des_fact", "a parameter used to check if the fast direction can be used as the line search direction (for Chen-Goldfarb line search).", 0.0, true, 1e-1, "")?;
1441 r.add_lower_bounded_number_option("pen_init_fac", "a parameter used to choose initial penalty parameters when the regularized Newton method is used.", 0.0, true, 5e1, "")?;
1442 r.add_bool_option(
1443 "never_use_fact_cgpen_direction",
1444 "Toggle to switch off the fast Chen-Goldfarb direction",
1445 false,
1446 "",
1447 )?;
1448
1449 r.set_registering_category("CG Penalty");
1451 r.add_bool_option(
1452 "never_use_piecewise_penalty_ls",
1453 "Toggle to switch off the piecewise penalty method",
1454 false,
1455 "",
1456 )?;
1457 r.add_bounded_number_option(
1458 "eta_penalty",
1459 "Relaxation factor in the Armijo condition for the penalty function.",
1460 0.0,
1461 true,
1462 0.5,
1463 true,
1464 1e-8,
1465 "",
1466 )?;
1467 r.add_lower_bounded_number_option("penalty_update_infeasibility_tol", "Threshold for infeasibility in penalty parameter update test.", 0.0, true, 1e-9, "If the new constraint violation is smaller than this tolerance, the penalty parameter is not increased.")?;
1468 r.add_lower_bounded_number_option("eta_min", "", 0.0, true, 1e1, "")?;
1469 r.add_lower_bounded_number_option("pen_theta_max_fact", "Determines upper bound for constraint violation in the filter.", 0.0, true, 1e4, "The algorithmic parameter theta_max is determined as theta_max_fact times the maximum of 1 and the constraint violation at initial point. Any point with a constraint violation larger than theta_max is unacceptable to the filter (see Eqn. (21) in implementation paper).")?;
1470 r.add_lower_bounded_number_option("penalty_update_compl_tol", "", 0.0, true, 1e1, "")?;
1471 r.add_lower_bounded_number_option("chi_hat", "", 0.0, true, 2.0, "")?;
1472 r.add_lower_bounded_number_option("chi_tilde", "", 0.0, true, 5.0, "")?;
1473 r.add_lower_bounded_number_option("chi_cup", "", 0.0, true, 1.5, "")?;
1474 r.add_lower_bounded_number_option("gamma_hat", "", 0.0, true, 0.04, "")?;
1475 r.add_lower_bounded_number_option("gamma_tilde", "", 0.0, true, 4.0, "")?;
1476 r.add_lower_bounded_number_option("epsilon_c", "", 0.0, true, 1e-2, "")?;
1477 r.add_lower_bounded_number_option("piecewisepenalty_gamma_obj", "", 0.0, true, 1e-13, "")?;
1478 r.add_lower_bounded_number_option("piecewisepenalty_gamma_infeasi", "", 0.0, true, 1e-13, "")?;
1479 r.add_lower_bounded_number_option("min_alpha_primal", "", 0.0, true, 1e-13, "")?;
1480 r.add_lower_bounded_number_option("theta_min", "", 0.0, true, 1e-6, "")?;
1481 r.add_lower_bounded_number_option(
1482 "mult_diverg_feasibility_tol",
1483 "tolerance for deciding if the multipliers are diverging",
1484 0.0,
1485 true,
1486 1e-7,
1487 "",
1488 )?;
1489 r.add_lower_bounded_number_option(
1490 "mult_diverg_y_tol",
1491 "tolerance for deciding if the multipliers are diverging",
1492 0.0,
1493 true,
1494 1e8,
1495 "",
1496 )?;
1497
1498 r.set_registering_category("Linear Solver");
1500 r.add_bool_option("linear_scaling_on_demand", "Flag indicating that linear scaling is only done if it seems required.", true, "This option is only important if a linear scaling method (e.g., mc19) is used. If you choose \"no\", then the scaling factors are computed for every linear system from the start. This can be quite expensive. Choosing \"yes\" means that the algorithm will start the scaling method only when the solutions to the linear system seem not good, and then use it until the end.")?;
1501
1502 r.set_registering_category("MA27 Linear Solver");
1504 r.add_bounded_integer_option("ma27_print_level", "Debug printing level for the linear solver MA27", 0, 4, 0, "0: no printing; 1: Error messages only; 2: Error and warning messages; 3: Error and warning messages and terse monitoring; 4: All information.")?;
1505 r.add_bounded_number_option(
1506 "ma27_pivtol",
1507 "Pivot tolerance for the linear solver MA27.",
1508 0.0,
1509 true,
1510 1.0,
1511 true,
1512 1e-8,
1513 "A smaller number pivots for sparsity, a larger number pivots for stability.",
1514 )?;
1515 r.add_bounded_number_option("ma27_pivtolmax", "Maximum pivot tolerance for the linear solver MA27.", 0.0, true, 1.0, true, 1e-4, "Ipopt may increase pivtol as high as ma27_pivtolmax to get a more accurate solution to the linear system.")?;
1516 r.add_lower_bounded_number_option("ma27_liw_init_factor", "Integer workspace memory for MA27.", 1.0, false, 5.0, "The initial integer workspace memory = liw_init_factor * memory required by unfactored system. Ipopt will increase the workspace size by ma27_meminc_factor if required.")?;
1517 r.add_lower_bounded_number_option("ma27_la_init_factor", "Real workspace memory for MA27.", 1.0, false, 5.0, "The initial real workspace memory = la_init_factor * memory required by unfactored system. Ipopt will increase the workspace size by ma27_meminc_factor if required.")?;
1518 r.add_lower_bounded_number_option("ma27_meminc_factor", "Increment factor for workspace size for MA27.", 1.0, false, 2.0, "If the integer or real workspace is not large enough, Ipopt will increase its size by this factor.")?;
1519 r.add_bool_option("ma27_skip_inertia_check", "Whether to always pretend that inertia is correct.", false, "Setting this option to \"yes\" essentially disables inertia check. This option makes the algorithm non-robust and easily fail, but it might give some insight into the necessity of inertia control.")?;
1520 r.add_bool_option("ma27_ignore_singularity", "Whether to use MA27's ability to solve a linear system even if the matrix is singular.", false, "Setting this option to \"yes\" means that Ipopt will call MA27 to compute solutions for right hand sides, even if MA27 has detected that the matrix is singular (but is still able to solve the linear system). In some cases this might be better than using Ipopt's heuristic of small perturbation of the lower diagonal of the KKT matrix.")?;
1521
1522 r.set_registering_category("MA57 Linear Solver");
1524 r.add_lower_bounded_integer_option("ma57_print_level", "Debug printing level for the linear solver MA57", 0, 0, "0: no printing; 1: Error messages only; 2: Error and warning messages; 3: Error and warning messages and terse monitoring; >=4: All information.")?;
1525 r.add_bounded_number_option(
1526 "ma57_pivtol",
1527 "Pivot tolerance for the linear solver MA57.",
1528 0.0,
1529 true,
1530 1.0,
1531 true,
1532 1e-8,
1533 "A smaller number pivots for sparsity, a larger number pivots for stability.",
1534 )?;
1535 r.add_bounded_number_option("ma57_pivtolmax", "Maximum pivot tolerance for the linear solver MA57.", 0.0, true, 1.0, true, 1e-4, "Ipopt may increase pivtol as high as ma57_pivtolmax to get a more accurate solution to the linear system.")?;
1536 r.add_lower_bounded_number_option("ma57_pre_alloc", "Safety factor for work space memory allocation for the linear solver MA57.", 1.0, false, 1.05, "If 1 is chosen, the suggested amount of work space is used. However, choosing a larger number might avoid reallocation if the suggest values do not suffice.")?;
1537 r.add_bounded_integer_option(
1538 "ma57_pivot_order",
1539 "Controls pivot order in MA57",
1540 0,
1541 5,
1542 5,
1543 "This is ICNTL(6) in MA57.",
1544 )?;
1545 r.add_bool_option("ma57_automatic_scaling", "Controls whether to enable automatic scaling in MA57", false, "For higher reliability of the MA57 solver, you may want to set this option to yes. This is ICNTL(15) in MA57.")?;
1546 r.add_lower_bounded_integer_option(
1547 "ma57_block_size",
1548 "Controls block size used by Level 3 BLAS in MA57BD",
1549 1,
1550 16,
1551 "This is ICNTL(11) in MA57.",
1552 )?;
1553 r.add_lower_bounded_integer_option(
1554 "ma57_node_amalgamation",
1555 "Node amalgamation parameter",
1556 1,
1557 16,
1558 "This is ICNTL(12) in MA57.",
1559 )?;
1560 r.add_bounded_integer_option("ma57_small_pivot_flag", "Handling of small pivots", 0, 1, 0, "If set to 1, then when small entries defined by CNTL(2) are detected they are removed and the corresponding pivots placed at the end of the factorization. This can be particularly efficient if the matrix is highly rank deficient. This is ICNTL(16) in MA57.")?;
1561
1562 r.add_bool_option("ma57_batched_backsolve", "Whether MA57 may answer several right-hand sides in one blocked back-substitution.", false, "pounce extension; not an upstream Ipopt option. Callers that batch right-hand sides purely to save time -- today the Sherman-Morrison-Woodbury correction block in the limited-memory quasi-Newton path -- ask the linear solver first whether its multi-RHS answer is bit-identical to solving the columns one at a time. MA57 blocks the substitution across columns, so it is not: the batched answer is tolerance-equal but differs in about the last bit. Setting this to \"yes\" says you accept that. It is a trajectory change, not just a speed-up: on gh#809's review model the same binary with and without the batch diverges in the last digit of the objective at iteration 20 and finishes at a different iteration count, and on a nonconvex problem a perturbation that size can select a different local optimum (gh#729 did exactly that on pooling_rt2stp, landing 25% worse while still reporting Optimal Solution Found). What you get for it, measured per iteration on that model: back-solve -32%, numeric factorization +18.5%, linear-algebra total -4.2% against a 3-5% replicate spread. Do not compare wall-clock across this option -- the runs walk different trajectories, so the difference is dominated by iteration count rather than by work removed. See dev-notes/ma57-batched-backsolve.md.")?;
1571
1572 r.set_registering_category("FERAL Linear Solver");
1583 r.add_bool_option("feral_cascade_break", "Whether to explicitly force FERAL's cascade-break pivot heuristic on or off.", false, "Cascade-break accelerates the dense trailing-update kernels in FERAL's supernodal LDL^T factor (~30x per factor on the 64k x 64k pinene_3200 KKT). Historically (pre-FERAL Phase B) it could flip the negative-eigenvalue count on borderline iterates because per-supernode delayed-pivot catchment was unbounded, causing spurious WrongInertia and delta_w escalation on robot_1600 / NARX_CFy / marine_1600 / rocket_12800. FERAL Phase B (issue #55, commit 7554a78) bounds catchment at symbolic-analysis time and arms CB out of the box; pounce now inherits that default whenever this option is left unset. `feral_cascade_break yes` records explicit intent (no behavioural change vs unset). `feral_cascade_break no` explicitly disarms CB and surfaces FERAL's `DelayBudgetExceeded` on non-root cascade victims — only useful to reproduce pre-Phase-B behaviour. The registered default value below (`false`) is the bool fallback when querying the OptionsList without checking the explicit-set bit, but pounce's binding (application.rs::feral_config_from_options) reads only when the option was explicitly set, so the displayed default does not actually apply at the solver level. See crates/pounce-feral/src/lib.rs and pounce#31 / feral#17, #55.")?;
1584 r.add_bool_option("feral_fma", "Whether FERAL should dispatch dense kernels through fused multiply-add intrinsics.", false, "On aarch64 / x86_v3, FMA-dispatched panel and trailing-update kernels run at roughly 2x the throughput of the generic kernels. The downside is small per-pivot rounding drift that trips more WrongInertia checks and delayed pivots — the same failure mode that forced cascade-break off by default. Off by default; turn on for workloads where kernel throughput dominates and the IPM tolerates a slightly noisier inertia signal.")?;
1585 r.add_bool_option("feral_increase_quality", "Whether FERAL may escalate its factorization (scaling, then pivot threshold) when the interior-point refinement stalls.", true, "DELIBERATE DEVIATION FROM UPSTREAM IPOPT (pounce#850), and a two-sided one -- this is ON by default, and the option exists because the rung both wins and loses solves. Ipopt calls IncreaseQuality when PdFullSpaceSolver's refinement stalls (IpPDFullSpaceSolver.cpp:296), and MA57 answers by raising pivtol toward pivtolmax (IpMa57TSolverInterface.cpp:832) -- strictly more conservative each time, so keeping it raised for the rest of the solve can only make the factorization safer. FERAL's ladder instead changes WHICH pivots are taken, which is lateral in trajectory terms, and it persists the same way. That ladder is documented as two rungs -- scaling Identity -> InfNorm, then pivot_threshold^0.75 -- but the first is UNREACHABLE as pounce ships: feral 0.17.0 takes the scaling rung only when numeric_params.scaling is ScalingStrategy::Identity, and pounce's default is ScalingStrategy::Auto (crates/pounce-feral/src/lib.rs). So every escalation a pounce user sees is a pivot_threshold bump, the first of them 1e-8 -> 1e-6, a factor of 100. This matters for the obvious remedy: a milder ladder cannot help, because on square_flowsheet_resto's lbfgs leg every static feral_pivtol in {1e-6, 3.16e-5, 4.2e-4, 1e-2, 0.5} loses the leg from iteration 0 and only 1e-8 solves it. The harm is the destination, not the size of the step to it -- across every later factorization, a restoration sub-solve's included. It therefore reroutes solves, in both directions. It COSTS two whole solves on square_flowsheet_resto: the exact leg goes Optimal at 99 iterations to RestorationFailed at 131, shipped only because a second-opinion rung rescues it at 185 total, and the lbfgs leg goes Optimal at 178 to the 3000-iteration cap with nothing to rescue it. It BUYS accuracy that nothing else supplies: the 12-variable model in watchdog_trial_is_not_a_divergence_verdict ends Solved_To_Acceptable_Level at obj 3.7e-6 with the rung and at obj 3.42 against f* = 0 without it -- a wrong-ish answer under a success-shaped status, which is worse than an honest failure -- and it buys 15-25% of the iterations on several more fixture-legs (deb7 171 -> 147 and pooling_rt2stp 128 -> 109 on the exact leg, plus three second-opinion ladder totals). Two entries once listed alongside those do not belong there and are corrected here (pounce#857): lbfgs eigena2 goes 202 -> 186 but exits ErrorInStepComputation either way, so it is 16 fewer iterations to the same non-answer and not a win; and lbfgs pooling_rt2stp goes the other way, 273 -> 295, so the rung COSTS iterations there. No policy separates those. The rung fires twice in square_flowsheet_resto's BASE solve on the exact leg -- once in the main loop, which the info string prints as a q on the row labelled 26, and once inside restoration at 76r, which the info string does not print at all because restoration sub-solve rows carry no info column. Both figures are now reported directly by the quality_escalations statistic (pounce#857), in the JSON report, the console summary and the sweep's q= column; the base-solve 2 was originally derived with a process-global firing cap and the statistic reproduces it independently. Read the scope: 2 is the base solve, and the exact leg escalates SIX times process-wide once the second-opinion rung's own solve is counted, while the lbfgs leg reaches 25 per solve. Allowing only the first firing still loses the leg, so declining it for the restoration sub-solve alone would not help; nor does a count, since deb7 and square_flowsheet_resto each fire it exactly twice on their exact legs, one gaining and one losing -- which is why the recovery rung added in pounce#857 gates on the VERDICT and uses the count only as a >= 1 admission test. Set no to recover a model this rung costs; square_flowsheet_resto solves cleanly on both legs with it off. The obvious upstream fix was a REVERTIBLE escalation -- one that does not govern every later factorization, which FERAL's ladder could not express when quality_level only ratcheted up -- and it was requested as jkitchin/feral#192, landed as reset_quality, and MEASURED HERE: plumbed and instrumented (376 escalations, 376 matching resets on one solve) it does not recover either leg at either re-baselining boundary. That refutes the \"not whether to escalate but for how long\" diagnosis pounce#857 was filed under. It is consistent with the static-pivtol result above -- the harm is the destination, and a trajectory that visited the raised threshold even once is already on the other path -- and it is why the recovery here is a RE-SOLVE rather than a re-baselining. Taking the feral bump is separately unattractive: it costs the clean exact leg on its own (jkitchin/feral#196). So the losing direction recovers itself instead: feral_increase_quality_retry (pounce#857) re-solves once with this option off when a solve that ACTUALLY escalated ends in Restoration_Failed or Maximum_Iterations_Exceeded, which is what turns square_flowsheet_resto's lbfgs leg from the 3000-iteration cap back into Optimal at 178 without the user having to know this option exists. Only the NLP path consults this; pounce-convex's engines never call increase_quality. Not to be confused with feral_refine, which is a different half of the same commit and is where its performance win lives -- refinement makes no difference to either regressed leg.")?;
1586 r.add_bool_option("feral_refine", "Whether FERAL should run iterative refinement on every back-solve.", false, "Off by default, as it is on every direct linear solver Ipopt ships and on pounce's own MA57. Per Waechter-Biegler section 3.10, refinement belongs on the *unreduced* Newton system, because the condensation Sigma = S^-1 Z destroys information as mu -> 0 and a backend can only refine the condensed system it factorized. PdFullSpaceSolver::compute_residuals is the unreduced one, so pounce already refines the right system in the right place, capped at max_refinement_steps (10) and accepting at residual_ratio_max (1e-10). Upstream disables the backend loop everywhere: MA27 has no such routine; MA57's MA57D is never declared or called (IpMa57TSolverInterface.cpp:785 calls only ma57c); MUMPS sets icntl[9] = 0 under the comment \"no iterative refinement iterations\"; Pardiso Project registers 0; only MKL Pardiso and WSMP opt in. Turning it on nests FERAL's loop inside pounce's: FERAL's convergence target is hard-wired to eps*sqrt(n), machine precision on the condensed system, which on a large ill-conditioned KKT is unreachable, so the inner loop runs to its cap on every back-solve driving a residual nobody consults to a tolerance nobody set. One augmented-system solve then costs up to 10x11 substitution passes plus a matvec per step. It was on by default in every release through 0.10.0, and the reason was real but incomplete: FERAL defaults to ZeroPivotAction::ForceAccept, so its raw solve can leave real residual against the system it factorized -- something Ipopt's architecture assumes a backend does not do -- and with nothing else to catch that, the gh#590 badly-scaled LP (data scale 1e11) exits RestorationFailed with refinement off. But Ipopt's answer to a factorization that cannot deliver is not to refine inside the backend; it is IncreaseQuality (IpPDFullSpaceSolver.cpp:296) -- escalate the factorization and refactor -- and FeralSolverInterface::increase_quality returned a hard-coded false, so that rung was missing and the inner refinement was standing in for it. Wiring increase_quality through to FERAL's own ladder (scaling Identity -> InfNorm, then pivot_threshold^0.75, the analog of MA57's pivtol = min(pivtolmax, pivtol^0.75)) restores it, and gh#590 then solves with this off. Measured on the 126028-dimension laptime KKT under limited-memory, one binary, three runs back to back: 68.9 s with this on against 18.8 s with it off and the rung wired, next to MA57's 10.7 s, with LinearSystemBackSolve going 54.6 s -> 8.2 s. Set yes to restore the pre-0.11 behaviour on a problem that needs it; see feral_refine_steps to cap the inner loop instead of leaving it, and feral_refine_target to skip it on back-solves that are already accurate enough. Neither Ma57SolverInterface nor TSymLinearSolver has in-backend refinement, so this duplication is FERAL-only.")?;
1587 r.add_lower_bounded_integer_option("feral_refine_steps", "Maximum correction steps in FERAL's inner iterative refinement, per back-solve.", 0, 10, "FERAL's inner iterative-refinement budget (feral#178, requested by pounce gh#698 observation 5 and tracked as gh#710). Only consulted when feral_refine is yes. Ten is FERAL's own default and is right for a caller that solves Ax=b once and keeps the answer; it is wrong for a caller running its own refinement over the same system, which PdFullSpaceSolver does -- it computes the residual after each back-solve and decides from it whether to continue. Nested, one augmented-system solve costs up to max_steps x (max_steps+1) substitution passes plus a matvec against the original matrix per step, and only the outer loop consults the residual it drives. Measured on a 118276-dimension KKT: turning the inner loop off entirely cut LinearSystemBackSolve 60% (147.3 s to 58.3 s) and wall time 20%. Setting 0 is not the same as feral_refine=no -- it still routes through the refined entry point and so through that path's choice of solve core -- so use feral_refine=no to switch refinement off. The cap is an upper bound only: FERAL's eps*sqrt(n) residual target, its 100x divergence guard and its 2-strike plateau exit all keep priority, and the best-iterate contract holds at every value, so no cap can return an answer worse than the unrefined solve. Worth knowing before retuning: feral#179 measured that nothing merely ill-conditioned reaches a 10-step budget at all (Hilbert n=8..40 stops at 3-7, ill-conditioned bordered KKTs at 1); the budget is only reachable when the factor is a genuinely perturbed approximate inverse, which is pounce's case because pounce perturbs the L-factor. Ten remains the default, and no smaller constant is safe -- but it is the cap that survives the corpus, not a number anyone chose. The alternatives each lose something different: at 0 the gh#590 badly-scaled LP (data scale 1e11) exits RestorationFailed; at 1 deb7 on the exact leg goes SolveSucceeded 171 -> ErrorInStepComputation 183 and cresc4 under limited-memory SolveSucceeded 143 -> InfeasibleProblemDetected 32; at 2 deb7 goes to ErrorInStepComputation 258 and cresc4 needs 997 iterations instead of 143. The reason it is chaotic rather than monotone: FERAL's convergence test is ||r||/||b|| < eps*sqrt(n), machine precision, and on a 118276-dimension near-singular KKT that target is unreachable, so the loop runs to the cap on every back-solve and the best-iterate contract returns whichever of the k+1 non-converged iterates had the smallest ||r||. Smallest residual on the condensed system does not mean better Newton step, so each cap is close to a lottery draw -- eigena2 under limited-memory is Optimal at 5, SolvedToAcceptableLevel at 10 and ErrorInStepComputation at 0, 1, 2, 3 and 4. It is not cascade-break: laptime with feral_cascade_break=no runs 62.113 s against 61.741 s armed, bit-identical objective. Ipopt does not have this problem because it disables backend-internal refinement on every direct solver it ships (MA27 has no such routine; MA57's MA57D is never declared or called, IpMa57TSolverInterface.cpp:785 calls only ma57c; MUMPS sets icntl[9] = 0 under the comment \"no iterative refinement iterations\"; Pardiso Project registers 0; only MKL Pardiso and WSMP opt in), per Waechter-Biegler section 3.10: refinement belongs on the *unreduced* non-symmetric Newton system, because the condensation Sigma = S^-1 Z destroys information as mu -> 0, and a backend can only refine the condensed system it factorized. PdFullSpaceSolver::compute_residuals is the unreduced one, so pounce already refines the right system in the right place. What stops pounce simply passing 0 like Ipopt is that FERAL defaults to ZeroPivotAction::ForceAccept, so its raw solve can leave real residual against the system it factorized, which Ipopt's architecture assumes a backend does not; that is what loses the gh#590 LP at 0. The real fix is a residual-target option upstream so the inner loop can stop at what the host needs (residual_ratio_max, 1e-10) instead of chasing machine precision -- RefineOptions carries max_steps and nothing else today. Cost of leaving it at 10 on the 58014-variable laptime benchmark under limited-memory: LinearSystemBackSolve is 48.962 s of a 61.741 s solve, against 7.443 s of 16.728 s at a cap of 0 -- 45 seconds, 73% of wall time. Lower it per problem when back-solve dominates the timing report and re-check the answer; it is not safe to lower globally.")?;
1588 r.add_bool_option("feral_static_pivoting", "Whether FERAL should factor with static pivoting (SSIDS-style delayed pivots disabled).", false, "When yes, every supernode runs as the root does (allow_delayed_pivots = false): a pivot that fails the column-relative threshold is force-accepted in place (ZeroPivotAction::ForceAccept) with iterative refinement recovering the residual, instead of being delayed up the elimination tree. This is FERAL's analogue of MA57's cntl[4] static-pivoting fallback and the fast path out of the delayed-pivot cascade (feral#8: an 87 s factor on an otherwise sub-second problem; the motivating emfl050 case in pounce#254 is a ~44 s single factorization). The trade is bounded L growth on force-accepted small pivots, which the IPM's outer regularization (delta_x, delta_c) and back-solve refinement absorb. Off by default (inherit delayed pivoting). This is deliberately an explicit, per-solve opt-in and is NOT coupled to max_wall_time: budget-triggered numerics would make a solve's result — and a branch-and-bound node's dual bound — depend on the clock, so the accuracy/speed trade is left to the caller. Pounce reads this option only when set explicitly; unset it falls back to the POUNCE_FERAL_STATIC_PIVOTING environment variable. See crates/pounce-feral/src/lib.rs (FeralConfig::static_pivoting) and dev-notes/feral-factor-interrupt.md.")?;
1589 r.add_lower_bounded_number_option("feral_refine_target", "Residual level at which FERAL's inner iterative refinement is skipped.", 0.0, false, 0.0, "Consulted only when feral_refine is on. Before refining, the back-solve is solved once without refinement and its relative residual ||b - A*x||_2 / ||b||_2 is measured; if it is at or below this value the answer is accepted as is and the refinement loop never runs. 0 (the default) disables the check, so every back-solve refines -- the behaviour every release through 0.10.0 shipped. The check exists because FERAL's RefineOptions carries a step cap and no target: its convergence test is hard-wired to eps*sqrt(n), the tightest residual the arithmetic admits, while PdFullSpaceSolver accepts a solve at residual_ratio_max = 1e-10 on the unreduced system. On the 126028-dimension KKT of the laptime benchmark the unrefined solve already lands at 1.5e-11 against a hard-wired target of 7.9e-14, and the loop then spends four to five steps -- 48.962 s of a 61.741 s solve -- chasing digits the caller discards. Unlike feral_refine_steps=0 this still refines the back-solves that need it, which is what keeps the gh#590 noise-floor LP (data scale 1e11) solving. Falls back to the POUNCE_FERAL_REFINE_TARGET environment variable when not set on the OptionsList. Upstream fix: feral#190. See crates/pounce-feral/src/lib.rs (FeralConfig::refine_target).")?;
1590 r.add_lower_bounded_number_option("feral_singular_pivot_floor", "Near-singularity trigger for the FERAL backend.", 0.0, false, 1e-20, "FERAL's default zero-pivot policy force-accepts a pivot at the working-precision floor and still reports a successful factorization, so a numerically rank-deficient KKT system that happens to land on the correct inertia produces a clean solve and the IPM never escalates delta_w. This option is pounce's analog of MA57's CNTL(2) small-pivot threshold: after a successful factor, the smallest accepted D-block pivot magnitude min|lambda(D)| (scaled space) is compared against this absolute floor, and if it falls below, the factor is reported Singular so PDPerturbationHandler::PerturbForSingularity bumps the Hessian perturbation. An absolute floor is used rather than the scale-free ratio min/max ~ 1/cond(D), because an interior-point KKT is designed to become ill-conditioned as mu->0 and the ratio collapses on healthy full-rank systems near the solution. Lower values are more permissive (fewer factors flagged singular); 0 disables the trigger. Default 1e-20 (MA57 CNTL(2)). See crates/pounce-feral/src/lib.rs and feral dev/research/near-singularity-signal.md.")?;
1591 r.add_lower_bounded_number_option("feral_inertia_pivot_floor", "Pivot magnitude below which a mismatching inertia count is treated as noise (FERAL backend).", 0.0, false, 1e-12, "Consulted only when the factorization's negative-eigenvalue count already disagrees with what the IPM asked for. If the smallest accepted pivot magnitude (scaled space) is under this floor, the factor is reported Singular instead of WrongInertia, so PDPerturbationHandler::PerturbForSingularity raises delta_c (which repairs a rank-deficient constraint Jacobian) before the delta_w ladder starts multiplying the Hessian perturbation by 8 per retry. Rationale (pounce gh#540): a pivot at the working-precision floor of an equilibrated matrix carries no reliable sign, so the count read off it is not a measurement -- on eigena2 the same iterate returns 64, 58 and 62 negatives against an expected 55, and an exact LAPACK eigendecomposition of the dumped matrices agrees with none of them. Escalating delta_w against such a reading damped the Newton step from 1.2e-7 to 8e-9 and cost the superlinear tail. Because the trigger fires only on a factorization the caller was already going to reject, it can never turn a usable factor into a failure; it only changes which perturbation is reached for first. Necessarily larger than feral_singular_pivot_floor, which governs factors that are unusable outright. IMPORTANT (pounce gh#592): pounce reads this option only when it is set explicitly, and unset does NOT mean the 1e-12 registered above -- it means the dimension-aware default n*eps, where n is the order of the factored matrix. The level at which an equilibrated pivot loses its sign is n*eps, spanning 2e-15 at n=10 to 2e-10 at n=10^6; the fixed 1e-12 this option shipped with sits mid-range but corresponds to n~4500, so on the few-hundred-order KKTs an IPM actually factors (n*eps ~ 5e-14) it convicted pivots more than an order of magnitude above the noise, spending delta_c on a full-rank constraint Jacobian and -- because delta_c persists once switched on -- making the inertia harder to hit for the rest of the solve. Setting this option pins an absolute floor at every dimension; 0 disables the trigger. See crates/pounce-feral/src/lib.rs (inertia_trust_floor) and dev-notes/issue-592-restart-non-idempotence.md.")?;
1592 r.add_lower_bounded_number_option("feral_min_par_flops", "Flop threshold above which FERAL dispatches a supernode subtree to a parallel worker.", 0.0, false, 1e8, "FERAL's parallel-dispatch gate (feral#19): a supernode tree is only handed to rayon once its estimated flop count clears this threshold, so small trees stay serial and avoid the fork/join overhead. Lower values dispatch more aggressively (0 fires the gate on every multi-child tree at or above N_PAR_MIN supernodes); a very large value effectively rejects all tree-level parallel dispatch (the value is cast to u64 with saturation, so anything at or beyond u64::MAX pins the reject-all sentinel). This option matters only when FERAL's internal parallelism is active (see feral_parallel / the FERAL_PARALLEL env var); on a serial factor it has no effect. Pounce reads it only when set explicitly; when unset, FeralConfig inherits FERAL's built-in NumericParams default (10^8), so behaviour is unchanged. Falls back to the POUNCE_FERAL_MIN_PAR_FLOPS environment variable when not set on the OptionsList. See crates/pounce-feral/src/lib.rs (FeralConfig::min_par_flops).")?;
1593 r.add_bounded_number_option(
1594 "feral_pivtol",
1595 "Pivot tolerance for the linear solver FERAL.",
1596 0.0,
1597 true,
1598 0.5,
1599 false,
1600 1e-8,
1601 "Relative Bunch-Kaufman partial-pivoting threshold u: a candidate diagonal pivot is rejected when |d| < u * col_max. Direct analog of ma27_pivtol / ma57_pivtol. A smaller number pivots for sparsity (preserves the AMD ordering, keeps L sparse, factors faster but is less stable); a larger number pivots for stability (rejects more candidates, delays pivots, forces more 2x2 blocks, denser L but better backward error). LAPACK's textbook maximum-stability value is 0.5. Falls back to the POUNCE_FERAL_PIVTOL environment variable (or its deprecated legacy alias FERAL_PIVTOL) when not set on the OptionsList.",
1602 )?;
1603 r.add_string_option(
1604 "feral_ordering",
1605 "Fill-reducing ordering method for the FERAL backend.",
1606 "auto",
1607 &[
1608 ("auto", "Adaptive dispatcher: picks a concrete method per matrix from cheap pattern features (very-large-and-sparse → AMD; n ≤ 10000 → AMF; otherwise → MetisND). Pounce default."),
1609 ("auto_race", "Race-based dispatcher: runs symbolic factorization on AMD, MetisND, ScotchND, KahipND and keeps the smallest factor_nnz. ~4× a single symbolic pass, paid once per problem because symbolic factorization is cached across numeric refactorizations with the same pattern. Use when symbolic cost is amortized over many numeric factorizations on a hard problem."),
1610 ("amd", "Approximate Minimum Degree (feral-amd: external degree with aggressive element absorption). Matches SuiteSparse/faer; robust default for IPM workloads. Best for very-large-and-sparse (n > 100k, avg_deg < 5)."),
1611 ("amf", "Approximate Minimum Fill (feral-amf, HAMF4 variant of Amestoy 1999). Strong on small-and-sparse populations (n ≤ 10000); aggregate fill ≈ 0.87× AMD on the IPM small-sparse inventory."),
1612 ("metis", "feral-metis multilevel nested dissection. Tends to produce squarer fronts than AMD on banded / nearly-1D structure; preferred for large structured matrices."),
1613 ("scotch", "feral-scotch nested dissection. Similar regime to METIS; alternative when METIS is unavailable or for cross-validation."),
1614 ("kahip", "feral-kahip flow-based nested dissection with K1 preprocessing. Ties METIS on fill geomean at 4-6× per-call symbolic cost; reach for it only when ND fill matters and per-call cost is amortized."),
1615 ],
1616 "Pounce reads this option only when set explicitly. When unset, FeralConfig defaults to Auto, the same adaptive dispatcher that feral's `pick_default_method` uses internally. Concrete-method choices bypass the dispatcher and pin a single method for the run. AutoRace measures actual symbolic outcomes per problem and is the safest choice when the per-problem winner is uncertain. Falls back to the POUNCE_FERAL_ORDERING environment variable (same tag set) when not set on the OptionsList. See `crates/pounce-feral/src/lib.rs` (FeralConfig::ordering) and `feral/src/symbolic/mod.rs` (OrderingMethod) for per-variant rationale and evidence.",
1617 )?;
1618 r.add_string_option(
1619 "feral_scaling",
1620 "Diagonal scaling strategy for the FERAL backend.",
1621 "auto",
1622 &[
1623 ("auto", "Adaptive shape-based router: picks Mc64Symmetric on arrow-KKT signatures (many degree-1 constraint-slack columns) and InfNorm otherwise. FERAL default and pounce default."),
1624 ("infnorm", "Knight-Ruiz iterative ∞-norm equilibration. Matches the dense Bunch-Kaufman scaling; the only choice that solves the MSS1_0009-class residual to working precision today."),
1625 ("mc64", "MC64-style symmetric matching-based scaling (MUMPS SYM=2 / SSIDS options%scaling=1 default). Useful where matching conditions the matrix better than ∞-norm balancing; recovers exact inertia on some ill-conditioned saddle-point KKTs where Auto mis-pivots and reports spurious zero pivots (discs, sawpath — see feral#65), at the cost of MC64 symbolic overhead on every factor."),
1626 ("identity", "Identity scaling (no-op). For regression testing and inputs where any scaling is inappropriate."),
1627 ],
1628 "Pounce reads this option only when set explicitly. When unset, FeralConfig defaults to Auto, FERAL's current built-in default, so behaviour is unchanged. Falls back to the POUNCE_FERAL_SCALING environment variable (same tag set) when not set on the OptionsList. The External(Vec<f64>) strategy is not reachable from this string option. See `crates/pounce-feral/src/lib.rs` (FeralConfig::scaling) and `feral/src/scaling/mod.rs` (ScalingStrategy) for per-variant rationale.",
1629 )?;
1630 r.add_bool_option(
1631 "feral_infeasibility_scaling_retry",
1632 "Re-solve once with MC64 scaling if the solve declares local infeasibility under Auto/InfNorm.",
1633 true,
1634 "Some interior-point KKT trajectories are hypersensitive: under two different (equally backward-stable) linear-solver scalings the iterates stay bit-identical for many iterations, then diverge by ~1 ULP and fall into different basins — one reaching the optimum, the other a spurious stationary point of the constraint violation that the IPM reports as Infeasible_Problem_Detected (discs.nl is the canonical case: InfNorm → local infeasibility, MC64/Identity/MA57/IPOPT → optimal). This is sensitive dependence, not a bad solve, so the a-priori scaling router cannot distinguish the two and no per-factor backward-error signal flags it; the only reliable signal is the whole-solve verdict. When this option is set (default), a solve that ends in Infeasible_Problem_Detected under a non-MC64 effective scaling is re-run once with feral_scaling=mc64 (main IPM and restoration sub-IPM both). The MC64 result is promoted only if it returns Solve_Succeeded / Solved_To_Acceptable_Level; otherwise the original infeasibility verdict stands. This is rung 1 of the second-opinion ladder and probes ONLY numerical hypersensitivity: when the trajectory is not ULP-sensitive, MC64 retraces the same iterates and agrees for the same reason the first solve was wrong, so its agreement is not by itself evidence — see infeasibility_mu_strategy_retry for the rung that varies the trajectory. Skipped when the effective scaling is already MC64, and when the interactive debugger is active. Set to no to keep behaviour bit-for-bit faithful to upstream IPOPT (which does not retry). Honoured by every single-solve entry point — the pounce CLI, the Python Problem.solve, the C IpoptSolve, and the pounce-rs builder — which all drive the ladder through pounce_restoration::second_opinion_driver::run_second_opinion_ladder. Not run by the multi-start paths (solve_nlp_batch, the CLI's minima global search), where a failed start is routine and up to three extra solves per failed start would multiply cost for no benefit.",
1635 )?;
1636 r.add_bool_option(
1637 "infeasibility_mu_strategy_retry",
1638 "Re-solve once with mu_strategy=adaptive if the solve declares local infeasibility under the monotone barrier default.",
1639 true,
1640 "Rung 2 of the local-infeasibility second-opinion ladder, and the one that varies the iterate sequence rather than the linear algebra. A local-infeasibility verdict is a local statement about a nonconvex problem — the IPM reached a stationary point of the constraint violation, not a proof that no feasible point is reachable — and which stationary point it reaches depends on the barrier trajectory. gh #524 is the worked case: on the CUTE problem cresc4 (6 variables, 8 constraints, feasible, IPOPT solves it in 71 iterations) the monotone-mu default converges to a point with constraint violation 0.51 and reports Infeasible_Problem_Detected, while mu_strategy=adaptive reaches the known optimum 0.8718975; the MC64 rung above reproduced the failing trajectory character-for-character through iteration 15, diverged at iteration 16 in the eighth significant digit, and landed in the same basin anyway, so its agreement added no information. Retrying with a different barrier strategy is also the standard remedy IPOPT's own documentation gives a user who gets an infeasibility verdict on a problem they believe is feasible. When this option is set (default), a solve that ends in Infeasible_Problem_Detected is re-run once with mu_strategy=adaptive (main IPM and restoration sub-IPM both) AND with feral_scaling restored to its baseline value, so exactly one knob differs from the original solve — stacking it on top of the MC64 rung loses the fix on cresc4. The result is promoted only if it returns Solve_Succeeded / Solved_To_Acceptable_Level, so a promotion is always backed by the retry's own convergence check rather than by trusting the strategy; otherwise the original infeasibility verdict stands. Costs one extra solve only on runs that would otherwise report failure. Skipped when mu_strategy is already adaptive, when the infeasibility was certified by presolve, and when the interactive debugger is active. Set to no to keep behaviour bit-for-bit faithful to upstream IPOPT (which does not retry). Honoured by every single-solve entry point — the pounce CLI, the Python Problem.solve, the C IpoptSolve, and the pounce-rs builder — which all drive the ladder through pounce_restoration::second_opinion_driver::run_second_opinion_ladder. Not run by the multi-start paths (solve_nlp_batch, the CLI's minima global search), where a failed start is routine and up to three extra solves per failed start would multiply cost for no benefit.",
1641 )?;
1642
1643 r.add_bool_option(
1644 "feral_increase_quality_retry",
1645 "Re-solve once with feral_increase_quality=no when a solve that actually escalated the factorization ends in restoration failure, at the iteration limit, or at a point of local infeasibility.",
1646 true,
1647 "Rung 4 of the second-opinion ladder, and the only one whose gate is a MEASUREMENT of the failing solve rather than a property of the options it ran under (pounce#857). feral_increase_quality is on by default and is a two-sided rung -- it buys accuracy and 15-25% of the iterations on several fixture-legs and it loses whole solves on others (see that option's own text) -- and the losing direction had no automatic recovery: the documented remedy was for the user to notice, read the option, and re-run. This rung is that re-run, taken automatically, and only where it can possibly be the explanation. The gate is two conditions and both are necessary. (1) The verdict must be Restoration_Failed, Maximum_Iterations_Exceeded or Infeasible_Problem_Detected. The first two are the shapes the regression takes on square_flowsheet_resto under macOS/aarch64 -- the exact leg goes Optimal/99 to Restoration_Failed/131 and the lbfgs leg goes Optimal/178 to the 3000-iteration cap -- and the second of those is a verdict SecondOpinionTrigger::for_status otherwise opens no ladder on at all, on the sound general reasoning that the answer to a budget exit is a bigger budget. That reasoning has exactly this exception: when the escalation is what rerouted the trajectory into the wall, a bigger budget re-runs the same wall, and 178 iterations were available on the un-escalated path. The third is the same regression on the same fixture and the same leg under linux/x86_64, where the identical 3000 iterations and 25 escalations end in Infeasible_Problem_Detected instead -- a FALSE infeasibility verdict on a feasible model, which is the worst thing the escalation does, because it is a wrong answer reported as a verdict rather than as a failure, and the three pre-existing infeasibility rungs all fail to rescue it. Naming that status is not free the way the other two are, since a genuinely infeasible model cannot be recovered by any rung and the re-solve only confirms the verdict: across the fixture corpus it moves six lines, all of them infeasibility fixtures that escalated, costing one extra solve each (infeasible_square_scaled_1em4 61 -> 78 total iterations on the exact leg, issue_508_infeasible_gap_1em4 982 -> 1423) with no status, objective, iteration count or engine moving anywhere. The escalation gate is what bounds that cost -- of the eight NLP-arm infeasibility fixture-legs, four escalated and take the rung and four never escalated and are untouched. (2) The solve must have escalated at least once, which is what the quality_escalations statistic reports (also pounce#857; it is in the JSON report, the console summary, and the sweep's q= column). Without that count the gate is unimplementable: an escalation leaves no trace in status, objective, iteration count or engine, and the printed q info-string flag misses the ones that happen inside a restoration sub-solve. With it, a solve that never escalated is provably not a candidate and pays nothing -- which is what keeps this from being a blanket extra solve on every Maximum_Iterations_Exceeded exit. What the gate deliberately is NOT is a count threshold. deb7 and square_flowsheet_resto's base solve each escalate exactly twice on their exact legs, one gaining and one losing, so no count separates them; only the verdict does, and deb7's verdict is Optimal, so it never reaches this rung. Appended LAST, after the perturbed-start rung, so a Restoration_Failed that the pounce#815 rung already recovers costs nothing new -- square_flowsheet_resto's exact leg is exactly that case and is unchanged by this option. The result is promoted only if it returns Solve_Succeeded / Solved_To_Acceptable_Level. Skipped when feral_increase_quality is already no, when the solve escalated zero times, and when the interactive debugger is active. The cost is one extra solve on a run that was already going to report failure, with one case worth naming: a Maximum_Iterations_Exceeded exit is the only trigger a user can induce deliberately, by setting a small max_iter, and an escalating capped run now spends a second budget's worth of iterations before reporting. Set to no to hold a capped run to exactly the budget it was given, or to keep a failing solve's verdict as the escalating trajectory produced it. Honoured by every single-solve entry point -- the pounce CLI, the Python Problem.solve, the C IpoptSolve, and the pounce-rs builder -- which all drive the ladder through pounce_restoration::second_opinion_driver::run_second_opinion_ladder. Not run by the multi-start paths (solve_nlp_batch, the CLI's minima global search), where a failed start is routine. This option ALSO stands down the mu_strategy stall retry, and that is not a second feature but the same one: mu_strategy_fallback fires unconditionally on Maximum_Iterations_Exceeded, so before pounce#857 an escalating budget exit paid for two rescue solves and used one -- on square_flowsheet_resto's limited-memory leg, 3000 capped iterations, then a second full 3000 under the flipped schedule that escalated 25 times again and ended no better, and only then this rung's 178. When this rung is open on a budget exit the flip is skipped, which takes that run from three solves to two and from 6178 real iterations to 3178 without changing a single reported number (it=, q=, the objective and the engine all belong to the promoted solve, which is why the fixture sweep is byte-identical across the STAND-DOWN specifically -- not across the whole of pounce#857, whose infeasibility trigger moves six lines -- and the only visible trace of the saved solve is the count of per-solve summary blocks). Setting this option to no therefore restores both halves of the pre-857 behaviour: no rung 4, and the mu flip back on an escalating budget exit.",
1648 )?;
1649 r.set_registering_category("Initialization");
1650 r.add_bool_option(
1651 "infeasibility_perturbed_start_retry",
1652 "Re-solve once from a slightly displaced starting point if the solve declares local infeasibility or hits an invalid number.",
1653 true,
1654 "Rung 3 of the second-opinion ladder, and the one that varies neither the linear algebra (rung 1) nor the barrier trajectory (rung 2) but the point the trajectory starts from. It exists because a measurement said the starting point, not the algorithm, is where most of these failures are decided. Over a 244-problem corpus taken from the KRONOS benchmark set (Ahmed & Hasan 2026, doi:10.1016/j.compchemeng.2026.109839), fifteen models ended Infeasible_Problem_Detected or Invalid_Number_Detected from their bundled start; ten of those are models an independent solver proves feasible to 2.4e-7 or better, so the verdict was wrong. Of the fifteen, start_with_resto recovered 0, expect_infeasible_problem 0, mu_strategy=adaptive 4, and one displaced start 13 — and adding restoration on top of the displaced start reached 14. That ordering is the diagnosis: the iterate does not need to be BETTER, it needs to be NON-DEGENERATE. The common failure is a start where the constraint Jacobian is structurally rank-deficient — a squared slack sitting at zero, or an origin start on a homogeneous quadratic — at which LICQ fails and the filter line search has no descent direction to find, whatever it is given. Displacing the point by a relative 1e-2 restores rank, and the solve that follows is an ordinary one. When this option is set (default), a solve that ends Infeasible_Problem_Detected, Invalid_Number_Detected or Restoration_Failed is re-run once with start_point_perturbation=1e-2 AND with the earlier rungs' knobs restored to baseline, so exactly one thing differs from the original solve. Restoration_Failed was added by gh#815: restoration failing says the iterate reached somewhere its sub-problem could not work from, which is a statement about the path and not about the model, and it stops far short of max_iter so a bigger budget is not the answer. It opens this rung and no other, so it costs one extra solve. The result is promoted only if it returns Solve_Succeeded / Solved_To_Acceptable_Level. The displacement is deterministic given start_point_perturbation_seed, so a promotion is reproducible and a failure is reportable. Non-finite entries in the starting point are replaced with a finite in-bounds value before the displacement, because NaN plus noise is NaN and without that step the retry would reproduce the original Invalid_Number_Detected exactly. Costs one extra solve only on runs that would otherwise report failure. Skipped when the infeasibility was certified by presolve, and when the interactive debugger is active. Set to no to keep behaviour bit-for-bit faithful to upstream IPOPT (which does not retry). Honoured by every single-solve entry point — the pounce CLI, the Python Problem.solve, the C IpoptSolve, and the pounce-rs builder; not run by the multi-start paths (solve_nlp_batch, the CLI's minima global search).",
1655 )?;
1656 r.add_lower_bounded_number_option(
1657 "start_point_perturbation",
1658 "Relative magnitude of a deterministic displacement applied to the starting point before the solve (0 disables).",
1659 0.0,
1660 false,
1661 0.0,
1662 "Each variable is displaced by scale*(1 + |x_i|)*u_i with u_i drawn uniformly from [-1, 1), then clipped back inside any bound it has. The (1 + |x_i|) factor is what makes the displacement nonzero at x_i = 0: a purely relative perturbation is identically zero at the origin, and a start at the origin is the single most common degenerate start in the corpus this was measured on. Non-finite entries are replaced by a finite in-bounds value (the midpoint of a two-sided box, one unit inside a one-sided bound, zero if free) before the displacement, so this also rescues a start carrying a NaN. Off by default: displacing a start the user chose is a trajectory change, and a user who supplied a considered initial guess is entitled to have it used. The intended way to reach this is the automatic rung — see infeasibility_perturbed_start_retry — which applies it only after the solve has already failed, where there is no good trajectory left to preserve. Setting it directly is for reproducing such a retry, or for a deliberate multistart driven from outside.",
1663 )?;
1664 r.add_lower_bounded_integer_option(
1665 "start_point_perturbation_seed",
1666 "Seed for the start_point_perturbation displacement.",
1667 0,
1668 0,
1669 "The displacement is drawn from SplitMix64 seeded by this value and nothing else — no clock, no address, no thread identity — so the same seed and the same incoming point give the same displaced point on every platform and every run. That is what makes a promoted retry reproducible and a failed one reportable. Vary it to drive a multistart by hand.",
1670 )?;
1671 r.add_string_option(
1672 "start_point_conditioner",
1673 "Optional first-order warm-up run on the starting point before the barrier solve.",
1674 "none",
1675 &[
1676 ("none", "Use the starting point as given."),
1677 ("adam", "Run Adam on the penalised merit f(x) + rho*||violation(x)||^2 and start the barrier solve from where it lands."),
1678 ],
1679 "The `adam` setting is stage 0 of the KRONOS algorithm (Ahmed & Hasan 2026, doi:10.1016/j.compchemeng.2026.109839), generalised from that paper's equality-only rho*||h(x)||^2 to two-sided constraint bounds so it applies to an arbitrary NLP rather than only to a squared-slack reformulation; the violation of a row is its distance outside [g_l, g_u] and zero inside, which reduces to g - b on an equality row. It changes only where the solve starts — no algorithm, no derivative, no option below it moves — so the barrier solve that follows is exactly the solve pounce would have run had the conditioned point been passed in. It is off by default because it is a real preconditioner with a fat tail. Measured on 40 problems pounce already solves it broke none of them and cut the iteration count on 22, sometimes hard (rk23 82 -> 11, bt5 45 -> 9, chnrosnb 40 -> 10, hs056 42 -> 12), with median 0.83x and geometric mean 0.79x — but the TOTAL rose 1.62x (3030 -> 4900 iterations), driven by palmer1c 71 -> 1023 and biggs6 1906 -> 2938; excluding those two the ratio is 0.89x. A fixed unscaled penalty against a badly-scaled model walks the iterate somewhere the barrier method then has to walk back from. A median win with a 14x tail is an option, not a default. The warm-up is also guarded: if it does not reduce the merit it hands back the original point unchanged, so enabling it can never cost more than the function evaluations it spent.",
1680 )?;
1681 r.add_lower_bounded_integer_option(
1682 "adam_warmup_iters",
1683 "Iteration budget for start_point_conditioner=adam.",
1684 0,
1685 200,
1686 "KRONOS's published stage-0 budget. Adam's step is size-capped near the learning rate regardless of the gradient, so this budget buys roughly iters*learning_rate units of travel in each coordinate — 10 units at the defaults. Raise it for a model whose start is far from anywhere useful; the cost is that many objective, constraint and Jacobian evaluations. Ignored unless start_point_conditioner=adam.",
1687 )?;
1688 r.add_lower_bounded_number_option(
1689 "adam_warmup_learning_rate",
1690 "Step size for start_point_conditioner=adam.",
1691 0.0,
1692 true,
1693 5e-2,
1694 "KRONOS's published stage-0 value. Because Adam normalises by the second-moment estimate, this is very nearly the per-coordinate step length in the model's own units, not a scale factor on the gradient — so it wants setting against the size of the variables, not the size of the derivatives. Ignored unless start_point_conditioner=adam.",
1695 )?;
1696 r.add_lower_bounded_number_option(
1697 "adam_warmup_penalty",
1698 "Weight rho on the squared constraint violation in the start_point_conditioner=adam merit.",
1699 0.0,
1700 false,
1701 10.0,
1702 "KRONOS's published stage-0 value. The merit is f(x) + rho*||violation(x)||^2, so rho trades the objective against feasibility during the warm-up only; it has no effect on the barrier solve that follows. The fixed, unscaled default is the most likely cause of the measured tail on badly-scaled models (palmer1c 71 -> 1023 iterations) — if the warm-up hurts a particular model, this is the first knob to move. Ignored unless start_point_conditioner=adam.",
1703 )?;
1704
1705 r.set_registering_category("MA77 Linear Solver");
1707 r.add_integer_option("ma77_print_level", "Debug printing level for the linear solver MA77", -1, "<0: no printing; 0: Error and warning messages only; 1: Limited diagnostic printing; >1 Additional diagnostic printing.")?;
1708 r.add_lower_bounded_integer_option(
1709 "ma77_buffer_lpage",
1710 "Number of scalars per MA77 in-core buffer page in the out-of-core solver MA77",
1711 1,
1712 4096,
1713 "Must be at most ma77_file_size.",
1714 )?;
1715 r.add_lower_bounded_integer_option(
1716 "ma77_buffer_npage",
1717 "Number of pages that make up MA77 buffer",
1718 1,
1719 1600,
1720 "Number of pages of size buffer_lpage that exist in-core for the out-of-core solver MA77.",
1721 )?;
1722 r.add_lower_bounded_integer_option("ma77_file_size", "Target size of each temporary file for MA77, scalars per type", 1, 2097152, "MA77 uses many temporary files, this option controls the size of each one. It is measured in the number of entries (int or double), NOT bytes.")?;
1723 r.add_lower_bounded_integer_option("ma77_maxstore", "Maximum storage size for MA77 in-core mode", 0, 0, "If greater than zero, the maximum size of factors stored in core before out-of-core mode is invoked.")?;
1724 r.add_lower_bounded_integer_option(
1725 "ma77_nemin",
1726 "Node Amalgamation parameter",
1727 1,
1728 8,
1729 "Two nodes in elimination tree are merged if result has fewer than ma77_nemin variables.",
1730 )?;
1731 r.add_lower_bounded_number_option(
1732 "ma77_small",
1733 "Zero Pivot Threshold",
1734 0.0,
1735 false,
1736 1e-20,
1737 "Any pivot less than ma77_small is treated as zero.",
1738 )?;
1739 r.add_lower_bounded_number_option("ma77_static", "Static Pivoting Threshold", 0.0, false, 0.0, "See MA77 documentation. Either ma77_static=0.0 or ma77_static>ma77_small. ma77_static=0.0 disables static pivoting.")?;
1740 r.add_bounded_number_option(
1741 "ma77_u",
1742 "Pivoting Threshold",
1743 0.0,
1744 false,
1745 0.5,
1746 false,
1747 1e-8,
1748 "See MA77 documentation.",
1749 )?;
1750 r.add_bounded_number_option(
1751 "ma77_umax",
1752 "Maximum Pivoting Threshold",
1753 0.0,
1754 false,
1755 0.5,
1756 false,
1757 1e-4,
1758 "Maximum value to which u will be increased to improve quality.",
1759 )?;
1760 r.add_string_option(
1761 "ma77_order",
1762 "Controls type of ordering used by MA77",
1763 "metis",
1764 &[
1765 (
1766 "amd",
1767 "Use the HSL_MC68 approximate minimum degree algorithm",
1768 ),
1769 (
1770 "metis",
1771 "Use the MeTiS nested dissection algorithm (if available)",
1772 ),
1773 ],
1774 "",
1775 )?;
1776
1777 r.set_registering_category("MA86 Linear Solver");
1779 r.add_integer_option("ma86_print_level", "Debug printing level", -1, "<0: no printing; 0: Error and warning messages only; 1: Limited diagnostic printing; >1 Additional diagnostic printing.")?;
1780 r.add_lower_bounded_integer_option(
1781 "ma86_nemin",
1782 "Node Amalgamation parameter",
1783 1,
1784 32,
1785 "Two nodes in elimination tree are merged if result has fewer than ma86_nemin variables.",
1786 )?;
1787 r.add_lower_bounded_number_option(
1788 "ma86_small",
1789 "Zero Pivot Threshold",
1790 0.0,
1791 false,
1792 1e-20,
1793 "Any pivot less than ma86_small is treated as zero.",
1794 )?;
1795 r.add_lower_bounded_number_option("ma86_static", "Static Pivoting Threshold", 0.0, false, 0.0, "See MA86 documentation. Either ma86_static=0.0 or ma86_static>ma86_small. ma86_static=0.0 disables static pivoting.")?;
1796 r.add_bounded_number_option(
1797 "ma86_u",
1798 "Pivoting Threshold",
1799 0.0,
1800 false,
1801 0.5,
1802 false,
1803 1e-8,
1804 "See MA86 documentation.",
1805 )?;
1806 r.add_bounded_number_option(
1807 "ma86_umax",
1808 "Maximum Pivoting Threshold",
1809 0.0,
1810 false,
1811 0.5,
1812 false,
1813 1e-4,
1814 "Maximum value to which u will be increased to improve quality.",
1815 )?;
1816 r.add_string_option(
1817 "ma86_scaling",
1818 "Controls scaling of matrix",
1819 "mc64",
1820 &[
1821 ("none", "Do not scale the linear system matrix"),
1822 ("mc64", "Scale linear system matrix using MC64"),
1823 ("mc77", "Scale linear system matrix using MC77 [1,3,0]"),
1824 ],
1825 "",
1826 )?;
1827 r.add_string_option(
1828 "ma86_order",
1829 "Controls type of ordering",
1830 "auto",
1831 &[
1832 ("auto", "Try both AMD and MeTiS, pick best"),
1833 (
1834 "amd",
1835 "Use the HSL_MC68 approximate minimum degree algorithm",
1836 ),
1837 (
1838 "metis",
1839 "Use the MeTiS nested dissection algorithm (if available)",
1840 ),
1841 ],
1842 "",
1843 )?;
1844
1845 r.set_registering_category("MA97 Linear Solver");
1847 r.add_integer_option("ma97_print_level", "Debug printing level", -1, "<0: no printing; 0: Error and warning messages only; 1: Limited diagnostic printing; >1 Additional diagnostic printing.")?;
1848 r.add_lower_bounded_integer_option(
1849 "ma97_nemin",
1850 "Node Amalgamation parameter",
1851 1,
1852 8,
1853 "Two nodes in elimination tree are merged if result has fewer than ma97_nemin variables.",
1854 )?;
1855 r.add_lower_bounded_number_option(
1856 "ma97_small",
1857 "Zero Pivot Threshold",
1858 0.0,
1859 false,
1860 1e-20,
1861 "Any pivot less than ma97_small is treated as zero.",
1862 )?;
1863 r.add_bounded_number_option(
1864 "ma97_u",
1865 "Pivoting Threshold",
1866 0.0,
1867 false,
1868 0.5,
1869 false,
1870 1e-8,
1871 "See MA97 documentation.",
1872 )?;
1873 r.add_bounded_number_option(
1874 "ma97_umax",
1875 "Maximum Pivoting Threshold",
1876 0.0,
1877 false,
1878 0.5,
1879 false,
1880 1e-4,
1881 "See MA97 documentation.",
1882 )?;
1883 r.add_string_option("ma97_scaling", "Specifies strategy for scaling", "dynamic", &[("none", "Do not scale the linear system matrix"), ("mc30", "Scale all linear system matrices using MC30"), ("mc64", "Scale all linear system matrices using MC64"), ("mc77", "Scale all linear system matrices using MC77 [1,3,0]"), ("dynamic", "Dynamically select scaling according to rules specified by ma97_scalingX and ma97_switchX options.")], "")?;
1884 r.add_string_option("ma97_scaling1", "First scaling.", "mc64", &[("none", "No scaling"), ("mc30", "Scale linear system matrix using MC30"), ("mc64", "Scale linear system matrix using MC64"), ("mc77", "Scale linear system matrix using MC77 [1,3,0]")], "If ma97_scaling=dynamic, this scaling is used according to the trigger ma97_switch1. If ma97_switch2 is triggered it is disabled.")?;
1885 r.add_string_option("ma97_switch1", "First switch, determine when ma97_scaling1 is enabled.", "od_hd_reuse", &[("never", "Scaling is never enabled."), ("at_start", "Scaling to be used from the very start."), ("at_start_reuse", "Scaling to be used on first iteration, then reused thereafter."), ("on_demand", "Scaling to be used after Ipopt request improved solution (i.e. iterative refinement has failed)."), ("on_demand_reuse", "As on_demand, but reuse scaling from previous itr"), ("high_delay", "Scaling to be used after more than 0.05*n delays are present"), ("high_delay_reuse", "Scaling to be used only when previous itr created more that 0.05*n additional delays, otherwise reuse scaling from previous itr"), ("od_hd", "Combination of on_demand and high_delay"), ("od_hd_reuse", "Combination of on_demand_reuse and high_delay_reuse")], "If ma97_scaling=dynamic, ma97_scaling1 is enabled according to this condition. If ma97_switch2 occurs this option is henceforth ignored.")?;
1886 r.add_string_option("ma97_scaling2", "Second scaling.", "mc64", &[("none", "No scaling"), ("mc30", "Scale linear system matrix using MC30"), ("mc64", "Scale linear system matrix using MC64"), ("mc77", "Scale linear system matrix using MC77 [1,3,0]")], "If ma97_scaling=dynamic, this scaling is used according to the trigger ma97_switch2. If ma97_switch3 is triggered it is disabled.")?;
1887 r.add_string_option("ma97_switch2", "Second switch, determine when ma97_scaling2 is enabled.", "never", &[("never", "Scaling is never enabled."), ("at_start", "Scaling to be used from the very start."), ("at_start_reuse", "Scaling to be used on first iteration, then reused thereafter."), ("on_demand", "Scaling to be used after Ipopt request improved solution (i.e. iterative refinement has failed)."), ("on_demand_reuse", "As on_demand, but reuse scaling from previous itr"), ("high_delay", "Scaling to be used after more than 0.05*n delays are present"), ("high_delay_reuse", "Scaling to be used only when previous itr created more that 0.05*n additional delays, otherwise reuse scaling from previous itr"), ("od_hd", "Combination of on_demand and high_delay"), ("od_hd_reuse", "Combination of on_demand_reuse and high_delay_reuse")], "If ma97_scaling=dynamic, ma97_scaling2 is enabled according to this condition. If ma97_switch3 occurs this option is henceforth ignored.")?;
1888 r.add_string_option(
1889 "ma97_scaling3",
1890 "Third scaling.",
1891 "mc64",
1892 &[
1893 ("none", "No scaling"),
1894 ("mc30", "Scale linear system matrix using MC30"),
1895 ("mc64", "Scale linear system matrix using MC64"),
1896 ("mc77", "Scale linear system matrix using MC77 [1,3,0]"),
1897 ],
1898 "If ma97_scaling=dynamic, this scaling is used according to the trigger ma97_switch3.",
1899 )?;
1900 r.add_string_option("ma97_switch3", "Third switch, determine when ma97_scaling3 is enabled.", "never", &[("never", "Scaling is never enabled."), ("at_start", "Scaling to be used from the very start."), ("at_start_reuse", "Scaling to be used on first iteration, then reused thereafter."), ("on_demand", "Scaling to be used after Ipopt request improved solution (i.e. iterative refinement has failed)."), ("on_demand_reuse", "As on_demand, but reuse scaling from previous itr"), ("high_delay", "Scaling to be used after more than 0.05*n delays are present"), ("high_delay_reuse", "Scaling to be used only when previous itr created more that 0.05*n additional delays, otherwise reuse scaling from previous itr"), ("od_hd", "Combination of on_demand and high_delay"), ("od_hd_reuse", "Combination of on_demand_reuse and high_delay_reuse")], "If ma97_scaling=dynamic, ma97_scaling3 is enabled according to this condition.")?;
1901 r.add_string_option(
1902 "ma97_order",
1903 "Controls type of ordering",
1904 "auto",
1905 &[
1906 (
1907 "auto",
1908 "Use HSL_MA97 heuristic to guess best of AMD and METIS",
1909 ),
1910 ("best", "Try both AMD and MeTiS, pick best"),
1911 (
1912 "amd",
1913 "Use the HSL_MC68 approximate minimum degree algorithm",
1914 ),
1915 ("metis", "Use the MeTiS nested dissection algorithm"),
1916 (
1917 "matched-auto",
1918 "Use the HSL_MC80 matching with heuristic choice of AMD or METIS",
1919 ),
1920 (
1921 "matched-metis",
1922 "Use the HSL_MC80 matching based ordering with METIS",
1923 ),
1924 (
1925 "matched-amd",
1926 "Use the HSL_MC80 matching based ordering with AMD",
1927 ),
1928 ],
1929 "",
1930 )?;
1931 r.add_string_option(
1932 "ma97_dump_matrix",
1933 "Controls whether HSL_MA97 dumps each matrix to a file",
1934 "no",
1935 &[("no", "Do not dump matrix"), ("yes", "Do dump matrix")],
1936 "",
1937 )?;
1938 r.add_string_option(
1939 "ma97_solve_blas3",
1940 "Controls if blas2 or blas3 routines are used for solve",
1941 "no",
1942 &[
1943 (
1944 "no",
1945 "Use BLAS2 (faster, some implementations bit incompatible)",
1946 ),
1947 ("yes", "Use BLAS3 (slower)"),
1948 ],
1949 "",
1950 )?;
1951
1952 r.set_registering_category("Mumps Linear Solver");
1954 r.add_lower_bounded_integer_option("mumps_print_level", "Debug printing level for the linear solver MUMPS", 0, 0, "0: no printing; 1: Error messages only; 2: Error, warning, and main statistic messages; 3: Error and warning messages and terse diagnostics; >=4: All information.")?;
1955 r.add_bounded_number_option(
1956 "mumps_pivtol",
1957 "Pivot tolerance for the linear solver MUMPS.",
1958 0.0,
1959 false,
1960 1.0,
1961 false,
1962 1e-6,
1963 "A smaller number pivots for sparsity, a larger number pivots for stability.",
1964 )?;
1965 r.add_bounded_number_option("mumps_pivtolmax", "Maximum pivot tolerance for the linear solver MUMPS.", 0.0, false, 1.0, false, 0.1, "Ipopt may increase pivtol as high as pivtolmax to get a more accurate solution to the linear system.")?;
1966 r.add_lower_bounded_integer_option("mumps_mem_percent", "Percentage increase in the estimated working space for MUMPS.", 0, 1000, "When significant extra fill-in is caused by numerical pivoting, larger values of mumps_mem_percent may help use the workspace more efficiently. On the other hand, if memory requirement are too large at the very beginning of the optimization, choosing a much smaller value for this option, such as 5, might reduce memory requirements.")?;
1967 r.add_bounded_integer_option(
1968 "mumps_permuting_scaling",
1969 "Controls permuting and scaling in MUMPS",
1970 0,
1971 7,
1972 7,
1973 "This is ICNTL(6) in MUMPS.",
1974 )?;
1975 r.add_bounded_integer_option(
1976 "mumps_pivot_order",
1977 "Controls pivot order in MUMPS",
1978 0,
1979 7,
1980 7,
1981 "This is ICNTL(7) in MUMPS.",
1982 )?;
1983 r.add_bounded_integer_option(
1984 "mumps_scaling",
1985 "Controls scaling in MUMPS",
1986 -2,
1987 77,
1988 77,
1989 "This is ICNTL(8) in MUMPS.",
1990 )?;
1991 r.add_number_option("mumps_dep_tol", "Threshold to consider a pivot at zero in detection of linearly dependent constraints with MUMPS.", 0.0, "This is CNTL(3) in MUMPS.")?;
1992 r.add_integer_option("mumps_mpi_communicator", "MPI communicator used for matrix operations", -987654, "This sets the MPI communicator. MPI_COMM_WORLD is the default. Any other value should be the return value from MPI_Comm_c2f. This option is only available if MUMPS's libseq/mpi.h is not used.")?;
1993
1994 r.set_registering_category("Pardiso (pardiso-project.org) Linear Solver");
1996 r.add_string_option(
1997 "pardiso_matching_strategy",
1998 "Matching strategy to be used by Pardiso",
1999 "complete+2x2",
2000 &[
2001 ("complete", "Match complete (IPAR(13)=1)"),
2002 ("complete+2x2", "Match complete+2x2 (IPAR(13)=2)"),
2003 ("constraints", "Match constraints (IPAR(13)=3)"),
2004 ],
2005 "This is IPAR(13) in Pardiso manual.",
2006 )?;
2007 r.add_string_option("pardiso_redo_symbolic_fact_only_if_inertia_wrong", "Toggle for handling case when elements were perturbed by Pardiso.", "no", &[("no", "Always redo symbolic factorization when elements were perturbed"), ("yes", "Only redo symbolic factorization when elements were perturbed if also the inertia was wrong")], "")?;
2008 r.add_bool_option("pardiso_repeated_perturbation_means_singular", "Whether to assume that matrix is singular if elements were perturbed after recent symbolic factorization.", false, "")?;
2009 r.add_lower_bounded_integer_option(
2010 "pardiso_msglvl",
2011 "Pardiso message level",
2012 0,
2013 0,
2014 "This is MSGLVL in the Pardiso manual.",
2015 )?;
2016 r.add_bool_option("pardiso_skip_inertia_check", "Whether to pretend that inertia is correct.", false, "Setting this option to \"yes\" essentially disables inertia check. This option makes the algorithm non-robust and easily fail, but it might give some insight into the necessity of inertia control.")?;
2017 r.add_integer_option("pardiso_max_iterative_refinement_steps", "Limit on number of iterative refinement steps.", 0, "The solver does not perform more than the absolute value of this value steps of iterative refinement and stops the process if a satisfactory level of accuracy of the solution in terms of backward error is achieved. If negative, the accumulation of the residue uses extended precision real and complex data types. Perturbed pivots result in iterative refinement. The solver automatically performs two steps of iterative refinements when perturbed pivots are obtained during the numerical factorization and this option is set to 0.")?;
2018 r.add_string_option(
2019 "pardiso_order",
2020 "Controls the fill-in reduction ordering algorithm for the input matrix.",
2021 "metis",
2022 &[
2023 ("amd", "minimum degree algorithm"),
2024 ("one", ""),
2025 ("metis", "MeTiS nested dissection algorithm"),
2026 (
2027 "pmetis",
2028 "parallel (OpenMP) version of MeTiS nested dissection algorithm",
2029 ),
2030 ("four", ""),
2031 ("five", ""),
2032 ],
2033 "",
2034 )?;
2035 r.add_lower_bounded_integer_option(
2036 "pardiso_max_iter",
2037 "Maximum number of Krylov-Subspace Iteration",
2038 1,
2039 500,
2040 "DPARM(1)",
2041 )?;
2042 r.add_bounded_number_option(
2043 "pardiso_iter_relative_tol",
2044 "Relative Residual Convergence",
2045 0.0,
2046 true,
2047 1.0,
2048 true,
2049 1e-6,
2050 "DPARM(2)",
2051 )?;
2052 r.add_lower_bounded_integer_option(
2053 "pardiso_iter_coarse_size",
2054 "Maximum Size of Coarse Grid Matrix",
2055 1,
2056 5000,
2057 "DPARM(3)",
2058 )?;
2059 r.add_lower_bounded_integer_option(
2060 "pardiso_iter_max_levels",
2061 "Maximum Size of Grid Levels",
2062 1,
2063 10,
2064 "DPARM(4)",
2065 )?;
2066 r.add_bounded_number_option(
2067 "pardiso_iter_dropping_factor",
2068 "dropping value for incomplete factor",
2069 0.0,
2070 true,
2071 1.0,
2072 true,
2073 0.5,
2074 "DPARM(5)",
2075 )?;
2076 r.add_bounded_number_option(
2077 "pardiso_iter_dropping_schur",
2078 "dropping value for sparsify schur complement factor",
2079 0.0,
2080 true,
2081 1.0,
2082 true,
2083 1e-1,
2084 "DPARM(6)",
2085 )?;
2086 r.add_lower_bounded_integer_option(
2087 "pardiso_iter_max_row_fill",
2088 "max fill for each row",
2089 1,
2090 10000000,
2091 "DPARM(7)",
2092 )?;
2093 r.add_lower_bounded_number_option(
2094 "pardiso_iter_inverse_norm_factor",
2095 "",
2096 1.0,
2097 true,
2098 5000000.0,
2099 "DPARM(8)",
2100 )?;
2101 r.add_bool_option(
2102 "pardiso_iterative",
2103 "Switch for iterative solver in Pardiso library",
2104 false,
2105 "",
2106 )?;
2107 r.add_lower_bounded_integer_option(
2108 "pardiso_max_droptol_corrections",
2109 "Maximal number of decreases of drop tolerance during one solve.",
2110 1,
2111 4,
2112 "This is relevant only for iterative Pardiso options.",
2113 )?;
2114
2115 r.set_registering_category("Pardiso (MKL) Linear Solver");
2117 r.add_string_option(
2118 "pardisomkl_matching_strategy",
2119 "Matching strategy to be used by Pardiso",
2120 "complete+2x2",
2121 &[
2122 ("complete", "Match complete (IPAR(13)=1)"),
2123 ("complete+2x2", "Match complete+2x2 (IPAR(13)=2)"),
2124 ("constraints", "Match constraints (IPAR(13)=3)"),
2125 ],
2126 "This is IPAR(13) in Pardiso manual.",
2127 )?;
2128 r.add_string_option("pardisomkl_redo_symbolic_fact_only_if_inertia_wrong", "Toggle for handling case when elements were perturbed by Pardiso.", "no", &[("no", "Always redo symbolic factorization when elements were perturbed"), ("yes", "Only redo symbolic factorization when elements were perturbed if also the inertia was wrong")], "")?;
2129 r.add_bool_option("pardisomkl_repeated_perturbation_means_singular", "Whether to assume that matrix is singular if elements were perturbed after recent symbolic factorization.", false, "")?;
2130 r.add_lower_bounded_integer_option(
2131 "pardisomkl_msglvl",
2132 "Pardiso message level",
2133 0,
2134 0,
2135 "This is MSGLVL in the Pardiso manual.",
2136 )?;
2137 r.add_bool_option("pardisomkl_skip_inertia_check", "Whether to pretend that inertia is correct.", false, "Setting this option to \"yes\" essentially disables inertia check. This option makes the algorithm non-robust and easily fail, but it might give some insight into the necessity of inertia control.")?;
2138 r.add_integer_option("pardisomkl_max_iterative_refinement_steps", "Limit on number of iterative refinement steps.", 1, "The solver does not perform more than the absolute value of this value steps of iterative refinement and stops the process if a satisfactory level of accuracy of the solution in terms of backward error is achieved. If negative, the accumulation of the residue uses extended precision real and complex data types. Perturbed pivots result in iterative refinement. The solver automatically performs two steps of iterative refinements when perturbed pivots are obtained during the numerical factorization and this option is set to 0.")?;
2139 r.add_string_option(
2140 "pardisomkl_order",
2141 "Controls the fill-in reduction ordering algorithm for the input matrix.",
2142 "metis",
2143 &[
2144 ("amd", "minimum degree algorithm"),
2145 ("one", "undocumented"),
2146 ("metis", "MeTiS nested dissection algorithm"),
2147 (
2148 "pmetis",
2149 "parallel (OpenMP) version of MeTiS nested dissection algorithm",
2150 ),
2151 ],
2152 "",
2153 )?;
2154
2155 r.set_registering_category("SPRAL Linear Solver");
2157 r.add_lower_bounded_integer_option(
2158 "spral_cpu_block_size",
2159 "CPU Parallelization Block Size",
2160 1,
2161 256,
2162 "Block size to use for parallelization of large nodes on CPU resources.",
2163 )?;
2164 r.add_lower_bounded_number_option(
2165 "spral_gpu_perf_coeff",
2166 "GPU Performance Coefficient",
2167 0.0,
2168 true,
2169 1.0,
2170 "How many times faster a GPU is than a CPU at factoring a subtree.",
2171 )?;
2172 r.add_string_option(
2173 "spral_ignore_numa",
2174 "Non-uniform memory access (NUMA) region setting.",
2175 "yes",
2176 &[
2177 (
2178 "no",
2179 "Do not treat CPUs and GPUs as belonging to a single NUMA region.",
2180 ),
2181 (
2182 "yes",
2183 "Treat CPUs and GPUs as belonging to a single NUMA region.",
2184 ),
2185 ],
2186 "",
2187 )?;
2188 r.add_lower_bounded_number_option(
2189 "spral_max_load_inbalance",
2190 "Maximum Permissible Load",
2191 1.0,
2192 true,
2193 1.2,
2194 "Maximum permissible load inbalance for leaf subtree allocations.",
2195 )?;
2196 r.add_lower_bounded_number_option(
2197 "spral_min_gpu_work",
2198 "Minimum GPU Work",
2199 0.0,
2200 false,
2201 5.0e9,
2202 "Minimum number of FLOPS in subtree before scheduling on GPU.",
2203 )?;
2204 r.add_lower_bounded_integer_option("spral_nemin", "Node Amalgamation Parameter", 1, 32, "Two nodes in the elimination tree are merged if the result has fewer than spral_nemin variables.")?;
2205 r.add_string_option(
2206 "spral_order",
2207 "Controls type of ordering used by SPRAL",
2208 "matching",
2209 &[
2210 ("metis", "Use METIS with default settings."),
2211 ("matching", "Use matching-based elimination ordering."),
2212 ],
2213 "",
2214 )?;
2215 r.add_string_option(
2216 "spral_pivot_method",
2217 "Specifies strategy for scaling in SPRAL linear solver.",
2218 "block",
2219 &[
2220 ("aggressive", "Aggressive a posteori pivoting."),
2221 ("block", "Block a posteori pivoting."),
2222 ("threshold", "Threshold partial pivoting (not parallel)."),
2223 ],
2224 "",
2225 )?;
2226 r.add_integer_option("spral_print_level", "Print level for the linear solver SPRAL", -1, "<0: no printing, 0: errors and warning messages, 1: limited diagnostics, >1: additional diagnostics")?;
2227 r.add_string_option(
2228 "spral_scaling",
2229 "Specifies strategy for scaling in SPRAL linear solver.",
2230 "matching",
2231 &[
2232 ("none", "Do not scale the linear system matrix."),
2233 ("mc64", "Scale using weighted bipartite matching (MC64)."),
2234 ("auction", "Scale using the auction algorithm."),
2235 ("matching", "Scale using the matching-based ordering."),
2236 (
2237 "ruiz",
2238 "Scale using the norm-equilibration algorithm of Ruiz (MC77).",
2239 ),
2240 (
2241 "dynamic",
2242 "Dynamically select scaling according to switch options.",
2243 ),
2244 ],
2245 "",
2246 )?;
2247 r.add_string_option("spral_scaling_1", "First scaling strategy.", "matching", &[("none", "Do not scale the linear system matrix."), ("mc64", "Scale using weighted bipartite matching (MC64)."), ("auction", "Scale using the auction algorithm."), ("matching", "Scale using the matching-based ordering."), ("ruiz", "Scale using the norm-equilibration algorithm of Ruiz (MC77).")], "If spral_scaling = dynamic, this scaling is used according to the trigger spral_switch_1. If spral_switch_2 is triggered, it is disabled.")?;
2248 r.add_string_option("spral_scaling_2", "Second scaling strategy.", "mc64", &[("none", "Do not scale the linear system matrix."), ("mc64", "Scale using weighted bipartite matching (MC64)."), ("auction", "Scale using the auction algorithm."), ("matching", "Scale using the matching-based ordering."), ("ruiz", "Scale using the norm-equilibration algorithm of Ruiz (MC77).")], "If spral_scaling = dynamic, this scaling is used according to the trigger spral_switch_2. If spral_switch_3 is triggered, it is disabled.")?;
2249 r.add_string_option(
2250 "spral_scaling_3",
2251 "Third scaling strategy.",
2252 "none",
2253 &[
2254 ("none", "Do not scale the linear system matrix."),
2255 ("mc64", "Scale using weighted bipartite matching (MC64)."),
2256 ("auction", "Scale using the auction algorithm."),
2257 ("matching", "Scale using the matching-based ordering."),
2258 (
2259 "ruiz",
2260 "Scale using the norm-equilibration algorithm of Ruiz (MC77).",
2261 ),
2262 ],
2263 "If spral_scaling = dynamic, this scaling is used according to the trigger spral_switch_3.",
2264 )?;
2265 r.add_string_option("spral_switch_1", "First switch, determining when spral_scaling_1 is enabled.", "at_start", &[("never", "Scaling is never enabled."), ("at_start", "Scaling is used from the very start."), ("at_start_reuse", "Scaling is used on the first iteration, then reused thereafter."), ("on_demand", "Scaling is used when iterative refinement has failed."), ("on_demand_reuse", "As on_demand, but scaling from previous iteration is reused."), ("high_delay", "Scaling is used after more than 0.05*n delays are present."), ("high_delay_reuse", "Scaling is used only when previous iteration created more that 0.05*n additional delays; otherwise, reuse scaling from the previous iteration."), ("od_hd", "Combination of on_demand and high_delay."), ("od_hd_reuse", "Combination of on_demand_reuse and high_delay_reuse")], "If spral_scaling = dynamic, spral_scaling_1 is enabled according to this condition. If spral_switch_2 occurs, this option is henceforth ignored.")?;
2266 r.add_string_option("spral_switch_2", "Second switch, determining when spral_scaling_2 is enabled.", "on_demand", &[("never", "Scaling is never enabled."), ("at_start", "Scaling is used from the very start."), ("at_start_reuse", "Scaling is used on the first iteration, then reused thereafter."), ("on_demand", "Scaling is used when iterative refinement has failed."), ("on_demand_reuse", "As on_demand, but scaling from previous iteration is reused."), ("high_delay", "Scaling is used after more than 0.05*n delays are present."), ("high_delay_reuse", "Scaling is used only when previous iteration created more that 0.05*n additional delays; otherwise, reuse scaling from the previous iteration."), ("od_hd", "Combination of on_demand and high_delay."), ("od_hd_reuse", "Combination of on_demand_reuse and high_delay_reuse")], "If spral_scaling = dynamic, spral_scaling_2 is enabled according to this condition. If spral_switch_3 occurs, this option is henceforth ignored.")?;
2267 r.add_string_option("spral_switch_3", "Third switch, determining when spral_scaling_3 is enabled.", "never", &[("never", "Scaling is never enabled."), ("at_start", "Scaling is used from the very start."), ("at_start_reuse", "Scaling is used on the first iteration, then reused thereafter."), ("on_demand", "Scaling is used when iterative refinement has failed."), ("on_demand_reuse", "As on_demand, but scaling from previous iteration is reused."), ("high_delay", "Scaling is used after more than 0.05*n delays are present."), ("high_delay_reuse", "Scaling is used only when previous iteration created more that 0.05*n additional delays; otherwise, reuse scaling from the previous iteration."), ("od_hd", "Combination of on_demand and high_delay."), ("od_hd_reuse", "Combination of on_demand_reuse and high_delay_reuse")], "If spral_scaling = dynamic, spral_scaling_3 is enabled according to this condition.")?;
2268 r.add_lower_bounded_number_option(
2269 "spral_small",
2270 "Zero Pivot Threshold",
2271 0.0,
2272 true,
2273 1.0e-20,
2274 "Any pivot less than spral_small is treated as zero.",
2275 )?;
2276 r.add_lower_bounded_number_option(
2277 "spral_small_subtree_threshold",
2278 "Small Subtree Threshold",
2279 0.0,
2280 true,
2281 4.0e6,
2282 "Maximum number of FLOPS in a subtree treated as a single task.",
2283 )?;
2284 r.add_bounded_number_option(
2285 "spral_u",
2286 "Pivoting Threshold",
2287 0.0,
2288 true,
2289 0.5,
2290 false,
2291 1.0e-8,
2292 "Relative pivot threshold used in symmetric indefinite case.",
2293 )?;
2294 r.add_bounded_number_option(
2295 "spral_umax",
2296 "Maximum Pivoting Threshold",
2297 0.0,
2298 true,
2299 0.5,
2300 false,
2301 1.0e-4,
2302 "See SPRAL documentation.",
2303 )?;
2304 r.add_bool_option("spral_use_gpu", "Specifies whether or not graphics processing units (GPUs) are used by the SPRAL linear solver if present.", true, "")?;
2305
2306 r.set_registering_category("WSMP Linear Solver");
2308 r.add_integer_option(
2309 "wsmp_num_threads",
2310 "Number of threads to be used in WSMP",
2311 1,
2312 "",
2313 )?;
2314 r.add_bounded_integer_option(
2315 "wsmp_ordering_option",
2316 "Determines how ordering is done in WSMP",
2317 -2,
2318 3,
2319 1,
2320 "This corresponds to the value of WSSMP's IPARM(16).",
2321 )?;
2322 r.add_bounded_integer_option(
2323 "wsmp_ordering_option2",
2324 "Determines how ordering is done in WSMP",
2325 0,
2326 3,
2327 1,
2328 "This corresponds to the value of WSSMP's IPARM(20).",
2329 )?;
2330 r.add_bounded_number_option(
2331 "wsmp_pivtol",
2332 "Pivot tolerance for the linear solver WSMP.",
2333 0.0,
2334 true,
2335 1.0,
2336 true,
2337 1e-4,
2338 "A smaller number pivots for sparsity, a larger number pivots for stability.",
2339 )?;
2340 r.add_bounded_number_option("wsmp_pivtolmax", "Maximum pivot tolerance for the linear solver WSMP.", 0.0, true, 1.0, true, 1e-1, "Ipopt may increase pivtol as high as pivtolmax to get a more accurate solution to the linear system.")?;
2341 r.add_bounded_integer_option(
2342 "wsmp_scaling",
2343 "Determines how the matrix is scaled by WSMP.",
2344 0,
2345 3,
2346 0,
2347 "This corresponds to the value of WSSMP's IPARM(10).",
2348 )?;
2349 r.add_bounded_number_option("wsmp_singularity_threshold", "WSMP's singularity threshold.", 0.0, true, 1.0, true, 1e-18, "WSMP's DPARM(10) parameter. The smaller this value the less likely a matrix is declared singular.")?;
2350 r.add_lower_bounded_integer_option("wsmp_write_matrix_iteration", "Iteration in which the matrices are written to files.", -1, -1, "If non-negative, this option determines the iteration in which all matrices given to WSMP are written to files.")?;
2351 r.add_bool_option("wsmp_skip_inertia_check", "Whether to always pretend that inertia is correct.", false, "Setting this option to \"yes\" essentially disables inertia check. This option makes the algorithm non-robust and easily fail, but it might give some insight into the necessity of inertia control.")?;
2352 r.add_string_option("wsmp_no_pivoting", "Whether to use the static pivoting option of WSMP.", "no", &[("no", "use the regular version"), ("yes", "use static pivoting")], "Setting this option to \"yes\" means that WSMP is instructed not to do pivoting. This works only in certain situations (when the Hessian block is known to be positive definite or when we are using L-BFGS). It can also lead to a lot of fill-in.")?;
2353
2354 r.set_registering_category("WSMP Linear Solver");
2356 r.add_lower_bounded_integer_option(
2357 "wsmp_max_iter",
2358 "Maximal number of iterations in iterative WISMP",
2359 1,
2360 1000,
2361 "",
2362 )?;
2363 r.add_lower_bounded_number_option(
2364 "wsmp_inexact_droptol",
2365 "Drop tolerance for inexact factorization preconditioner in WISMP.",
2366 0.0,
2367 false,
2368 0.0,
2369 "DPARM(14) in WISMP",
2370 )?;
2371 r.add_lower_bounded_number_option(
2372 "wsmp_inexact_fillin_limit",
2373 "Fill-in limit for inexact factorization preconditioner in WISMP.",
2374 0.0,
2375 false,
2376 0.0,
2377 "DPARM(15) in WISMP",
2378 )?;
2379
2380 r.set_registering_category("MA28 Linear Solver");
2382 r.add_bounded_number_option(
2383 "ma28_pivtol",
2384 "Pivot tolerance for linear solver MA28.",
2385 0.0,
2386 true,
2387 1.0,
2388 false,
2389 0.01,
2390 "",
2391 )?;
2392
2393 r.set_registering_category("L1 Exact Penalty-Barrier Wrapper");
2400 r.add_bool_option(
2401 "l1_exact_penalty_barrier",
2402 "Wrap the NLP in the Thierry-Biegler ℓ₁ penalty-barrier reformulation before solving.",
2403 false,
2404 "When set, every equality row c_i(x)=g_i is rewritten as c_i(x)-p_i+n_i=g_i with non-negative slack pair (p_i, n_i), and the objective is augmented by ρ·Σ(p+n). The augmented NLP automatically satisfies LICQ on the slack variables, which makes the standard interior-point machinery handle degenerate / MPCC-like cases that the stock filter line search thrashes on. Default off; the solve trajectory is byte-identical to pre-l1 pounce when off.",
2405 )?;
2406 r.add_lower_bounded_number_option(
2407 "l1_penalty_init",
2408 "Initial value of the penalty weight ρ in the ℓ₁ wrapper.",
2409 0.0,
2410 true,
2411 1.0,
2412 "Initial ρ. After each inner solve the Byrd-Nocedal-Waltz steering rule escalates ρ until the slacks collapse or the maximum is reached.",
2413 )?;
2414 r.add_lower_bounded_number_option(
2415 "l1_penalty_max",
2416 "Upper cap on the ℓ₁ penalty weight ρ.",
2417 0.0,
2418 true,
2419 1.0e6,
2420 "BNW steering increases ρ but never above this cap. If the cap is reached with non-collapsed slacks, the original problem is locally infeasible at the returned ℓ₁-best point and the status is upgraded to LocalInfeasibility / Infeasible_Problem_Detected.",
2421 )?;
2422 r.add_lower_bounded_number_option(
2423 "l1_penalty_increase_factor",
2424 "Geometric ρ-escalation factor.",
2425 1.0,
2426 false,
2427 8.0,
2428 "Multiplicative floor on ρ growth between outer iterations. The BNW rule sets ρ_new = max(ρ·factor, τ·‖y_eq‖∞ + ε), so factor sets the slowest sustained growth rate.",
2429 )?;
2430 r.add_lower_bounded_integer_option(
2431 "l1_penalty_max_outer_iter",
2432 "Maximum number of outer ρ-escalation steps.",
2433 1,
2434 8,
2435 "Caps the number of inner-solve cycles the wrapper runs. Slack-collapse termination is the usual exit; this is a safety bound for pathological problems where the BNW rule plateaus.",
2436 )?;
2437 r.add_lower_bounded_number_option(
2438 "l1_slack_tol",
2439 "Fallback slack-sum tolerance, used only when the model's own feasibility cannot be measured.",
2440 0.0,
2441 true,
2442 1.0e-6,
2443 "SINCE gh#794 THIS IS A FALLBACK, NOT THE TEST. The wrapper's outer loop and its honest-infeasibility upgrade both judge the ORIGINAL model's feasibility at the returned point -- the violation of the user's own rows and bounds -- against the tolerances the caller set (tol for the strict verdict, acceptable_tol for Solved_To_Acceptable_Level), scale-relative. Sigma(p+n) was the wrong quantity twice over: the violation of equality row i is |p_i - n_i|, not p_i + n_i, and at the barrier's interior both slacks stay positive where their difference is zero; and 1e-6 is four orders looser than a tol = 1e-8 solve asked for, so a violation the strict gate would refuse on the unwrapped problem read as \"the constraints are satisfied\". Measured: ralph1 (benchmarks/mpcc) returned Solve_Succeeded at a point violating its one equality row by 2.5e-07 while reporting final_constr_viol = 9.6e-15, the augmented problem's residual, with no field in the result disclosing it. Sigma(p+n) keeps its other job unchanged -- it is the Byrd-Nocedal-Waltz steering signal for rho escalation, which is what it is the right quantity for -- and this tolerance still applies on a model whose rows cannot be evaluated at the returned point.",
2444 )?;
2445 r.add_lower_bounded_number_option(
2446 "l1_steering_factor",
2447 "BNW steering factor τ relating ρ to ‖y_eq‖∞.",
2448 0.0,
2449 true,
2450 10.0,
2451 "BNW chooses ρ_new = max(ρ·factor, τ·‖y_eq‖∞ + ε). τ>1 ensures the augmented (p,n) bound multipliers z_p = ρ-y_eq, z_n = ρ+y_eq remain strictly positive and bounded away from 0, which is the property the slack-collapse argument relies on.",
2452 )?;
2453 r.add_bool_option(
2454 "l1_fallback_on_restoration_failure",
2455 "Auto-retry with the ℓ₁ wrapper when the standard solve hits a non-success terminal status.",
2456 false,
2457 "When set, optimize_tnlp first runs the standard solve. If it terminates in Restoration_Failed / Infeasible_Problem_Detected / Solved_To_Acceptable_Level / Maximum_Iterations_Exceeded / Not_Enough_Degrees_Of_Freedom, the wrapper is enabled and the solve is repeated. The retry's status replaces the original ONLY if the retry returns Solve_Succeeded (promotion); otherwise the original status is returned. NOTE: the user TNLP's finalize_solution is called once per attempt, so when the retry doesn't promote the user's captured fields hold the retry's iterate (the ℓ₁-best least-infeasible point) even though the returned status is the original's — pounce#10 Phase-4 note.",
2458 )?;
2459 r.add_bool_option(
2460 "mu_strategy_fallback",
2461 "Auto-retry with the opposite mu_strategy when the standard solve stalls short of optimal.",
2462 true,
2463 "When set, optimize_tnlp first runs the standard solve. If it terminates in Solved_To_Acceptable_Level or Maximum_Iterations_Exceeded — the stall signatures where the dual infeasibility parks above tol while constraint violation and complementarity are already converged — the mu_strategy is flipped (adaptive↔monotone) and the solve is repeated once. The retry's status replaces the original ONLY if the retry returns Solve_Succeeded (promotion); otherwise the original status is returned. Motivated by pounce#138: on several princetonlib instances adaptive under-converges the dual term while monotone drives it under tol (maxcut, price recover from acceptable-level; fermat2_vareps recovers from a 3000-iter stall) — and the converse holds on other models (globallib/ex8_3_1), so neither strategy is globally correct and a one-shot fallback recovers the stalled cases without a blanket flag flip. NOTE: like the ℓ₁ fallback, the user TNLP's finalize_solution runs once per attempt, so when the retry doesn't promote the captured fields hold the retry's iterate.DEFAULT FLIPPED TO ON in pounce#748. It had been off since pounce#138 registered it, which left the recovery available only to users who knew to ask for it. Two things settled it. First, pounce#746 made hessian_approximation=limited-memory default to adaptive (matching IpAlgBuilder.cpp:1059) -- a net win on the 47-problem mittelmann corpus, Solve_Succeeded 28 -> 31 with three Error_In_Step_Computation exits and a false Infeasible_Problem_Detected eliminated -- but it cost dirichlet120, which monotone solves in 176 iterations and adaptive stalls on for 3000. Ipopt stalls there identically, so that model is not a POUNCE defect in the strategy; it is a case where neither schedule dominates, which is exactly the premise this option was registered on. WHAT THE DEFAULT-ON RETRY TRIGGERS ON IS NARROWER THAN THE OPT-IN, AND THE NARROWING IS CONDITIONAL (pounce#757): Maximum_Iterations_Exceeded always, and Solved_To_Acceptable_Level only while the caller has not named an option that defines what termination means. An explicit mu_strategy_fallback=yes keeps both statuses unconditionally, as pounce#138 registered them. The list of termination-policy options is Application::TERMINATION_POLICY_OPTIONS: tol and the component tolerances, the acceptable_* family, kkt_fidelity_tol, the certificate-mask and noise-floor kappas, the divergence and infeasibility streaks, and the restoration-decline pair. Options that only move the starting point are not in it. Solved_To_Acceptable_Level is not a failure -- it is a converged answer at the acceptable tolerance -- and pounce#748 took it out of the default trigger because retrying it unconditionally is wrong three ways: it doubles the cost of a solve that already succeeded; it launders downgrades the caller induced deliberately, so the signal those options exist to produce never arrives; and because the retry returns the other run's POINT and not just its status, it can hand back a different local solution (autocorr_bern55-06 with the dual-divergence guard on: -2304.0000278 became -2320.0000298). Two of those three are properties of a caller-MODIFIED configuration, not of the exit status: all five test targets the wide trigger broke arm a non-default termination option to provoke the downgrade the retry then erased -- optimize_hs71 (tight kkt_fidelity_tol), masked_certificate_fuzz (certificate veto), issue_250_dual_guard_never_worse (dual_diverging_streak), issue_534_resto_decline_progress (resto_decline_deferrals), issue_616_ls_init_downgrades. Deferring to those options instead of to the status keeps both objections and gives the recovery back to the stock configuration, where nobody asked for an acceptable-level answer. The third objection, cost, stands and is the accepted price: a stock run that ends acceptable now pays for a second solve. What that buys is pounce#757's motivating case, cho_parmest, a 12-parameter kinetic fit that stalls at an overall error of 1.05e-08 against a 1e-8 tolerance -- a 5 percent miss, entirely in the dual term, with the iterate frozen at steps near 1e-12 while inf_du swings by a factor of ten on evaluation noise -- and that adaptive certifies in 20 iterations at 8.87e-09. It is the registered signature exactly: dual parked above tol with constraint violation at 7.3e-13 and complementarity at 9.1e-10 already converged. With this trigger the 71-fixture sweep moves three lines of 142, both legs, all improvements and no regressions (exact csfi2 Solved_To_Acceptable_Level/35 -> Solve_Succeeded/21, lbfgs pooling_rt2stp Solved_To_Acceptable_Level/362 -> Solve_Succeeded/295, lbfgs eigenb2 69 -> 41). Under pounce#748's status-only narrowing the same sweep moved nothing at all; those three lines are what it gave up. By the promote-only-on-Solve_Succeeded rule the status is never worse than it would have been. One limit is deliberate: it CANNOT rescue a Maximum_CpuTime_Exceeded exit and does not try, because the budget a retry needs is precisely the budget already spent. nql180 regresses that way under pounce#746 and is not recovered here; see adaptive_mu_max_free_returns and pounce#749. The default is conditional in one respect: absent an explicit setting the retry is on ONLY while the user has not named a mu_strategy themselves. Retrying under the other schedule recovers a solve that stalled on a strategy POUNCE chose; it is not licence to override a strategy the caller chose, and without that condition the flipped default would silently contaminate every controlled comparison that pins mu_strategy on purpose. An explicit mu_strategy_fallback=yes overrides the condition and retries regardless. The motivating case is unaffected, since dirichlet120 stalls under the limited-memory substitution, which by definition only occurs when mu_strategy is unset. Set to no to restore the previous default and upstream's single-solve behaviour. ONE CASE STANDS THE FLIP DOWN (pounce#857): a Maximum_Iterations_Exceeded exit whose solve escalated the linear-solver quality at least once (quality_escalations >= 1) skips the flip entirely, because feral_increase_quality_retry -- rung 4 of the second-opinion ladder -- is about to re-solve that same run with the escalation off, and that is the hypothesis the measurement supports. The flip is blind: it varies the barrier schedule. FERAL's escalation reroutes which pivots are taken and never steps back down, so flipping mu on top of it holds the knob that is implicated and varies the one that is not. Measured on square_flowsheet_resto's limited-memory leg: the base solve runs to the 3000 cap with 25 escalations, the flip then runs a second full 3000 and escalates 25 times again for no gain, and rung 4 converges the model in 178 -- 6178 real iterations to reach an answer 3178 of them reach. mu_strategy=adaptive alone still gives 3000/25 on that leg and feral_increase_quality=no gives 178 under either schedule, so the escalation is the operative variable and the schedule is not. The stand-down is scoped to Maximum_Iterations_Exceeded, the one status rung 4 opens on: a Solved_To_Acceptable_Level exit opens no escalation rung, so declining there would drop a retry with nothing in its place. It is gated on feral_increase_quality_retry rather than on this option, so feral_increase_quality_retry=no restores the pre-857 behaviour on both sides at once -- no rung 4 and no stand-down. Note that the fallback cannot instead fold the escalation off INTO its own retry: the FERAL backend factory is minted from an options snapshot the caller takes before solve() runs, so writing feral_increase_quality from inside the fallback never reaches the retry's linear solver, while mu_strategy is read per-solve and does.",
2464 )?;
2465
2466 r.add_bool_option(
2467 "dual_divergence_retry",
2468 "Re-solve from scratch with perturb_always_cd when the primal settles while the multipliers run away.",
2469 true,
2470 "gh#884. On an MPCC lowered through an exact complementarity product G*H = 0, a pair that is BIACTIVE at the solution -- both G and H zero -- leaves that row's gradient H*grad(G) + G*grad(H) identically zero. The row is still present, so its multiplier is arbitrary rather than nonexistent, and the interior-point method drives it to infinity while the primal iterate sits on the answer. The convergence verdict is reached on an s_d-normalised aggregate and s_d grows with the mean multiplier magnitude, so the aggregate reads clean while the residual in the model's own units does not: MacMPEC's qpec_small under the ncp_eq/prod_eq lowering reported Solved_To_Acceptable_Level at an UNSCALED dual infeasibility of 7.9e+04. When set, optimize_tnlp runs the standard solve, and if that solve was observed to settle its primal while its multipliers diverged, throws the iterate away and solves once more from iteration 0 with perturb_always_cd=yes. WHAT THE DETECTOR IS, AND WHY IT IS A CONJUNCTION AT ONE ITERATE: the algorithm sets a sticky flag when, at one and the same iterate, (a) the primal is converged (inf_pr <= dual_divergence_retry_primal_tol, 1e-8), (b) the step is at zero on a scale-relative measure (max_i |d_i| / (1 + |x_i|) <= dual_divergence_retry_step_tol), and (c) the UNSCALED dual infeasibility is at or above dual_divergence_retry_du_floor. Requiring all three at the same iterate is what makes this the discriminator gh#884 asks for -- 'converged primal, unbounded multiplier' as against 'diverging iterate' -- and a large multiplier on a small gradient cannot satisfy it, because (c) is measured in the model's own units where the multiplier is not divided back out. WHY A RETRY AND NOT A GATE: four other shapes were measured and rejected, and dev-notes/mpcc-biactive-dual-divergence.md records all four with numbers -- a Hessian sparsity hypothesis (refuted), engaging delta_c in flight (too late by construction: by the time the runaway is visible the iterate is unrecoverable), flipping perturb_always_cd on globally (it trades an honest failure for a silent wrong answer, measured below), and putting a dual ceiling on the acceptable-level gate (on an unconstrained model grad(L) IS grad(f), so the ratio such a ceiling tests is identically 1 and the bound collapses to dual_inf_tol -- a nine-order tightening of acceptable_dual_inf_tol for the whole unconstrained class). 'Too late to recover this iterate' is not 'too late to act' when the action is to stop using it. THE DETECTOR IS THE SAFETY BARRIER AND THAT IS THE POINT: the remedy the retry reaches for, perturb_always_cd=yes, is measured to return a wrong answer reported as success on MacMPEC's ralph1 -- Solve_Succeeded at f = -2.71e-5, below the model's f* = 0 -- so a false positive does not merely cost a doubled solve, it routes the model into a configuration known to lie. What keeps ralph1 out is condition (b): qpec_small's scale-relative step settles to 4.3e-8 while ralph1's bottoms out at 7.2e-3, five orders apart, and the default threshold sits between them. THE PROMOTION GATE IS THE SECOND BARRIER: the retry's verdict replaces the base one only if the retry returns Solve_Succeeded, its claimed success is real IN THE MODEL'S OWN UNITS (unscaled KKT error and unscaled constraint violation both at or below acceptable_tol), its unscaled KKT error is STRICTLY better than the base attempt's, AND its ANSWER is admissible next to the base attempt's. That last conjunct is a separate barrier and not a restatement of the others: everything before it ranks the two attempts on their CERTIFICATES, and a certificate cannot tell you which of two feasible points a caller should receive. Two rules, both skipped when the base attempt is not itself feasible within acceptable_tol: (1) the retry may not return a strictly worse objective -- measured on 400 random QPECs under the prod_eq lowering, three promotions handed back a worse FEASIBLE point, worst case -13.0057 against the base attempt's, both independently verified feasible; and (2) an objective IMPROVEMENT may not be bought with primal slack, i.e. a retry that reports a better objective at a larger constraint violation than the base attempt's is refused. Rule 2 is the correctness half: MacMPEC's scholtes4 under this lowering has f* = 0 exactly, and the retry promoted f = -6.61e-05 -- a value no feasible point of the model attains -- by moving the complementarity row from 2.07e-25 to 1.09e-09, and reported Optimal Solution Found. Both comparisons use acceptable_tol scaled by max(1, |base objective|); the smallest move that must be ADMITTED is the reproducer's own 5.8e-11 and the smallest that must be REFUSED is 0.198, four and five orders either side of it. Promoting on the status alone would reproduce the defect one attempt later, since the base attempt's defect was precisely a status its own unscaled residual contradicts. When the retry does not promote, the base attempt's status, point, statistics and last trace row are all restored, by the same three-sink floor mu_strategy_fallback uses (pounce#870). WHICH BASE VERDICTS OPEN THE RETRY: Solved_To_Acceptable_Level, which is gh#884 verbatim, and Restoration_Failed, which is the same defect one step earlier and is where the reproducer's TNLP path lands. Never Solve_Succeeded, which is already the best verdict available and whose certificate has already been checked unscaled, and never the generic exhaustion exits Error_In_Step_Computation and Maximum_Iterations_Exceeded, which every hard model reaches for every reason -- retrying those is not repairing a runaway, it is trying again harder, which mu_strategy_fallback and the second-opinion ladder already are. AND THE ANSWER MUST STILL BE A CONVERGED POINT WITH A RUNAWAY MULTIPLIER: the detector fires on an ITERATE, and a solve that passes through a settled point with a diverged multiplier and then works its way back down has nothing left for perturb_always_cd to repair. This is tested as a dominance RATIO and, since the ratio is scale-free and so cannot say the residual is large, also against the same dual_divergence_retry_du_floor the detector's third conjunct uses -- without which an answer converged to 1e-30 primal with a dual residual of 4.4e-01 passed as a runaway, which it is not by any reading of this issue; that floor alone removes 7 of the 68 QPEC-family promotions. What gh#884's defect looks like in the ANSWER is a point converged EXCEPT that one multiplier ran away -- the primal is exact, complementarity is met, and the entire residual is dual infeasibility -- so the retry is spent only when the reported answer's unscaled constraint violation and unscaled complementarity are both at or below 1e-6 times its unscaled dual infeasibility. Measured, that ratio is 1.5e-14 for the reproducer through the .nl file and 8.7e-15 through the TNLP, against 4.7e-2 for deb7 on the L-BFGS leg with limited_memory_ls_failure_restarts=1, whose complementarity is five percent of its own KKT error -- that answer is not a converged point with a runaway multiplier, it is an unconverged point (gh#887). The gate reads only the answer being reported, as a ratio WITHIN it, so it carries no units and cannot depend on which attempt fired or on how a platform rounded. That property is the point, and two gates that lacked it were measured and dropped: an absolute floor on the reported residual declines deb7 by a ONE PERCENT margin, which is a coincidence rather than a discriminator; and a comparison against the runaway the DETECTOR saw separates the same runs by five orders and still failed on CI, because it reads two numbers from a trajectory and deb7's is not portable -- the same invocation reaches objective 99.677 on macOS and 99.651 on Linux, with the detector reporting 9.2e5 on one attempt and 8.7e2 on another in the same run. Unlike mu_strategy_fallback's acceptable-level trigger this does NOT defer to the caller's termination-policy options: that deferral exists because the mu flip returns a different local solution and can launder a downgrade the caller induced deliberately. This retry was originally documented as unable to do the same, on the grounds that the promotion gate requires the promoted answer to satisfy the KKT conditions unscaled -- which does NOT follow, since any other KKT point satisfies them too, and measurement bore that out: 42 of 68 promotions on the QPEC family moved the objective materially, i.e. returned a different local solution. What actually bounds it is the answer-admissibility conjunct described above, which is why that conjunct exists rather than the deferral. COST: one extra solve on a run that satisfied the three-way detector, reached a scoped failure verdict, AND whose reported answer still has gh#884's shape -- which no fixture in the CLI corpus does at default options. Two statistics report what happened -- dual_divergence_signature (the detector fired) and dual_divergence_retry_promoted (the retry's answer was returned) -- both in the JSON solve report. Set to no to restore the pre-gh#884 behaviour outright: no detector, no second solve, and the base attempt's verdict returned unchanged.",
2471 )?;
2472 r.add_lower_bounded_number_option(
2473 "dual_divergence_retry_step_tol",
2474 "Scale-relative step below which the dual-divergence detector calls the primal iterate settled.",
2475 0.0,
2476 false,
2477 1e-5,
2478 "gh#884. Condition (b) of the dual_divergence_retry detector: max_i |d_i| / (1 + |x_i|) over the primal step, taken over the x and s blocks together, at an iterate whose primal is already converged. THE POPULATION BEHIND THE DEFAULT, measured on 87402274 and taken only while inf_pr <= 1e-8: qpec_small/prod_eq from the origin reaches 4.3e-8 through the TNLP and 8.6e-14 through the .nl file, while ralph1/direct from the origin bottoms out at 7.2e-3. Any threshold in (4.3e-8, 7.2e-3) separates them and 1e-5 is the geometric middle of that range. The separation is the whole safety argument for the feature: ralph1's limit point admits no sign-feasible multiplier, its honest verdict is a failure, and the remedy this retry reaches for is measured to report success below its f* -- so a threshold that let ralph1 through would turn an honest failure into a silent wrong answer. Setting this to 0 disables the detector without disabling the option, which is what the algorithm-level tests use to hold the trigger off one solve at a time; dual_divergence_retry=no is the kill switch for the whole feature.",
2479 )?;
2480 r.add_lower_bounded_number_option(
2481 "dual_divergence_retry_du_floor",
2482 "Unscaled dual infeasibility above which the dual-divergence detector calls the multipliers diverged.",
2483 0.0,
2484 false,
2485 1e2,
2486 "gh#884. Condition (c) of the dual_divergence_retry detector, and the one measured in the MODEL'S OWN UNITS rather than the s_d-normalised aggregate the convergence gate reads -- which is the whole point, since the aggregate is what hid the defect. THE POPULATION BEHIND THE DEFAULT: qpec_small's runaway reaches 7.9e+04 unscaled at the acceptable-level exit gh#884 filed and 7.8e+11 by the end of the run, ten orders above this floor. Against that, the largest unscaled dual infeasibility any fixture in the CLI corpus reaches at an acceptable-level exit is 2.4e-11 (mu_fallback_point_floor, the corpus's only acceptable-level exit), and the closest non-MPCC approach to the floor is eigena2's limited-memory leg at 37 -- which the floor excludes, and whose step measure of 7.9e-9 would otherwise have satisfied conditions (a) and (b). Raising this floor narrows the detector; lowering it toward dual_inf_tol widens it toward every ordinary dual stall, which is not what the retry is for and not a population the remedy was measured on.",
2487 )?;
2488
2489 register_sipopt_options(r)?;
2490
2491 Ok(())
2492}
2493
2494fn register_sipopt_options(r: &RegisteredOptions) -> Result<(), SolverException> {
2507 r.set_registering_category("sIPOPT");
2508 r.add_lower_bounded_integer_option(
2509 "n_sens_steps",
2510 "Number of sensitivity steps to perform per converged solve.",
2511 0,
2512 1,
2513 "Number of parameter perturbations to step through. Mirrors upstream `n_sens_steps` (SensApplication.cpp:60).",
2514 )?;
2515 r.add_bool_option(
2516 "compute_red_hessian",
2517 "Compute the reduced Hessian at the converged solution.",
2518 false,
2519 "When set, after the IPM converges pounce-sensitivity assembles `H_R = obj_scal · B K⁻¹ Bᵀ` with B selecting the free variables. Output is written to the user via the sIPOPT C ABI (Phase D follow-up). Mirrors upstream `compute_red_hessian` (SensApplication.cpp:73).",
2520 )?;
2521 r.add_bool_option(
2522 "run_sens",
2523 "Run the sensitivity step calc after convergence.",
2524 false,
2525 "When set, pounce-sensitivity computes a forward-sensitivity step for the parameter perturbation declared via TNLP suffixes. Mirrors upstream `run_sens` (SensApplication.cpp:80).",
2526 )?;
2527 r.add_bool_option(
2528 "sens_boundcheck",
2529 "Verify the sensitivity step does not violate bound multipliers.",
2530 false,
2531 "Mirrors upstream `sens_boundcheck` (SensApplication.cpp:63).",
2532 )?;
2533 r.add_lower_bounded_number_option(
2534 "sens_bound_eps",
2535 "Safety margin enforced when sens_boundcheck is set.",
2536 0.0,
2537 true,
2538 1.0e-3,
2539 "Mirrors upstream `sens_bound_eps` (SensApplication.cpp:67).",
2540 )?;
2541 r.add_lower_bounded_number_option(
2542 "sens_max_pdpert",
2543 "Maximum primal-dual perturbation accepted in the sensitivity step.",
2544 0.0,
2545 true,
2546 1.0e-3,
2547 "Mirrors upstream `sens_max_pdpert` (SensApplication.cpp:98).",
2548 )?;
2549 r.add_bool_option(
2550 "rh_eigendecomp",
2551 "Compute eigendecomposition of the reduced Hessian.",
2552 false,
2553 "Mirrors upstream `rh_eigendecomp` (SensApplication.cpp:106). Pounce ships the option key for ipopt.opt-compatibility; the eigendecomposition itself is a Phase-D follow-up.",
2554 )?;
2555 Ok(())
2556}