• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

LSDOlab / modopt / 24972342774

27 Apr 2026 01:29AM UTC coverage: 80.839% (-0.3%) from 81.106%
24972342774

push

github

anugrahjo
Add `verbosity` option to OpenSQP and its docpage.

11 of 67 new or added lines in 1 file covered. (16.42%)

3 existing lines in 1 file now uncovered.

6147 of 7604 relevant lines covered (80.84%)

0.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

63.54
/modopt/core/optimization_algorithms/opensqp.py
1
import numpy as np
1✔
2
import time
1✔
3
import warnings
1✔
4
from typing import Dict
1✔
5

6
from modopt import Optimizer
1✔
7
from modopt.line_search_algorithms import Minpack2LS
1✔
8
from modopt.merit_functions import AugmentedLagrangian
1✔
9
from modopt.approximate_hessians import BFGSScipy
1✔
10
from modopt import CSDLAlphaProblem
1✔
11

12
import scipy
1✔
13
# from packaging.version import Version
14
from scipy._lib._pep440 import Version
1✔
15

16

17
class OpenSQP(Optimizer):
1✔
18
    """
19
    A reconfigurable open-source Sequential Quadratic Programming (SQP) optimizer
20
    developed in modOpt for constrained nonlinear optimization.
21

22
    Parameters
23
    ----------
24
    problem : Problem or ProblemLite
25
        Object containing the problem to be solved.
26
    recording : bool, default=False
27
        If ``True``, record all outputs from the optimization.
28
        This needs to be enabled for hot-starting the same problem later,
29
        if the optimization is interrupted.
30
    out_dir : str, optional
31
        The directory to store all the output files generated from the optimization.
32
    hot_start_from : str, optional
33
        The record file from which to hot-start the optimization.
34
    hot_start_atol : float, default=0.
35
        The absolute tolerance check for the inputs 
36
        when reusing outputs from the hot-start record.
37
    hot_start_rtol : float, default=0.
38
        The relative tolerance check for the inputs 
39
        when reusing outputs from the hot-start record.
40
    visualize : list, default=[]
41
        The list of scalar variables to visualize during the optimization.
42
    keep_viz_open : bool, default=False
43
        If ``True``, keeps the visualization window open after the optimization is complete.
44
    turn_off_outputs : bool, default=False
45
        If ``True``, prevent modOpt from generating any output files.
46

47
    verbosity : {0, 1}, default=0
48
        If ``0``, suppresses all console outputs.
49
        If ``1``, prints various convergence measures to monitor optimization progress
50
        and diagnostic messages for debugging purposes.
51
    maxiter : int, default=1000
52
        Maximum number of major iterations.
53
    opt_tol : float, default=1e-7
54
        Optimality tolerance.
55
    feas_tol : float, default=1e-7
56
        Feasibility tolerance.
57
        Certifies convergence when the "scaled" maximum constraint violation 
58
        is less than this value.
59
    aqp_primal_feas_tol : float, default=1e-8
60
        Tolerance for the primal feasibility of the augmented QP subproblem.
61
    aqp_dual_feas_tol : float, default=1e-8
62
        Tolerance for the dual feasibility of the augmented QP subproblem.
63
    aqp_time_limit : float, default=5.0
64
        Time limit for the augmented QP solution in seconds.
65
    ls_min_step : float, default=1e-12
66
        Minimum step size for the line search.
67
    ls_max_step : float, default=1.
68
        Maximum step size for the line search.
69
    ls_maxiter : int, default=10
70
        Maximum number of iterations for the line search.
71
    ls_eta_a : float, default=1e-4
72
        Armijo (sufficient decrease condition) parameter for the line search.
73
    ls_eta_w : float, default=0.9
74
        Wolfe (curvature condition) parameter for the line search.
75
    ls_alpha_tol : float, default=1e-14
76
        Relative tolerance for an acceptable step in the line search.
77
    bfgs_init_lag_hess : np.ndarray, default=None
78
        Initial symmetric and positive definite approximation of
79
        the Lagrangian Hessian of shape (nx,nx) for the BFGS update.
80
        Available only with SciPy >= 1.14.0.
81
    init_multipliers : dict, default=None
82
        Initial Lagrange multiplier values for the bounds and constraints.
83
        Should be a dict with keys 'cl', 'cu', 'xl', 'xu' corresponding to
84
        the lower and upper bounds of the constraints and variables, and
85
        values being arrays of the same length as the number of constraints/variables.
86

87
    readable_outputs : list, default=[]
88
        List of outputs to be written to readable text output files.
89
        Available outputs are: 'major', 'obj', 'x', 'lag_mult', 'slacks', 'constraints', 'opt',
90
        'feas', 'sum_viol', 'max_viol', 'time', 'nfev', 'ngev', 'step', 'rho', 
91
        'merit', 'elastic', 'gamma', 'low_curvature'.
92
    """
93
    # qp_tol : float, default=1e-4
94
    #     Tolerance for the QP subproblem.
95
    # qp_maxiter : int, default=5000
96
    #     Maximum number of iterations for the QP subproblem.
97
    def initialize(self):
1✔
98
        self.solver_name = 'opensqp'
1✔
99

100
        self.nx = self.problem.nx
1✔
101

102
        self.obj = self.problem._compute_objective
1✔
103
        self.grad = self.problem._compute_objective_gradient
1✔
104
        self.active_callbacks = ['obj', 'grad']
1✔
105
        if self.problem.constrained:
1✔
106
            self.con_in = self.problem._compute_constraints
1✔
107
            self.jac_in = self.problem._compute_constraint_jacobian
1✔
108
            self.active_callbacks += ['con', 'jac']
1✔
109

110
        ##################################################################
111
        # TEMPORARY: for CSDLAlphaProblem
112
        if type(self.problem) is CSDLAlphaProblem:
1✔
113
            self.obj = lambda x: self.problem._compute_objective(x, check_failure=True)
×
114
            self.grad = lambda x: self.problem._compute_objective_gradient(x, check_failure=True)
×
115
            if self.problem.constrained:
×
116
                self.con_in = lambda x: self.problem._compute_constraints(x, check_failure=True)
×
117
                self.jac_in = lambda x: self.problem._compute_constraint_jacobian(x, check_failure=True)
×
118
        ##################################################################
119

120
        self.options.declare('verbosity', default=0, values=[0, 1])
1✔
121
        self.options.declare('maxiter', default=1000, types=int)
1✔
122
        self.options.declare('opt_tol', default=1e-7, types=float)
1✔
123
        self.options.declare('feas_tol', default=1e-7, types=float)
1✔
124
        self.options.declare('readable_outputs', types=list, default=[])
1✔
125
        self.options.declare('aqp_primal_feas_tol', default=1e-8, types=float)
1✔
126
        self.options.declare('aqp_dual_feas_tol', default=1e-8, types=float)
1✔
127
        self.options.declare('aqp_time_limit', default=5.0, types=float)
1✔
128
        # self.options.declare('qp_maxiter', default=5000, types=int)
129

130
        self.options.declare('ls_min_step', default=1e-12, types=float)
1✔
131
        self.options.declare('ls_max_step', default=1.0, types=float)
1✔
132
        self.options.declare('ls_maxiter', default=10, types=int)
1✔
133
        self.options.declare('ls_eta_a', default=1e-4, types=float)
1✔
134
        self.options.declare('ls_eta_w', default=0.95, types=float)
1✔
135
        self.options.declare('ls_alpha_tol', default=1e-14, types=float)
1✔
136

137
        self.options.declare('init_multipliers', default=None, types=(type(None), Dict))
1✔
138
        if Version(scipy.__version__) >= Version("1.14.0"):
1✔
139
            self.options.declare('bfgs_init_lag_hess', default=None, types=(type(None), np.ndarray))
1✔
140
        else:
NEW
141
            warnings.warn("SciPy version is less than 1.14.0, 'bfgs_init_lag_hess' option is not available.")
×
142

143
        self.available_outputs = {
1✔
144
            'major': int,
145
            'obj': float,
146
            # for arrays from each iteration, shapes need to be declared
147
            'x': (float, (self.problem.nx, )),
148

149
            # Note that the number of constraints will
150
            # be updated after constraints are setup
151
            'lag_mult': (float, (self.problem.nc, )),
152
            'slacks': (float, (self.problem.nc, )),
153
            'constraints': (float, (self.problem.nc, )),
154
            'opt': float,
155
            'feas': float,
156
            'sum_viol': float,
157
            'max_viol': float,
158
            'time': float,
159
            'nfev': int,
160
            'ngev': int,
161
            'step': float,
162
            'rho': float,
163
            'merit': float,
164
            'elastic': int,
165
            'gamma': float,
166
            'low_curvature': int,
167
        }
168

169
    def setup(self):
1✔
170
        self.setup_constraints()
1✔
171
        self.setup_initial_multipliers()
1✔
172
        nx   = self.nx
1✔
173
        nc   = self.nc
1✔
174
        nc_e = self.nc_e
1✔
175
        self.available_outputs['lag_mult'] = (float, (nc, ))
1✔
176
        self.available_outputs['slacks'] = (float, (nc, ))
1✔
177
        self.available_outputs['constraints'] = (float, (nc, ))
1✔
178
        # Damping parameters
179
        self.delta_rho = 1.
1✔
180
        self.num_rho_changes = 0
1✔
181
        self.num_rho_increases = 0
1✔
182
        self.num_rho_decreases = 0
1✔
183

184
        self.successive_undefined_iterations = 0
1✔
185
        
186
        init_scale = 1.0
1✔
187
        if Version(scipy.__version__) >= Version("1.14.0") and self.options['bfgs_init_lag_hess'] is not None:
1✔
188
            init_scale = self.options['bfgs_init_lag_hess'] * 1.0
1✔
189
        self.QN = BFGSScipy(nx=nx,
1✔
190
                            exception_strategy='damp_update',
191
                            init_scale=init_scale)
192
            
193
        self.MF = AugmentedLagrangian(nx=nx,
1✔
194
                                      nc=nc,
195
                                      nc_e=nc_e,
196
                                      f=self.obj,
197
                                      c=self.con,
198
                                      g=self.grad,
199
                                      j=self.jac,
200
                                      non_bound_indices=self.non_bound_indices)
201

202
        self.LSS = Minpack2LS(f=self.MF.compute_function,
1✔
203
                              g=self.MF.compute_gradient,
204
                              min_step=self.options['ls_min_step'],
205
                              max_step=self.options['ls_max_step'],
206
                              maxiter=self.options['ls_maxiter'],
207
                              eta_a=self.options['ls_eta_a'],
208
                              eta_w=self.options['ls_eta_w'],
209
                              alpha_tol=self.options['ls_alpha_tol'],
210
                              )
211

212
    # Adapt constraints to C_e(x)=0, and C_i(x) >= 0
213
    def setup_constraints(self, ):
1✔
214
        xl = self.problem.x_lower
1✔
215
        xu = self.problem.x_upper
1✔
216

217
        ebi = self.eq_bound_indices    = np.array([])
1✔
218
        lbi = self.lower_bound_indices = np.where((xl != -np.inf))[0]
1✔
219
        ubi = self.upper_bound_indices = np.where((xu !=  np.inf))[0]
1✔
220

221
        self.eq_bounded    = True if len(ebi) > 0 else False
1✔
222
        self.lower_bounded = True if len(lbi) > 0 else False
1✔
223
        self.upper_bounded = True if len(ubi) > 0 else False   
1✔
224

225
        if self.problem.constrained:
1✔
226
            cl = self.problem.c_lower
1✔
227
            cu = self.problem.c_upper
1✔
228

229
            eci = self.eq_constraint_indices    = np.where(cl == cu)[0]
1✔
230
            lci = self.lower_constraint_indices = np.where((cl != -np.inf) & (cl != cu))[0]
1✔
231
            uci = self.upper_constraint_indices = np.where((cu !=  np.inf) & (cl != cu))[0]
1✔
232

233
        else:
234
            eci = np.array([])
1✔
235
            lci = np.array([])
1✔
236
            uci = np.array([])
1✔
237

238
        self.eq_constrained    = True if len(eci) > 0 else False
1✔
239
        self.lower_constrained = True if len(lci) > 0 else False
1✔
240
        self.upper_constrained = True if len(uci) > 0 else False
1✔
241

242
        self.nc_e = len(ebi) + len(eci)
1✔
243
        self.nc_b = len(lbi) + len(ubi)
1✔
244
        self.nc_i = len(lbi) + len(ubi) + len(lci) + len(uci)
1✔
245
        self.nc   = self.nc_e + self.nc_i
1✔
246

247
        self.non_bound_indices = np.concatenate([np.arange(self.nc_e), np.arange(self.nc_e+self.nc_b, self.nc)])
1✔
248

249
    def setup_initial_multipliers(self, ):
1✔
250
        pi_ = self.options['init_multipliers']
1✔
251
        if pi_ is None:
1✔
252
            self.pi_0 = None
1✔
253
            return
1✔
254
        
255
        if self.nc_b > 0 and not {'xl', 'xu'}.issubset(pi_.keys()):
1✔
256
            raise ValueError("init_multipliers must contain keys 'xl'and 'xu' for problems with variable bounds. \
×
257
                             If unsure about some of the multipliers, set them as zeros.")
258
        lbi = self.lower_bound_indices
1✔
259
        ubi = self.upper_bound_indices
1✔
260
        
261
        if self.problem.constrained:
1✔
262
            if not {'cl', 'cu'}.issubset(pi_.keys()):
1✔
263
                raise ValueError("init_multipliers must contain keys 'cl' and 'cu' for problems with constraints. \
×
264
                                 If unsure about some of the multipliers, set them as zeros.")
265
            eci = self.eq_constraint_indices
1✔
266
            lci = self.lower_constraint_indices
1✔
267
            uci = self.upper_constraint_indices
1✔
268

269
        pi_0 = np.array([])
1✔
270
        if self.eq_constrained:
1✔
271
            pi_0 = np.append(pi_0, pi_['cl'][eci])
1✔
272
        if self.lower_bounded:
1✔
273
            pi_0 = np.append(pi_0, pi_['xl'][lbi])
1✔
274
        if self.upper_bounded:
1✔
275
            pi_0 = np.append(pi_0, pi_['xu'][ubi])
×
276
        if self.lower_constrained:
1✔
277
            pi_0 = np.append(pi_0, pi_['cl'][lci])
1✔
278
        if self.upper_constrained:
1✔
279
            pi_0 = np.append(pi_0, pi_['cu'][uci])
×
280
        self.pi_0 = pi_0
1✔
281

282
    def con(self, x):
1✔
283
        ebi = self.eq_bound_indices
1✔
284
        lbi = self.lower_bound_indices
1✔
285
        ubi = self.upper_bound_indices
1✔
286
        
287
        if self.problem.constrained:
1✔
288
            eci = self.eq_constraint_indices
1✔
289
            lci = self.lower_constraint_indices
1✔
290
            uci = self.upper_constraint_indices
1✔
291
            # Compute problem constraints
292
            c_in = self.con_in(x)
1✔
293

294
        c_out = np.array([])
1✔
295

296
        if self.eq_bounded:
1✔
297
            c_out = np.append(c_out, x[ebi] - self.problem.x_lower[ebi])
×
298
        if self.eq_constrained:
1✔
299
            c_out = np.append(c_out, c_in[eci] - self.problem.c_lower[eci])
1✔
300

301
        if self.lower_bounded:
1✔
302
            c_out = np.append(c_out, x[lbi] - self.problem.x_lower[lbi])
1✔
303
        if self.upper_bounded:
1✔
304
            c_out = np.append(c_out, self.problem.x_upper[ubi] - x[ubi])
×
305
        if self.lower_constrained:
1✔
306
            c_out = np.append(c_out, c_in[lci] - self.problem.c_lower[lci])
1✔
307
        if self.upper_constrained:
1✔
308
            c_out = np.append(c_out, self.problem.c_upper[uci] - c_in[uci])
×
309

310
        return c_out
1✔
311

312
    def jac(self, x):
1✔
313
        nx = self.nx
1✔
314
        # ebi = self.eq_bound_indices
315
        lbi = self.lower_bound_indices
1✔
316
        ubi = self.upper_bound_indices
1✔
317

318
        if self.problem.constrained:
1✔
319
            eci = self.eq_constraint_indices
1✔
320
            lci = self.lower_constraint_indices
1✔
321
            uci = self.upper_constraint_indices
1✔
322
            # Compute problem constraint Jacobian
323
            j_in = self.jac_in(x)
1✔
324

325
        j_out = np.empty((0, nx), dtype=float)
1✔
326

327
        # if self.eq_bounded:
328
        #     j_out = np.append(j_out, np.identity(nx)[ebi], axis=0)
329
        if self.eq_constrained:
1✔
330
            j_out = np.append(j_out, j_in[eci], axis=0)
1✔
331

332
        if self.lower_bounded:
1✔
333
            j_out = np.append(j_out,  np.identity(nx)[lbi], axis=0)
1✔
334
        if self.upper_bounded:
1✔
335
            j_out = np.append(j_out, -np.identity(nx)[ubi], axis=0)
×
336
        if self.lower_constrained:
1✔
337
            j_out = np.append(j_out,  j_in[lci], axis=0)
1✔
338
        if self.upper_constrained:
1✔
339
            j_out = np.append(j_out, -j_in[uci], axis=0)
×
340

341
        return j_out
1✔
342

343
    # Minimize A.L. w.r.t. slacks s.t. s >= 0
344
    def reset_slacks(self, c, pi, rho):
1✔
345
        rho_zero_indices    = np.where(rho == 0)[0]
1✔
346
        rho_nonzero_indices = np.where(rho  > 0)[0]
1✔
347
        s = np.zeros((len(rho), ))
1✔
348

349
        if len(rho_zero_indices) > 0:
1✔
350
            # TODO: test over a range of problems to see which one is better
351
            s[rho_zero_indices] = 0.
1✔
352
            # s[rho_zero_indices] = np.maximum(0, c[rho_zero_indices])
353

354
        if len(rho_nonzero_indices) > 0:
1✔
355
            c_rnz   = c[rho_nonzero_indices]
1✔
356
            pi_rnz  = pi[rho_nonzero_indices]
1✔
357
            rho_rnz = rho[rho_nonzero_indices]
1✔
358
            s[rho_nonzero_indices] = np.maximum(0, (c_rnz - pi_rnz / rho_rnz))
1✔
359

360
        return s
1✔
361

362
    def update_scalar_rho(self, rho_k, dir_deriv_al, pTHp, p_pi, c_k, s_k):
1✔
363
        # Damping for scalar rho as in SNOPT
364
        nc = self.nc
×
365
        delta_rho = self.delta_rho
×
366
        nbi = self.non_bound_indices
×
367

368
        rho_ref = rho_k[0] * 1.
×
369

370
        # note: scalar rho_min here
371
        if np.linalg.norm((c_k - s_k)) == 0:
×
372
            rho_min = 0. # See lemma 4.3 in the paper
×
373
        else:
374
            rho_min = 2 * np.linalg.norm(p_pi) / np.linalg.norm((c_k - s_k))
×
375
            
376
        rho_computed = rho_min * 1.0
×
377

378
        # Damping for rho (Note: vector rho is still not updated)
379
        if rho_k[0] < 4 * (rho_computed + delta_rho):
×
380
            rho_damped = rho_k[0]
×
381
        else:
382
            rho_damped = np.sqrt(rho_k[0] * (rho_computed + delta_rho))
×
383

384
        rho_new = max(rho_computed, rho_damped)
×
385
        rho_k[:]   = rho_new
×
386

387
        # Increasing rho
388
        if rho_k[0] > rho_ref:
×
389
            if self.num_rho_changes >= 0:
×
390
                self.num_rho_changes += 1
×
391
            else:
392
                self.delta_rho *= 2.
×
393
                self.num_rho_changes = 0
×
394

395
        # Decreasing rho
396
        elif rho_k[0] < rho_ref:
×
397
            if self.num_rho_changes <= 0:
×
398
                self.num_rho_changes -= 1
×
399
            else:
400
                self.delta_rho *= 2.
×
401
                self.num_rho_changes = 0
×
402

403
        return rho_k
×
404

405
    def update_vector_rho(self, rho_k, dir_deriv_al, pTHp, p_pi, c_k, s_k):
1✔
406
        delta_rho = self.delta_rho
1✔
407
        num_rho_changes = self.num_rho_changes
1✔
408
        nbi = self.non_bound_indices
1✔
409

410
        rho_ref = np.linalg.norm(rho_k)
1✔
411

412
        a = (c_k - s_k) ** 2
1✔
413
        b = 0.5 * pTHp + dir_deriv_al + np.inner(a, rho_k)
1✔
414

415
        if b > 0:
1✔
416
            a_norm = np.linalg.norm(a)
1✔
417
            if a_norm > 0: # Just to be extra cautious (elastic iterations)
1✔
418
                rho_computed = (b / (a_norm ** 2)) * a
1✔
419
            else:
420
                rho_computed = np.zeros((len(rho_k), ))
×
421
        else:
422
            rho_computed = np.zeros((len(rho_k), ))
1✔
423

424
        # Damping for rho (Note: vector rho is still not updated)
425
        rho_damped = np.where(
1✔
426
            rho_k < 4 * (rho_computed + delta_rho), rho_k,
427
            np.sqrt(rho_k * (rho_computed + delta_rho)))
428

429
        rho_new = np.maximum(rho_computed, rho_damped)
1✔
430
        rho_k[:] = rho_new
1✔
431

432
        rho_norm = np.linalg.norm(rho_k)
1✔
433

434
        # Increasing rho
435
        if rho_norm > rho_ref:
1✔
436
            if self.num_rho_decreases >= 2:
1✔
437
                self.delta_rho *= 2.
×
438
            self.num_rho_increases += 1
1✔
439
            self.num_rho_decreases = 0
1✔
440
        # Decreasing rho
441
        elif rho_norm < rho_ref:
1✔
442
            if self.num_rho_increases >= 2:
1✔
443
                self.delta_rho *= 2.
1✔
444
            self.num_rho_decreases += 1
1✔
445
            self.num_rho_increases = 0
1✔
446
        # Constant rho
447
        else:
448
            self.num_rho_increases = 0
1✔
449
            self.num_rho_decreases = 0
1✔
450

451
        return rho_k
1✔
452

453
    def l1_penalty_line_search(self, x_k, eta, x_qp, p_k, rho_k_Powell, f_k, c_k, g_k):
1✔
454
        nc_e = self.nc_e
1✔
455
        nx   = self.nx
1✔
456

457
        new_f_evals = 0
1✔
458
        converged = False
1✔
459
        mu = rho_k_Powell[self.non_bound_indices]
1✔
460
        c_viol = c_k[self.non_bound_indices]
1✔
461
        c_viol[:nc_e] = np.abs(c_viol[:nc_e])
1✔
462
        c_viol[nc_e:] = np.maximum(0, -c_viol[nc_e:])
1✔
463
        # Penalty merit function value at alpha = alpha
464
        mf0 = f_k + np.dot(mu, c_viol)
1✔
465

466
        exred = g_k.T @ x_qp - np.dot(mu, c_viol) * (1-eta)
1✔
467

468
        ls_iter = 0
1✔
469
        step = 1.0
1✔
470
        alpha = 1.0
1✔
471
        alpha_min = 1e-1
1✔
472
        d = p_k[:nx] * 1.0
1✔
473
        while True:
1✔
474
            ls_iter += 1
1✔
475
            exred = alpha*exred # expected reduction (<0) in the merit function
1✔
476
            # Note that d needs to recursively scaled down: d != alpha * x_qp 
477
            d  = alpha * d
1✔
478
            x = x_k + d
1✔
479
            self.MF.update_functions_in_cache(['f', 'c'], x)
1✔
480
            f = self.MF.cache['f'][1]
1✔
481
            c = self.MF.cache['c'][1]
1✔
482
            new_f_evals += 1
1✔
483

484
            c_viol = c[self.non_bound_indices]
1✔
485
            c_viol[:nc_e] = np.abs(c_viol[:nc_e])
1✔
486
            c_viol[nc_e:] = np.maximum(0, -c_viol[nc_e:])
1✔
487

488
            # Penalty merit function value at alpha = alpha
489
            mf = f + np.dot(mu, c_viol)
1✔
490
            # Actual reduction in the merit function
491
            acred = mf - mf0
1✔
492

493
            # Break out of line search if the change (-ve) in the merit function 
494
            # is at least one-tenth of the expected change exred (always negative)
495
            # i.e., the obtained change in the merit function must be more negative 
496
            # than one-tenth of the expected reduction
497
            if (acred<=exred/10.0 or ls_iter > 10):
1✔
498
                new_g_evals = 0
1✔
499
                alpha = step * 1.0
1✔
500
                if acred <= exred:
1✔
501
                    converged = True
×
502
                break
1✔
503

504
            # Otherwise,
505
            alpha = np.maximum(exred/(2*(exred-acred)), alpha_min)
×
506
            step *= alpha
×
507

508
        return new_f_evals, converged, step
1✔
509

510
    def opt_check(self, pi, c, g, j):
1✔
511
        opt_tol = self.options['opt_tol']
1✔
512

513
        if self.nc == 0:
1✔
514
            opt_tol_factor = 1.
1✔
515
            nonneg_violation = 0.
1✔
516
            compl_violation = 0.
1✔
517
            stat_violation = np.linalg.norm(g, np.inf)
1✔
518
            
519
        else:
520
            opt_tol_factor = (1 + np.linalg.norm(pi, np.inf))
1✔
521
            if pi[self.nc_e:].size == 0:
1✔
522
                nonneg_violation = 0.
×
523
            else:
524
                nonneg_violation = max(0., -np.min(pi[self.nc_e:]))
1✔
525
            # Note: compl_violation can be negative(Question on SNOPT);
526
            #       Other 2 violations are always nonnegative
527
            compl_violation = np.max(np.abs(c * pi))
1✔
528
            stat_violation  = np.linalg.norm(g - j.T @ pi, np.inf)
1✔
529

530
        
531
        scaled_opt_tol = opt_tol * opt_tol_factor
1✔
532
        opt_check1 = (nonneg_violation <= scaled_opt_tol)
1✔
533
        opt_check2 = (compl_violation <= scaled_opt_tol)
1✔
534
        opt_check3 = (stat_violation <= scaled_opt_tol)
1✔
535

536
        # opt is always nonnegative
537
        if self.options['verbosity'] == 1:
1✔
NEW
538
            print('opt_tol_factor:', opt_tol_factor)
×
NEW
539
            print('nonneg_violation:', nonneg_violation)
×
NEW
540
            print('compl_violation:', compl_violation)
×
NEW
541
            print('stat_violation:', stat_violation)
×
542
        opt = max(nonneg_violation, compl_violation,
1✔
543
                  stat_violation) / opt_tol_factor
544
        opt_satisfied = (opt_check1 and opt_check2 and opt_check3)
1✔
545

546
        return opt_satisfied, opt
1✔
547

548
    def feas_check(self, x, c):
1✔
549
        feas_tol = self.options['feas_tol']
1✔
550

551
        feas_tol_factor = (1 + np.linalg.norm(x, np.inf))
1✔
552
        scaled_feas_tol = feas_tol * feas_tol_factor
1✔
553

554
        # Violation is positive
555
        max_con_violation_eq   = np.max(np.abs(c[:self.nc_e]))   if self.nc_e > 0 else 0.
1✔
556
        max_con_violation_ineq = max(0., -np.min(c[self.nc_e:])) if self.nc_i > 0 else 0.
1✔
557
        max_con_violation      = max(max_con_violation_eq, max_con_violation_ineq)
1✔
558
        feas_check = (max_con_violation <= scaled_feas_tol)
1✔
559

560
        sum_con_violation_eq   = np.sum(np.abs(c[:self.nc_e]))
1✔
561
        sum_con_violation_ineq = np.sum(np.maximum(0., -c[self.nc_e:]))
1✔
562
        sum_con_violation      = sum_con_violation_eq + sum_con_violation_ineq
1✔
563

564
        feas = max_con_violation / feas_tol_factor
1✔
565
        feas_satisfied = feas_check
1✔
566
        return feas_satisfied, feas, sum_con_violation, max_con_violation
1✔
567

568
    def exit_early(self, x_k, f_k, c_k, pi_k, opt, feas, nfev, ngev, niter, time, success, exit_msg):
1✔
NEW
569
        if self.options['verbosity'] == 1:
×
NEW
570
            print(f'{exit_msg} Exiting ...')
×
NEW
571
        self.results = {'x': x_k,
×
572
                        'objective': f_k,
573
                        'c': c_k,
574
                        'pi': pi_k,
575
                        'optimality': opt,
576
                        'feasibility': feas,
577
                        'nfev': nfev,
578
                        'ngev': ngev,
579
                        'niter': niter,
580
                        'time': time,
581
                        'success': success,
582
                        'exit_msg': exit_msg}
NEW
583
        return self.results
×
584

585
    def solve(self):
1✔
586
        try:
1✔
587
            import qpsolvers
1✔
588
        except ImportError:
×
589
            raise ImportError("qpsolvers cannot be imported for the QP solver.  Install it with 'pip install qpsolvers'.")
×
590
        
591
        try:
1✔
592
            from quadprog import solve_qp
1✔
593
        except ImportError:
×
594
            raise ImportError("quadprog cannot be imported for the QP solver.  Install it with 'pip install quadprog'.")
×
595
        
596
        try:
1✔
597
            import highspy
1✔
598
        except ImportError:
×
599
            raise ImportError("HiGHS cannot be imported for the QP solver.  Install it with 'pip install highspy'.")
×
600

601
        # Assign shorter names to variables and methods
602
        nx = self.nx
1✔
603
        nc = self.nc
1✔
604
        nc_e = self.nc_e
1✔
605
        nc_i = self.nc_i
1✔
606

607
        x0 = self.problem.x0
1✔
608
        verbosity       = self.options['verbosity']
1✔
609
        maxiter         = self.options['maxiter']
1✔
610
        aqp_primal_tol  = self.options['aqp_primal_feas_tol']
1✔
611
        aqp_dual_tol    = self.options['aqp_dual_feas_tol']
1✔
612
        aqp_time_limit  = self.options['aqp_time_limit']
1✔
613
        # qp_maxiter     = self.options['qp_maxiter']
614

615
        LSS = self.LSS
1✔
616
        QN = self.QN
1✔
617
        MF = self.MF
1✔
618

619
        eps = 2.22e-16
1✔
620

621
        start_time = time.time()
1✔
622

623
        # Proximal point initialization for a feasible start wrt bounds
624
        x_k = np.clip(x0, self.problem.x_lower, self.problem.x_upper)
1✔
625
        
626
        self.MF.update_functions_in_cache(['f', 'c', 'g', 'j'], x_k)
1✔
627
        f_k = self.MF.cache['f'][1]
1✔
628
        g_k = self.MF.cache['g'][1]
1✔
629
        c_k = self.MF.cache['c'][1]
1✔
630
        J_k = self.MF.cache['j'][1]
1✔
631
        undefined_proximal_point = False
1✔
632

633
        if np.isnan(f_k) or np.isinf(f_k):
1✔
634
            warnings.warn('Objective value at the computed proximal initial point is NaN or Inf. Trying given initial point ...')
×
635
            undefined_proximal_point = True
×
636
        elif np.any(np.isnan(c_k)) or np.any(np.isinf(c_k)):
1✔
637
            warnings.warn('Constraint values at the computed proximal initial point contains NaN or Inf. Trying given initial point ...')
×
638
            undefined_proximal_point = True
×
639
        elif np.any(np.isnan(g_k)) or np.any(np.isinf(g_k)):
1✔
640
            warnings.warn('Objective gradient at the computed proximal initial point contains NaN or Inf. Trying given initial point ...')
×
641
            undefined_proximal_point = True
×
642
        elif np.any(np.isnan(J_k)) or np.any(np.isinf(J_k)):
1✔
643
            warnings.warn('Constraint Jacobian at the computed proximal initial point contains NaN or Inf. Trying given initial point ...')
×
644
            undefined_proximal_point = True
×
645
        elif verbosity == 1:
1✔
UNCOV
646
            print('Proximal point initialization is well-defined and good to go.')
×
647
        
648
        if undefined_proximal_point:
1✔
649
            if np.all(x0 == x_k):
×
NEW
650
                exit_msg = 'Initial point provided and proximal point computed were the same and is undefined.'
×
NEW
651
                return self.exit_early(x_k, f_k, c_k, None, None, None, 1, 1, 0, time.time() - start_time, False, exit_msg)
×
652
            
653
            x_k = x0 * 1.
×
654

655
            self.MF.update_functions_in_cache(['f', 'c', 'g', 'j'], x_k)
×
656
            f_k = self.MF.cache['f'][1]
×
657
            g_k = self.MF.cache['g'][1]
×
658
            c_k = self.MF.cache['c'][1]
×
659
            J_k = self.MF.cache['j'][1]
×
660

661
            if np.isnan(f_k) or np.isinf(f_k):
×
NEW
662
                exit_msg = 'Objective value at given initial point and computed proximal point is NaN or Inf.'
×
NEW
663
                return self.exit_early(x_k, f_k, c_k, None, None, None, 2, 2, 0, time.time() - start_time, False, exit_msg)
×
664
            elif np.any(np.isnan(c_k)) or np.any(np.isinf(c_k)):
×
NEW
665
                exit_msg = 'Constraint values at given initial point and computed proximal point contains NaN or Inf.'
×
NEW
666
                return self.exit_early(x_k, f_k, c_k, None, None, None, 2, 2, 0, time.time() - start_time, False, exit_msg)
×
667
            elif np.any(np.isnan(g_k)) or np.any(np.isinf(g_k)):
×
NEW
668
                exit_msg = 'Gradient at given initial point and computed proximal point contains NaN or Inf.'
×
NEW
669
                return self.exit_early(x_k, f_k, c_k, None, None, None, 2, 2, 0, time.time() - start_time, False, exit_msg)
×
670
            elif np.any(np.isnan(J_k)) or np.any(np.isinf(J_k)):
×
NEW
671
                exit_msg = 'Constraint Jacobian at given initial point and computed proximal point contains NaN or Inf.'
×
NEW
672
                return self.exit_early(x_k, f_k, c_k, None, None, None, 2, 2, 0, time.time() - start_time, False, exit_msg)
×
673

674
        # Set initial values for multipliers and slacks
675
        # pi_k = np.full((nc, ), 1.)
676
        pi_k = self.pi_0 * 1. if self.pi_0 is not None else np.full((nc, ), 0.)
1✔
677

678
        nfev = 1
1✔
679
        ngev = 1
1✔
680

681
        # Vector of penalty parameters
682
        rho_k = np.full((nc, ), 0.)
1✔
683
        rho_k_Powell = np.full((nc, ), 0.)
1✔
684

685
        # Compute slacks by minimizing A.L. s.t. s_k >= 0
686
        s_k = self.reset_slacks(c_k[nc_e:], pi_k[nc_e:], rho_k[nc_e:])
1✔
687
        # s_k = np.maximum(0, c_k) # Use this if rho_k = 0. at start
688

689
        # Vector of design vars., lag. mults., and slack vars.
690
        v_k  = np.concatenate((x_k, pi_k, s_k))
1✔
691
        x_k  = v_k[:nx]
1✔
692
        pi_k = v_k[nx:(nx + nc)]
1✔
693
        s_k  = v_k[(nx + nc):]
1✔
694

695
        p_k  = np.zeros((len(v_k), ))
1✔
696
        p_x  = p_k[:nx]
1✔
697
        p_pi = p_k[nx:(nx + nc)]
1✔
698
        p_s  = p_k[(nx + nc):]
1✔
699

700
        # Iteration counter
701
        itr = 0
1✔
702

703
        opt_satisfied, opt = self.opt_check(pi_k, c_k, g_k, J_k)
1✔
704
        if self.nc == 0:
1✔
705
            feas_satisfied, feas, sviol, mviol = True, 0., 0., 0.
1✔
706
        else:
707
            feas_satisfied, feas, sviol, mviol = self.feas_check(x_k, c_k)
1✔
708
        
709
        tol_satisfied = (opt_satisfied and feas_satisfied)
1✔
710

711
        # Evaluate merit function value
712
        MF.set_rho(rho_k)
1✔
713
        mf_k  = MF.evaluate_function(x_k, pi_k, s_k, f_k, c_k)
1✔
714
        mfg_k = MF.evaluate_gradient(x_k, pi_k, s_k, f_k, c_k, g_k, J_k)
1✔
715

716
        # Initializing declared outputs
717
        self.update_outputs(major=0,
1✔
718
                            x=x_k,
719
                            lag_mult=pi_k,
720
                            slacks=s_k,
721
                            obj=f_k,
722
                            constraints=c_k,
723
                            opt=opt,
724
                            feas=feas,
725
                            sum_viol= sviol,
726
                            max_viol=mviol,
727
                            time=time.time() - start_time,
728
                            nfev=nfev,
729
                            ngev=ngev,
730
                            step=0.,
731
                            # rho=rho_k[0],
732
                            rho=np.linalg.norm(rho_k),
733
                            merit=mf_k,
734
                            elastic=0,
735
                            gamma=0.,
736
                            low_curvature=0)
737
                            # merit=penalty_k)
738

739
        elastic_itr = 0
1✔
740
        gamma_0 = 1e6
1✔
741
        max_gamma = 1e12
1✔
742
        gamma = gamma_0 * 1.
1✔
743
        el_wt_check_freq = 25
1✔
744
        el_feas_arr = np.zeros((el_wt_check_freq, ))
1✔
745

746
        while itr < maxiter:
1✔
747
            itr_start = time.time()
1✔
748
            itr += 1
1✔
749

750
            # ALGORITHM STARTS HERE
751
            # >>>>>>>>>>>>>>>>>>>>>
752

753
            # Compute the search direction toward the next iterate
754

755
            # Solve a strictly convex quadratic program
756
            # Minimize     1/2 x^T G x - a^T x
757
            # Subject to   C.T x >= b
758

759
            # def solve_qp(double[:, :] G, double[:] a, double[:, :] C=None, double[:] b=None, int meq=0, factorized=False):
760
            # First meq constraints are treated as equality constraints
761

762
            try:
1✔
763
                if c_k.size == 0:
1✔
764
                    x_qp, f_qp, xu_qp, iter_qp, lag_qp, iact_qp = solve_qp(QN.B_k, -g_k)
1✔
765
                    lag_qp = np.array([])
1✔
766
                else:
767
                    x_qp, f_qp, xu_qp, iter_qp, lag_qp, iact_qp = solve_qp(QN.B_k, -g_k, C=J_k.T, b=-c_k, meq=nc_e)
1✔
768
                    gamma = gamma_0 * 1.
1✔
769

770
                p_k[:nx] = x_qp
1✔
771
                p_k[nx:(nx + nc)] = lag_qp - v_k[nx:nx + nc]
1✔
772
                elastic_itr = 0         # reset elastic_itr if QP is successful
1✔
773
                elastic_mode = False    # reset elastic_mode if QP is successful
1✔
774
                eta = 0.
1✔
775

776
            except Exception as e:
×
NEW
777
                if verbosity == 1:
×
NEW
778
                    print('QP solver failed at major iteration:', itr)
×
NEW
779
                    print(e)
×
780

781
                if "matrix G is not positive definite" in str(e):
×
NEW
782
                    if verbosity == 1:
×
NEW
783
                        print('Matrix G is not positive definite. Resetting Hessian.')
×
784
                    # Reset Hessian
785
                    wTw = np.dot(w_k, w_k)
×
786
                    wTd = np.dot(w_k, d_k[:nx])
×
787

788
                    init_scale = wTw / (wTd+1e-16) if wTd > 0 else 1.
×
789
                    self.QN = QN = BFGSScipy(nx=nx,
×
790
                                            exception_strategy='damp_update',
791
                                            init_scale=init_scale)
792
                                            # init_scale=np.linalg.norm(np.diag(QN.B_k)))
793
                    
794
                    continue # Skip the rest of the iteration and solve QP with new reset Hessian
×
795

796
                # break
797
                if 'constraints are inconsistent' in str(e) or \
×
798
                "unsupported operand type(s) for -: 'NoneType' and 'float'" in str(e) or \
799
                "bad operand type for unary -: 'NoneType'" in str(e) or \
800
                "x_qp is zero" in str(e) or \
801
                "Rank(A) < p or Rank([P; A; G]) < n" in str(e):
NEW
802
                    if verbosity == 1:
×
NEW
803
                        print('Infeasible QP problem. Entering elastic mode.')
×
804
                    elastic_mode = True
×
805
                    while True:
×
806
                        el_feas_arr[elastic_itr % el_wt_check_freq] = feas
×
807
                        elastic_itr += 1
×
NEW
808
                        if verbosity == 1:
×
NEW
809
                            print('Elastic programmming with weight:', gamma)
×
810

811
                        # METHOD: Modified Powell's treatment of infeasible QP problems - eq ->ineq
812
                        # TODO: only for violation of inequality constraints
813
                        # minimize 1/2 x^T B_k x + g_k^T x + gamma/2 * \eta^2
814
                        # subject to J_k^T x + c_k(1-\eta)  = 0
815
                        #            J_k^T x + c_k(1-\eta) >= 0 # only for violated constraints
816
                        G_elastic = np.block([[QN.B_k, np.zeros((nx, 1))],
×
817
                                              [np.zeros((1, nx)),   gamma]])
818
                        a_elastic = -np.concatenate((g_k, [0.]))
×
819

820
                        el_c_k = np.concatenate((c_k, -c_k[:nc_e]))
×
821
                        el_c_k = -np.maximum(0, -el_c_k)
×
822
                        J_elastic = np.block([[J_k,         -el_c_k[:nc].reshape(nc, 1)],
×
823
                                              [-J_k[:nc_e], -el_c_k[nc:].reshape(nc_e, 1)],
824
                                              [np.zeros((2, nx)), np.array([[1.],[-1.]])]])
825

826
                        b_elastic = np.concatenate((-c_k, c_k[:nc_e], [0., -1.]))
×
827

828
                        if elastic_itr > el_wt_check_freq:
×
829
                                if gamma < max_gamma:
×
830
                                    gamma *= 10.
×
831
                                    
832
                                elastic_itr = 0
×
833

834
                        qp_prob = qpsolvers.Problem(G_elastic, -a_elastic, 
×
835
                                                    -J_elastic, -b_elastic)
836
                        qp_solver = 'highs'
×
837
                        qp_initvals = np.zeros((nx+1, ))
×
838
                        qp_initvals[-1] = 1.
×
NEW
839
                        if verbosity == 1:
×
NEW
840
                            print('Using elastic QP solver', qp_solver, 'with weight', gamma)
×
UNCOV
841
                        qp_sol = qpsolvers.solve_problem(qp_prob,
×
842
                                                         qp_solver,
843
                                                        #  initvals=qp_initvals,
844
                                                        #  verbose=True,
845
                                                        #  highs
846
                                                         dual_feasibility_tolerance=aqp_dual_tol,
847
                                                         primal_feasibility_tolerance=aqp_primal_tol,
848
                                                         time_limit=aqp_time_limit,
849
                                                         )
850
                        x_qp = qp_sol.x[:nx] * 1.
×
851
                        lag_qp = qp_sol.z[:nc] * 1.
×
852

853
                        if nc_e > 0:
×
854
                            lag_qp[:nc_e] -= qp_sol.z[nc:nc+nc_e]
×
855

856
                        p_k[:nx] = x_qp[:nx]
×
857
                        p_k[nx:(nx + nc)] = lag_qp - v_k[nx:nx + nc]
×
858
                        eta = qp_sol.x[nx] * 1.
×
859
                        break
×
860

861
                if np.linalg.norm(qp_sol.x[:nx]) == 0 and qp_sol.x[nx] == 1.:
×
NEW
862
                    exit_msg = 'Augmented QP is also infeasible'
×
NEW
863
                    return self.exit_early(x_k, f_k, c_k, pi_k, opt, feas, nfev, ngev, itr, time.time() - start_time, False, exit_msg)
×
864

865
            # Clip the step length such that the design variables remain within bounds
866
            p_k[:nx] = np.clip(p_x, self.problem.x_lower - x_k, self.problem.x_upper - x_k)
1✔
867

868
            # Search direction for s_k :
869
            # (c_k + J_k @ p_x) is the new estimate for s for iniequality constraints
870
            p_k[(nx + nc):] = c_k[nc_e:] + J_k[nc_e:] @ p_k[:nx] - v_k[nx + nc:]
1✔
871

872
            if verbosity == 1:
1✔
NEW
873
                print('Major iteration:', itr)
×
NEW
874
                print("=====================================")
×
875

876
            if self.nc > 0:
1✔
877
                dir_deriv_al = np.dot(mfg_k, p_k)
1✔
878
                pTHp = p_x.T @ (QN.B_k @ p_x)
1✔
879

880
                # Update penalty parameters
881
                rho_k_Powell = np.maximum(np.abs(lag_qp), 0.5*(rho_k_Powell + np.abs(lag_qp)))
1✔
882
                rho_k = self.update_vector_rho(rho_k, dir_deriv_al, pTHp,
1✔
883
                                               p_pi, c_k, np.concatenate([np.zeros((nc_e,)), s_k]))
884

885
            MF.set_rho(rho_k)
1✔
886
            mf_k  = MF.evaluate_function(x_k, pi_k, s_k, f_k, c_k)
1✔
887
            mfg_k = MF.evaluate_gradient(x_k, pi_k, s_k, f_k, c_k, g_k, J_k)
1✔
888
            dir_deriv_al_0 = np.dot(mfg_k, p_k)
1✔
889

890
            # Compute the step length along the search direction via a line search
891
            p_k_temp = p_k * 1.
1✔
892
            v_k_temp = v_k * 1.
1✔
893

894
            if dir_deriv_al_0 > -2.22e-16 or np.linalg.norm(p_k[:nx]) <= 2.22e-16:
1✔
895
                alpha = 1.0
×
896
                mf_new, mfg_new, mf_slope_new, new_f_evals, new_g_evals, converged = mf_k, mfg_k, dir_deriv_al_0, 1, 1, True
×
897

898
            else:
899
                alpha, mf_new, mfg_new, mf_slope_new, new_f_evals, new_g_evals, converged = LSS.search(
1✔
900
                    x=v_k_temp, p=p_k_temp, f0=mf_k, g0=mfg_k)
901

902
            undefined_direction = False
1✔
903
            if np.isnan(mf_new) or np.isinf(mf_new) or np.isnan(mfg_new).any() or np.isinf(mfg_new).any():
1✔
904
                undefined_direction = True
×
NEW
905
                if verbosity == 1:
×
NEW
906
                    print('Merit function or its gradient at the new point has NaN or inf.')
×
907
            else:
908
                nfev += new_f_evals
1✔
909
                ngev += new_g_evals
1✔
910

911
            alpha_golden = 0.061803398875
1✔
912
            if (not converged) and (not undefined_direction):
1✔
913
                # Use an inexact LS with l1-penalty and only function evaluations
914
                new_f_evals, converged, alpha = self.l1_penalty_line_search(x_k, eta, x_qp, p_k, rho_k_Powell, f_k, c_k, g_k)
1✔
915

916
                nfev += new_f_evals
1✔
917
                
918
            if not undefined_direction:
1✔
919
                if verbosity == 1:
1✔
NEW
920
                    print("###### SUCCESSFUL LINE SEARCH #######")
×
921
                d_k = alpha * p_k
1✔
922
                d_k[nx:(nx + nc)] = d_k[nx:(nx + nc)] / np.sqrt(alpha)
1✔
923
                d_k_temp = d_k * 1.
1✔
924

925

926
            g_old = g_k * 1.
1✔
927
            J_old = J_k * 1.
1✔
928

929
            # If the line search failed with nans at alpha=1 earlier, find a smaller alpha where the function is well-defined
930
            undefined_new_point = False
1✔
931
            if undefined_direction:
1✔
932

933
                x_k_new = x_k + p_k[:nx]
×
934

935
                self.MF.update_functions_in_cache(['f', 'g', 'c', 'j'], x_k_new)
×
936
                f_new = self.MF.cache['f'][1]
×
937
                g_new = self.MF.cache['g'][1]
×
938
                c_new = self.MF.cache['c'][1]
×
939
                J_new = self.MF.cache['j'][1]
×
940
                alpha = 1.0
×
941
                new_f_evals = 0
×
942
                new_g_evals = 0
×
943

944
                while np.isnan(f_new) or np.isinf(f_new):
×
NEW
945
                    if verbosity == 1:
×
NEW
946
                        print('Objective function is NaN or Inf. Stepping back x_k_new.')
×
947
                    alpha *= 0.1
×
948
                    d_k_temp = alpha * p_k
×
949
                    x_k_new = x_k + d_k_temp[:nx]
×
950
                    self.MF.update_functions_in_cache(['f'], x_k_new)
×
951
                    f_new = self.MF.cache['f'][1]
×
952
                    new_f_evals += 1
×
953
                    if alpha < 1e-12:
×
954
                        undefined_new_point = True
×
955
                        break
×
956
                
957
                if not undefined_new_point:
×
958
                    while np.any(np.isnan(c_new)) or np.any(np.isinf(c_new)):
×
NEW
959
                        if verbosity == 1:
×
NEW
960
                            print('Constraint values contain NaN or Inf. Stepping back x_k_new.')
×
961
                        alpha *= 0.1
×
962
                        d_k_temp = alpha * p_k
×
963
                        x_k_new = x_k + d_k_temp[:nx]
×
964
                        self.MF.update_functions_in_cache(['c'], x_k_new)
×
965
                        c_new = self.MF.cache['c'][1]
×
966
                        new_f_evals += 1
×
967
                        if alpha < 1e-12:
×
968
                            undefined_new_point = True
×
969
                            break
×
970
                
971
                if not undefined_new_point:
×
972
                    while np.any(np.isnan(g_new)) or np.any(np.isinf(g_new)):
×
NEW
973
                        if verbosity == 1:
×
NEW
974
                            print('Objective gradient contains NaN or Inf. Stepping back x_k_new.')
×
975
                        alpha *= 0.1
×
976
                        d_k_temp = alpha * p_k
×
977
                        x_k_new = x_k + d_k_temp[:nx]
×
978
                        self.MF.update_functions_in_cache(['g'], x_k_new)
×
979
                        g_new = self.MF.cache['g'][1]
×
980
                        new_g_evals += 1
×
981
                        if alpha < 1e-12:
×
982
                            undefined_new_point = True
×
983
                            break
×
984

985
                if not undefined_new_point:
×
986
                    while np.any(np.isnan(J_new)) or np.any(np.isinf(J_new)):
×
NEW
987
                        if verbosity == 1:
×
NEW
988
                            print('Constraint Jacobian contains NaN or Inf. Stepping back x_k_new.')
×
989
                        alpha *= 0.1
×
990
                        d_k_temp = alpha * p_k
×
991
                        x_k_new = x_k + d_k_temp[:nx]
×
992
                        self.MF.update_functions_in_cache(['j'], x_k_new)
×
993
                        J_new = self.MF.cache['j'][1]
×
994
                        new_g_evals += 1
×
995
                        if alpha < 1e-12:
×
996
                            undefined_new_point = True
×
997
                            break
×
998

999
                if self.problem.constrained:
×
1000
                    nfev += int(new_f_evals/2)
×
1001
                    ngev += int(new_g_evals/2)
×
1002
                else:
1003
                    nfev += new_f_evals
×
1004
                    ngev += new_g_evals
×
1005
            
1006
            if undefined_new_point:
1✔
1007
                self.successive_undefined_iterations += 1
×
1008
                if self.successive_undefined_iterations == 1:
×
NEW
1009
                    if verbosity == 1:
×
NEW
1010
                        print('No points along the search direction is well-defined. Resetting Hessian.')
×
UNCOV
1011
                    self.QN = QN = BFGSScipy(nx=nx,
×
1012
                                             exception_strategy='damp_update',
1013
                                             init_scale=1.)
1014
                    continue
×
1015

1016
                if self.successive_undefined_iterations == 2:
×
NEW
1017
                    exit_msg = 'Two successive iterations with unsuccessful search along predicted direction for well-defined points.'
×
NEW
1018
                    return self.exit_early(x_k, f_k, c_k, pi_k, opt, feas, nfev, ngev, itr, time.time() - start_time, False, exit_msg)
×
1019
     
1020
            elif undefined_direction:
1✔
1021
                self.successive_undefined_iterations = 0
×
1022

NEW
1023
                if verbosity == 1:
×
NEW
1024
                    print('alpha successful without nan:', alpha)
×
1025

1026
                # If the line search failed with nans at alpha=1 earlier, reperform line search with a smaller alpha found above
1027
                alpha_new, mf_new, mfg_new, mf_slope_new, new_f_evals, new_g_evals, converged = LSS.search(
×
1028
                    x=v_k_temp, p=alpha*p_k, f0=mf_k, g0=mfg_k)
1029
                
1030
                nfev += new_f_evals
×
1031
                ngev += new_g_evals
×
1032
                
1033
                # If the line search failed to find a point satisfying the Wolfe conditions, try backtracking line search
NEW
1034
                if not converged:
×
NEW
1035
                    if verbosity == 1:
×
NEW
1036
                        print('Inside SLSQP line search: Reperforming backtracking line search with a smaller alpha found above.')
×
1037
                    
1038
                    new_f_evals, converged, alpha_new = self.l1_penalty_line_search(x_k, eta, x_qp, alpha*p_k, rho_k_Powell, f_k, c_k, g_k)
×
1039
                    nfev += new_f_evals
×
1040

1041
                alpha *= alpha_new
×
1042
                d_k = alpha * p_k
×
1043
                d_k[nx:(nx + nc)] = d_k[nx:(nx + nc)] / np.sqrt(alpha) 
×
1044
                d_k_temp = d_k * 1.
×
1045

1046
            v_k += d_k_temp
1✔
1047

1048
            x_k  = v_k[:nx]
1✔
1049
            pi_k = v_k[nx:(nx + nc)]
1✔
1050

1051
            g_old = g_k * 1.
1✔
1052
            J_old = J_k * 1.
1✔
1053

1054
            self.MF.update_functions_in_cache(['f', 'c', 'g', 'j'], x_k)
1✔
1055
            f_k = self.MF.cache['f'][1]
1✔
1056
            c_k = self.MF.cache['c'][1]
1✔
1057
            g_k = self.MF.cache['g'][1]
1✔
1058
            J_k = self.MF.cache['j'][1]
1✔
1059

1060
            v_k[(nx + nc):] = self.reset_slacks(c_k[nc_e:], pi_k[nc_e:], rho_k[nc_e:])
1✔
1061
            s_k = v_k[(nx + nc):]
1✔
1062

1063
            # Note: MF changes (decreases) after slack reset
1064
            mf_k  = MF.evaluate_function(x_k, pi_k, s_k, f_k, c_k)
1✔
1065
            mfg_k = MF.evaluate_gradient(x_k, pi_k, s_k, f_k, c_k, g_k, J_k)
1✔
1066

1067
            w_k = (g_k - g_old) - (J_k - J_old).T @ pi_k
1✔
1068

1069

1070
            dTd = np.dot(d_k[:nx], d_k[:nx])
1✔
1071
            wTw = np.dot(w_k, w_k)
1✔
1072
            wTd = np.dot(w_k, d_k[:nx])
1✔
1073
            B_scaler = wTw / (wTd + 2.22e-16)
1✔
1074
            dBd = np.dot(d_k[:nx], QN.B_k @ d_k[:nx])
1✔
1075

1076
            low_curvature = 1 if (wTd > 0.2*dBd) else 0
1✔
1077

1078
            QN_d_k = d_k[:nx]
1✔
1079

1080
            QN.update(QN_d_k, w_k)
1✔
1081

1082
            # # <<<<<<<<<<<<<<<<<<<
1083
            # # ALGORITHM ENDS HERE
1084

1085
            opt_satisfied, opt = self.opt_check(pi_k, c_k, g_k, J_k)
1✔
1086
            if self.nc == 0:
1✔
1087
                feas_satisfied, feas, sviol, mviol = True, 0., 0., 0.
1✔
1088
            else:
1089
                feas_satisfied, feas, sviol, mviol = self.feas_check(x_k, c_k)
1✔
1090
            tol_satisfied = (opt_satisfied and feas_satisfied)
1✔
1091

1092
            # Update arrays inside outputs dict with new values from the current iteration
1093
            self.update_outputs(
1✔
1094
                major=itr,
1095
                x=x_k,
1096
                lag_mult=pi_k,
1097
                slacks=s_k,
1098
                obj=f_k,
1099
                constraints=c_k,
1100
                opt=opt,
1101
                feas=feas,
1102
                sum_viol=sviol,
1103
                max_viol=mviol,
1104
                time=time.time() - start_time,
1105
                nfev=nfev,
1106
                ngev=ngev,
1107
                # rho=rho_k[0],
1108
                rho=np.linalg.norm(rho_k),
1109
                step=alpha,
1110
                # step=0.5**ls_count,
1111
                merit=mf_k,
1112
                elastic=(elastic_itr>0),
1113
                gamma=gamma,
1114
                low_curvature=low_curvature,)
1115
                # merit=penalty_k)
1116
            if tol_satisfied:
1✔
1117
                exit_msg = 'Specified convergence tolerances achieved.'
1✔
1118
                break
1✔
1119

1120
        if not tol_satisfied:
1✔
NEW
1121
            exit_msg = 'Maximum iterations reached without convergence.'
×
1122

1123
        if verbosity == 1:
1✔
NEW
1124
            print(f'{exit_msg} Exiting ...')
×
1125

1126
        self.total_time = time.time() - start_time
1✔
1127

1128
        self.results = {
1✔
1129
            'x': x_k,
1130
            'objective': f_k,
1131
            'c': c_k,
1132
            'pi': pi_k,
1133
            'optimality': opt,
1134
            'feasibility': feas,
1135
            'nfev': nfev,
1136
            'ngev': ngev,
1137
            'niter': itr,
1138
            'time': self.total_time,
1139
            'success': tol_satisfied,
1140
            'exit_msg': exit_msg
1141
        }
1142

1143
        # Run post-processing for the Optimizer() base class
1144
        self.run_post_processing()
1✔
1145

1146
        return self.results
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc