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

LSDOlab / modopt / 12847926430

18 Jan 2025 10:24PM UTC coverage: 81.391% (+4.7%) from 76.658%
12847926430

push

github

anugrahjo
Update sqp_basic.py and add tests for it.

4 of 4 new or added lines in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

5244 of 6443 relevant lines covered (81.39%)

0.81 hits per line

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

73.42
/modopt/core/optimization_algorithms/sqp_basic.py
1
import numpy as np
1✔
2
import time
1✔
3

4
from modopt import Optimizer
1✔
5
from modopt.line_search_algorithms import ScipyLS, BacktrackingArmijo, Minpack2LS
1✔
6
from modopt.merit_functions import AugmentedLagrangian
1✔
7
from modopt.approximate_hessians import BFGSDamped as BFGS
1✔
8

9
# This optimizer takes constraints in all-inequality form, C(x) >= 0
10
class BSQP(Optimizer):
1✔
11
    '''
12
    An implementation of the Sequential Quadratic Programming (SQP) algorithm
13
    for general constrained optimization problems that uses a very basic QP solver.
14
    '''
15
    def initialize(self):
1✔
16
        self.solver_name = 'bsqp'
1✔
17

18
        self.nx = self.problem.nx
1✔
19

20
        self.obj = self.problem._compute_objective
1✔
21
        self.grad = self.problem._compute_objective_gradient
1✔
22
        # self.active_callbacks = ['obj', 'grad']
23
        if self.problem.constrained:
1✔
24
            self.con_in = self.problem._compute_constraints
1✔
25
            self.jac_in = self.problem._compute_constraint_jacobian
1✔
26
            # self.lag_hess = self.problem._compute_lagrangian_hessian
27
            # self.active_callbacks += ['con', 'jac']
28

29
        self.options.declare('maxiter', default=1000, types=int)
1✔
30
        self.options.declare('tol', default=1e-7, types=float)
1✔
31
        self.options.declare('readable_outputs', types=list, default=[])
1✔
32

33
        self.available_outputs = {
1✔
34
            'major': int,
35
            'obj': float,
36
            # for arrays from each iteration, shapes need to be declared
37
            'x': (float, (self.problem.nx, )),
38

39
            # Note that the number of constraints will
40
            # be updated after constraints are setup
41
            'lag_mult': (float, (self.problem.nc, )),
42
            # 'slacks': (float, (self.problem.nc, )),
43
            'constraints': (float, (self.problem.nc, )),
44
            # 'opt': float,
45
            # 'feas': float,
46
            'time': float,
47
            'nfev': int,
48
            'ngev': int,
49
            'step': float,
50
            # 'rho': float,
51
            'merit': float,
52
        }
53

54
    def setup(self):
1✔
55
        self.setup_constraints()
1✔
56
        nx   = self.nx
1✔
57
        nc   = self.nc
1✔
58
        nc_e = self.nc_e
1✔
59
        self.available_outputs['lag_mult'] = (float, (nc, ))
1✔
60
        # self.available_outputs['rho'] = (float, (nc, ))
61
        # self.available_outputs['slacks'] = (float, (nc, ))
62
        self.available_outputs['constraints'] = (float, (nc, ))
1✔
63
        # Damping parameters
64
        self.delta_rho = 1.
1✔
65
        self.num_rho_changes = 0
1✔
66

67
        # self.QN = BFGS(nx=nx)
68
        self.QN = BFGS(nx=nx,
1✔
69
                       exception_strategy='skip_update')
70
                    #    exception_strategy='damp_update')
71
        self.MF = AugmentedLagrangian(nx=nx,
1✔
72
                                      nc=nc,
73
                                      nc_e=nc_e,
74
                                      f=self.obj,
75
                                      c=self.con,
76
                                      g=self.grad,
77
                                      j=self.jac)
78

79
        self.LSS = ScipyLS(f=self.MF.compute_function,
1✔
80
                           g=self.MF.compute_gradient,
81
                           max_step=2.0)
82
        # self.LS = Minpack2LS(f=self.MF.compute_function,
83
        #                      g=self.MF.compute_gradient)
84
        self.LSB = BacktrackingArmijo(f=self.MF.compute_function,
1✔
85
                                      g=self.MF.compute_gradient)
86

87
        # For u_k of QP solver
88
        self.u_k = np.full((nc, ), np.inf)
1✔
89

90
    # Adapt constraints to C_e(x)=0, and C_i(x) >= 0
91
    def setup_constraints(self, ):
1✔
92
        xl = self.problem.x_lower
1✔
93
        xu = self.problem.x_upper
1✔
94

95
        ebi = self.eq_bound_indices    = np.where(xl == xu)[0]
1✔
96
        lbi = self.lower_bound_indices = np.where((xl != -np.inf) & (xl != xu))[0]
1✔
97
        ubi = self.upper_bound_indices = np.where((xu !=  np.inf) & (xl != xu))[0]
1✔
98

99
        self.eq_bounded    = True if len(ebi) > 0 else False
1✔
100
        self.lower_bounded = True if len(lbi) > 0 else False
1✔
101
        self.upper_bounded = True if len(ubi) > 0 else False   
1✔
102

103
        if self.problem.constrained:
1✔
104
            cl = self.problem.c_lower
1✔
105
            cu = self.problem.c_upper
1✔
106

107
            eci = self.eq_constraint_indices    = np.where(cl == cu)[0]
1✔
108
            lci = self.lower_constraint_indices = np.where((cl != -np.inf) & (cl != cu))[0]
1✔
109
            uci = self.upper_constraint_indices = np.where((cu !=  np.inf) & (cl != cu))[0]
1✔
110
        else:
111
            eci = np.array([])
1✔
112
            lci = np.array([])
1✔
113
            uci = np.array([])
1✔
114

115
        self.eq_constrained    = True if len(eci) > 0 else False
1✔
116
        self.lower_constrained = True if len(lci) > 0 else False
1✔
117
        self.upper_constrained = True if len(uci) > 0 else False
1✔
118

119
        self.nc_e = len(ebi) + len(eci)
1✔
120
        self.nc_i = len(lbi) + len(ubi) + len(lci) + len(uci)
1✔
121
        self.nc   = len(lbi) + len(ubi) + len(ebi) + len(lci) + len(uci) + len(eci)
1✔
122

123
    def con(self, x):
1✔
124
        ebi = self.eq_bound_indices
1✔
125
        lbi = self.lower_bound_indices
1✔
126
        ubi = self.upper_bound_indices
1✔
127
        
128
        if self.problem.constrained:
1✔
129
            eci = self.eq_constraint_indices
1✔
130
            lci = self.lower_constraint_indices
1✔
131
            uci = self.upper_constraint_indices
1✔
132
            # Compute problem constraints
133
            c_in = self.con_in(x)
1✔
134

135
        c_out = np.array([])
1✔
136

137
        if self.eq_bounded:
1✔
138
            c_out = np.append(c_out, x[ebi] - self.problem.x_lower[ebi])
×
139
        if self.eq_constrained:
1✔
140
            c_out = np.append(c_out, c_in[eci] - self.problem.c_lower[eci])
1✔
141

142
        if self.lower_bounded:
1✔
143
            c_out = np.append(c_out, x[lbi] - self.problem.x_lower[lbi])
1✔
144
        if self.upper_bounded:
1✔
145
            c_out = np.append(c_out, self.problem.x_upper[ubi] - x[ubi])
×
146
        if self.lower_constrained:
1✔
147
            c_out = np.append(c_out, c_in[lci] - self.problem.c_lower[lci])
1✔
148
        if self.upper_constrained:
1✔
149
            c_out = np.append(c_out, self.problem.c_upper[uci] - c_in[uci])
×
150

151
        return c_out
1✔
152

153
    def jac(self, x):
1✔
154
        nx = self.nx
1✔
155
        ebi = self.eq_bound_indices
1✔
156
        lbi = self.lower_bound_indices
1✔
157
        ubi = self.upper_bound_indices
1✔
158

159
        if self.problem.constrained:
1✔
160
            eci = self.eq_constraint_indices
1✔
161
            lci = self.lower_constraint_indices
1✔
162
            uci = self.upper_constraint_indices
1✔
163
            # Compute problem constraint Jacobian
164
            j_in = self.jac_in(x)
1✔
165

166
        j_out = np.empty((1, nx), dtype=float)
1✔
167

168
        if self.eq_bounded:
1✔
169
            j_out = np.append(j_out, np.identity(nx)[ebi], axis=0)
×
170
        if self.eq_constrained:
1✔
171
            j_out = np.append(j_out, j_in[eci], axis=0)
1✔
172

173
        if self.lower_bounded:
1✔
174
            j_out = np.append(j_out, np.identity(nx)[lbi], axis=0)
1✔
175
        if self.upper_bounded:
1✔
176
            j_out = np.append(j_out, -np.identity(nx)[ubi], axis=0)
×
177
        if self.lower_constrained:
1✔
178
            j_out = np.append(j_out, j_in[lci], axis=0)
1✔
179
        if self.upper_constrained:
1✔
180
            j_out = np.append(j_out, -j_in[uci], axis=0)
×
181

182
        return j_out[1:]
1✔
183

184
    # Minimize A.L. w.r.t. slacks s.t. s >= 0
185
    def reset_slacks(self, c, pi, rho):
1✔
186
        rho_zero_indices = np.where(rho == 0)[0]
1✔
187
        rho_nonzero_indices = np.where(rho > 0)[0]
1✔
188
        s = np.zeros((len(rho), ))
1✔
189

190
        if len(rho_zero_indices) > 0:
1✔
191
            s[rho_zero_indices] = np.maximum(0, c[rho_zero_indices])
1✔
192

193
        if len(rho_nonzero_indices) > 0:
1✔
194
            c_rnz = c[rho_nonzero_indices]
×
195
            pi_rnz = pi[rho_nonzero_indices]
×
196
            rho_rnz = rho[rho_nonzero_indices]
×
197
            s[rho_nonzero_indices] = np.maximum(
×
198
                0, (c_rnz - pi_rnz / rho_rnz))
199

200
        return s
1✔
201

202
    def update_scalar_rho(self, rho_k, dir_deriv_al, pTHp, p_pi, c_k, s_k):
1✔
203
        nc = self.nc
1✔
204
        delta_rho = self.delta_rho
1✔
205

206
        # rho_ref = np.linalg.norm(rho_k)
207
        rho_ref = rho_k[0] * 1.
1✔
208

209
        if dir_deriv_al <= -0.5 * pTHp:
1✔
210
            rho_computed = rho_k[0]
1✔
211
        else:
212
            # note: scalar rho_min here
213
            rho_min = 2 * np.linalg.norm(p_pi) / np.linalg.norm(c_k -
×
214
                                                                s_k)
215
            rho_computed = max(rho_min, 2 * rho_k[0])
×
216
            # rho[:] = np.max(rho_min, 2 * rho[0]) * np.ones((m,))
217

218
        # Damping for rho (Note: vector rho is still not updated)
219
        if rho_k[0] < 4 * (rho_computed + delta_rho):
1✔
220
            rho_damped = rho_k[0]
1✔
221
        else:
222
            rho_damped = np.sqrt(rho_k[0] * (rho_computed + delta_rho))
×
223

224
        rho_new = max(rho_computed, rho_damped)
1✔
225
        rho_k[:] = rho_new * np.ones((nc, ))
1✔
226

227
        # Increasing rho
228
        if rho_k[0] > rho_ref:
1✔
229
            if self.num_rho_changes >= 0:
×
230
                self.num_rho_changes += 1
×
231
            else:
232
                self.delta_rho *= 2.
×
233
                self.num_rho_changes = 0
×
234

235
        # Decreasing rho
236
        elif rho_k[0] < rho_ref:
1✔
237
            if self.num_rho_changes <= 0:
×
238
                self.num_rho_changes -= 1
×
239
            else:
240
                self.delta_rho *= 2.
×
241
                self.num_rho_changes = 0
×
242

243
        return rho_k
1✔
244

245
    def update_vector_rho(self, rho_k, dir_deriv_al, pTHp, p_pi, c_k, s_k):
1✔
UNCOV
246
        delta_rho = self.delta_rho
×
247

248
        rho_ref = np.linalg.norm(rho_k)
×
249
        # rho_ref = rho_k[0] * 1.
250

251
        # ck_sk =
252
        a = -np.power(c_k - s_k, 2)
×
253
        b = -0.5 * pTHp - dir_deriv_al + np.inner(a, rho_k)
×
254

255
        well_defined_a = True
×
256
        # print('a*sgn(b)', a * np.sign(b))
257

258
        if b > 0:
×
259
            a_plus = np.maximum(a, 0)
×
260
            a_plus_norm = np.linalg.norm(a_plus)
×
261
            if a_plus_norm == 0:
×
262
                well_defined_a = False
×
263
            else:
264
                rho_computed = (b / (a_plus_norm**2)) * a_plus
×
265
        elif b < 0:
×
266
            a_minus = -np.minimum(a, 0)
×
267
            a_minus_norm = np.linalg.norm(a_minus)
×
268
            if a_minus_norm == 0:
×
269
                well_defined_a = False
×
270
            else:
271
                rho_computed = -(b / (a_minus_norm**2)) * a_minus
×
272

273
        # TODO: check the condition below
274
        else:  # When b = 0
275
            rho_computed = np.zeros((len(rho_k), ))
×
276

277
        if not (well_defined_a):  # Perform a scalar rho update if a = 0
×
278
            if dir_deriv_al <= -0.5 * pTHp:
×
279
                rho_computed = rho_k * 1.
×
280
            else:
281
                # note: scalar rho_min here
282
                rho_min = 2 * np.linalg.norm(p_pi) / np.linalg.norm(
×
283
                    c_k - s_k)
284
                rho_computed = np.maximum(2 * rho_k, rho_min)
×
285

286
        # Damping for rho (Note: vector rho is still not updated)
287
        rho_damped = np.where(
×
288
            rho_k < 4 * (rho_computed + delta_rho), rho_k,
289
            np.sqrt(rho_k * (rho_computed + delta_rho)))
290

291
        rho_new = np.maximum(rho_computed, rho_damped)
×
292
        rho_k[:] = rho_new
×
293

294
        rho_norm = np.linalg.norm(rho_k)
×
295
        # Increasing rho
296
        if rho_norm > rho_ref:
×
297
            if self.num_rho_changes >= 0:
×
298
                self.num_rho_changes += 1
×
299
            else:
300
                self.delta_rho *= 2.
×
301
                self.num_rho_changes = 0
×
302

303
        # Decreasing rho
304
        elif rho_norm < rho_ref:
×
305
            if self.num_rho_changes <= 0:
×
306
                self.num_rho_changes -= 1
×
307
            else:
308
                self.delta_rho *= 2.
×
309
                self.num_rho_changes = 0
×
310

311
        return rho_k
×
312

313
    def opt_check(self, pi, c, g, j):
1✔
314
        opt_tol = self.options['opt_tol']
×
315

316
        if self.nc == 0:
×
317
            opt_tol_factor = 1.
×
318
            nonneg_violation = 0.
×
319
            compl_violation = 0.
×
320
            stat_violation = np.linalg.norm(g, np.inf)
×
321
            
322
        else:
323
            opt_tol_factor = (1 + np.linalg.norm(pi, np.inf))
×
324
            nonneg_violation = max(0., -np.min(pi))
×
325
            # Note: compl_violation can be negative(Question on SNOPT);
326
            #       Other 2 violations are always nonnegative
327
            compl_violation = np.max(c * pi)
×
328
            stat_violation = np.linalg.norm(g - j.T @ pi, np.inf)
×
329

330
        
331
        scaled_opt_tol = opt_tol * opt_tol_factor
×
332
        opt_check1 = (nonneg_violation <= scaled_opt_tol)
×
333
        opt_check2 = (compl_violation <= scaled_opt_tol)
×
334
        opt_check3 = (stat_violation <= scaled_opt_tol)
×
335

336
        # opt is always nonnegative
337
        opt = max(nonneg_violation, compl_violation,
×
338
                  stat_violation) / opt_tol_factor
339
        opt_satisfied = (opt_check1 and opt_check2 and opt_check3)
×
340

341
        return opt_satisfied, opt
×
342

343
    def feas_check(self, x, c):
1✔
344
        feas_tol = self.options['feas_tol']
×
345

346
        feas_tol_factor = (1 + np.linalg.norm(x, np.inf))
×
347
        scaled_feas_tol = feas_tol * feas_tol_factor
×
348

349
        # Violation is positive
350
        max_con_violation = max(0., -np.min(c))
×
351
        feas_check = (max_con_violation <= scaled_feas_tol)
×
352

353
        feas = max_con_violation / feas_tol_factor
×
354
        feas_satisfied = feas_check
×
355
        return feas_satisfied, feas
×
356

357
    def solve(self):
1✔
358
        # Assign shorter names to variables and methods
359
        nx = self.nx
1✔
360
        nc = self.nc
1✔
361
        nc_e = self.nc_e
1✔
362
        nc_i = self.nc_i
1✔
363

364
        x0 = self.problem.x0
1✔
365
        maxiter = self.options['maxiter']
1✔
366

367
        obj = self.obj
1✔
368
        grad = self.grad
1✔
369
        con = self.con
1✔
370
        jac = self.jac
1✔
371

372
        LSS = self.LSS
1✔
373
        LSB = self.LSB
1✔
374
        QN = self.QN
1✔
375
        MF = self.MF
1✔
376

377
        start_time = time.time()
1✔
378

379
        # Set intial values for current iterates
380
        x_k = x0 * 1.
1✔
381
        # pi_k = np.full((nc, ), 1.)
382
        pi_k = np.full((nc, ), 0.)
1✔
383

384
        f_k = obj(x_k)
1✔
385
        g_k = grad(x_k)
1✔
386
        c_k = con(x_k)
1✔
387
        J_k = jac(x_k)
1✔
388

389
        if nc > 0:
1✔
390
            print('dim of J_k:', J_k.shape)
1✔
391
            print('rank of J_k:', np.linalg.matrix_rank(J_k))
1✔
392

393
        eq_con_viol_indices   = np.where(c_k[:nc_e] != 0)[0]
1✔
394
        ineq_con_viol_indices = nc_e + np.where(c_k[nc_e:]  < 0)[0]
1✔
395
        con_viol_indices = np.concatenate((eq_con_viol_indices, ineq_con_viol_indices))
1✔
396
        penalty_k = f_k + np.dot(np.absolute(pi_k[con_viol_indices]), np.absolute(c_k[con_viol_indices]))
1✔
397

398
        # # A constraint (eq/ineq) is considered active if its absolute value is less than 1e-12
399
        # iact = np.where(np.abs(c_k) < 1e-12)[0]
400
        
401
        # Active set represented as a boolean vector
402
        iact = np.zeros((nc, ), dtype=bool)
1✔
403
        # Activate only equality constraints initially
404
        iact[:nc_e] = 1
1✔
405

406
        nfev = 1
1✔
407
        ngev = 1
1✔
408

409
        B_k = np.identity(nx)
1✔
410

411
        # Vector of penalty parameters
412
        # rho_k = np.full((nc, ), 1.)
413
        rho_k = np.full((nc, ), 0.)
1✔
414

415
        # Compute slacks by minimizing A.L. s.t. s_k >= 0
416
        # s_k = np.full((nc, ), 1.)
417
        s_k = self.reset_slacks(c_k, pi_k[nc_e:], rho_k[nc_e:])
1✔
418

419
        # s_k = np.maximum(0, c_k) # Use this if rho_k = 0. at start
420

421
        # Vector of design vars., lag. mults., and slack vars.
422
        v_k = np.concatenate((x_k, pi_k, s_k))
1✔
423
        # x_k = v_k[:nx]
424
        # pi_k = v_k[nx:(nx + nc)]
425
        # s_k = v_k[(nx + nc):]
426

427
        # p_k = np.zeros((len(v_k), ))
428
        # p_x = p_k[:nx]
429
        # p_pi = p_k[nx:(nx + nc)]
430
        # p_s = p_k[(nx + nc):]
431

432
        # # QP parameters
433
        # l_k = -c_k
434
        # u_k = self.u_k
435
        # A_k = J_k
436

437
        # Iteration counter
438
        itr = 0
1✔
439

440
        # opt_satisfied, opt = self.opt_check(pi_k, c_k, g_k, J_k)
441
        # if self.nc > 0:
442
        #     feas_satisfied, feas = self.feas_check(x_k, c_k)
443
        # else:
444
        #     feas_satisfied, feas = True, 0.
445
        
446
        # tol_satisfied = (opt_satisfied and feas_satisfied)
447

448
        # Evaluate merit function value
449
        MF.set_rho(rho_k)
1✔
450
        mf_k = MF.evaluate_function(x_k, pi_k, s_k, f_k, c_k)
1✔
451
        mfg_k = MF.evaluate_gradient(x_k, pi_k, s_k, f_k, c_k, g_k, J_k)
1✔
452

453
        # Initializing declared outputs
454
        self.update_outputs(major=0,
1✔
455
                            x=x_k,
456
                            lag_mult=pi_k,
457
                            # slacks=s_k,
458
                            obj=f_k,
459
                            constraints=c_k,
460
                            # opt=opt,
461
                            # feas=feas,
462
                            time=time.time() - start_time,
463
                            nfev=nfev,
464
                            ngev=ngev,
465
                            step=0.,
466
                            # rho=rho_k,
467
                            merit=mf_k)
468
                            # merit=penalty_k)
469

470
        # if self.nc > 0:
471
        #     qp_prob = qpsolvers.Problem(B_k, g_k, np.vstack([A_k, -A_k]), np.vstack([u_k, l_k]))
472
        # else:
473
        #     qp_prob = qpsolvers.Problem(B_k, g_k)
474

475
        # while (not (tol_satisfied) and itr < maxiter):
476
        while itr < maxiter:
1✔
477
            itr_start = time.time()
1✔
478
            itr += 1
1✔
479

480
            # ALGORITHM STARTS HERE
481
            # >>>>>>>>>>>>>>>>>>>>>
482

483
            # Compute the search direction toward the next iterate
484

485
            # Solve QP problem
486
            # qp_sol = qp_prob.solve()
487

488
            # qp_sol = qpsolvers.solve_problem(qp_prob, 'quadprog')
489
            # p_k[:nx] = qp_sol.x
490
            # p_k[nx:(nx + nc)] = (-qp_sol.z) - v_k[nx:nx + nc]
491

492
            eps = 1e-16
1✔
493
            for j in range(self.nc_i + 1): # + 1 is needed to ensure that one execution of the loop is done for unconstrained or equality constrained cases
1✔
494
                J_act = J_k[iact]
1✔
495
                c_act = c_k[iact]
1✔
496
                nc_act = len(c_act)
1✔
497
                kkt_mtx = np.block([[B_k, J_act.T], [J_act, np.zeros((nc_act, nc_act))]])
1✔
498
                kkt_rhs = -np.concatenate([g_k, c_act])
1✔
499
                kkt_sol = np.linalg.solve(kkt_mtx, kkt_rhs)
1✔
500

501
                p_x    =  kkt_sol[:nx]
1✔
502
                pi_new = np.zeros((nc, ))
1✔
503
                pi_new[iact] = -kkt_sol[nx:]
1✔
504
                p_pi  = pi_new - pi_k
1✔
505
                c_new  = c_k + J_k @ p_x
1✔
506

507
                # ineq_qp_con_viol_indices = np.where(c_new[nc_e:] < 0)[0]
508
                # iact[nc_e:] = c_new[nc_e:] < 0
509
                iact[nc_e:] = np.where(c_new[nc_e:] <  -eps, 1, iact[nc_e:])
1✔
510
                i_satisfied = np.where(c_new[nc_e:] >= -eps)[0]
1✔
511
                iact[nc_e:][i_satisfied] = np.where(pi_new[nc_e:][i_satisfied] < -eps, 0, iact[nc_e:][i_satisfied])
1✔
512
                # pi_new      = np.where(pi_new[nc_e:] < 0, 0, pi_new[nc_e:])
513

514
                ########## NEW ADDITION ##########
515

516

517
                ########## NEW ADDITION ##########
518
                
519
                if all(c_new[nc_e:] >= -eps) and all(pi_new[nc_e:] >= -eps):
1✔
520
                    print('All constraints satisfied with no negative multipliers '\
1✔
521
                          'at major iteration:', itr, 'with minor iterations:', j)
522
                    break
1✔
523

524
                # pi_new = np.where(kkt_sol[nx:] < 0, 0, kkt_sol[nx:])
525
                # p_x = np.linalg.solve(B_k, -g_k) if any(kkt_sol[nx:] < 0) else p_x
526
                # # p_x = np.linalg.solve(B_k, -g_k - J_k.T @ pi_new)
527
                # # p_pi = pi_new - pi_k
528

529
            else:
530
                print(f'Maximum number of minor iterations ({j}) reached at major iteration:', itr)
1✔
531

532
            # x_new  = x_k + p_x
533
            # f_new = obj(x_new)
534
            # g_new = grad(x_new)
535
            # c_new = con(x_new)
536
            # J_new = jac(x_new)
537

538
            # eq_con_viol_indices   = np.where(c_new[:nc_e] != 0)[0]
539
            # ineq_con_viol_indices = nc_e + np.where(c_new[nc_e:]  < 0)[0]
540
            # con_viol_indices = np.concatenate((eq_con_viol_indices, ineq_con_viol_indices))
541
            # penalty_new = f_new + np.dot(np.absolute(pi_new[con_viol_indices]) + 1, np.absolute(c_new[con_viol_indices]))
542

543
            # ls_count = 0
544
            # while penalty_new > penalty_k:
545
            #     ls_count += 1
546
            #     p_x = 0.5 * p_x
547
            #     x_new  = x_k + p_x
548
            #     f_new = obj(x_new)
549
            #     # g_new = grad(x_new)
550
            #     c_new = con(x_new)
551
            #     # J_new = jac(x_new)
552

553
            #     eq_con_viol_indices   = np.where(c_new[:nc_e] != 0)[0]
554
            #     ineq_con_viol_indices = nc_e + np.where(c_new[nc_e:]  < 0)[0]
555
            #     con_viol_indices = np.concatenate((eq_con_viol_indices, ineq_con_viol_indices))
556
            #     penalty_new = f_new + np.dot(np.absolute(pi_new[con_viol_indices]) + 1, np.absolute(c_new[con_viol_indices]))
557

558
            #     if ls_count > 10:
559
            #         break
560
            
561
            # print('step:', .5 ** ls_count)
562
            # print('merit:', penalty_new)
563
            # nfev += ls_count
564

565
            # Search direction for s_k :
566
            # (c_k + J_k @ p_x) is the new estimate for s
567
            # p_k[(nx + nc):] = c_k + J_k @ p_x - s_k
568
            p_s = c_k[nc_e:] + J_k[nc_e:] @ p_x - v_k[nx + nc:]
1✔
569

570
            p_k = np.concatenate([p_x, p_pi, p_s])
1✔
571

572
            dir_deriv_al = np.dot(mfg_k, p_k)
1✔
573
            pTHp = p_x.T @ (B_k @ p_x)
1✔
574

575
            # Update penalty parameters
576
            if self.nc > 0:
1✔
577
                rho_k = self.update_scalar_rho(rho_k, dir_deriv_al, pTHp,
1✔
578
                                               p_pi, c_k, np.concatenate([np.zeros((nc_e,)), s_k]))
579
                # rho_k = self.update_vector_rho(rho_k, dir_deriv_al, pTHp,
580
                #                                p_pi, c_k, s_k)
581

582
            MF.set_rho(rho_k)
1✔
583
            mf_k  = MF.evaluate_function(x_k, pi_k, s_k, f_k, c_k)
1✔
584
            mfg_k = MF.evaluate_gradient(x_k, pi_k, s_k, f_k, c_k, g_k, J_k)
1✔
585

586
            # Compute the step length along the search direction via a line search
587
            alpha, mf_new, mfg_new, mf_slope_new, new_f_evals, new_g_evals, converged = LSS.search(
1✔
588
                x=v_k, p=p_k, f0=mf_k, g0=mfg_k)
589

590
            # alpha, mf_new, new_f_evals, new_g_evals, converged = LSB.search(
591
            #     x=v_k, p=p_k, f0=mf_k, g0=mfg_k)
592

593
            nfev += new_f_evals
1✔
594
            ngev += new_g_evals
1✔
595

596
            # if not converged:  # Backup: Backtracking LS
597
            #     alpha, mf_new, new_f_evals, new_g_evals, converged = LSB.search(
598
            #         x=v_k, p=p_k, f0=mf_k, g0=mfg_k)
599

600
            #     nfev += new_f_evals
601
            #     ngev += new_g_evals
602

603
            # A step of length 1e-4 is taken along p_k if line search does not converge
604
            if not converged:
1✔
605
                "Compute this factor heuristically"
1✔
606
                alpha = 0.99
1✔
607
                d_k = p_k * 0.1
1✔
608
                print("###### FAILED LINE SEARCH #######")
1✔
609
                nfev += 1  # Commented because of bug in Scipy line search: calls f twice with amax before failure
1✔
610
                ngev += 1
1✔
611

612
            else:
613
                print("###### SUCCESSFUL LINE SEARCH #######")
1✔
614
                d_k = alpha * p_k
1✔
615

616
            v_k += d_k
1✔
617

618
            x_k = v_k[:nx]
1✔
619
            pi_k = v_k[nx:(nx + nc)]
1✔
620
            f_k = obj(x_k)
1✔
621
            g_old = g_k * 1.
1✔
622
            g_k = grad(x_k)
1✔
623
            c_k = con(x_k)
1✔
624
            J_old = J_k * 1.
1✔
625
            J_k = jac(x_k)
1✔
626

627
            v_k[(nx + nc):] = self.reset_slacks(c_k[nc_e:], pi_k[nc_e:], rho_k[nc_e:])
1✔
628
            s_k = v_k[(nx + nc):]
1✔
629

630
            # Note: MF changes (decreases) after slack reset
631
            mf_k = MF.evaluate_function(x_k, pi_k, s_k, f_k, c_k)
1✔
632
            mfg_k = MF.evaluate_gradient(x_k, pi_k, s_k, f_k, c_k, g_k, J_k)
1✔
633
            
634
            # x_k = x_new
635
            # f_k = f_new
636
            # g_k = grad(x_k)
637
            # c_k = c_new
638
            # J_k = jac(x_k)
639
            # penalty_k = penalty_new
640
            # ngev += 1
641

642
            w_k = (g_k - g_old) - (J_k - J_old).T @ pi_k
1✔
643
            p_x = d_k[:nx]
1✔
644
            QN.update(p_x, w_k)
1✔
645

646
            B_k = QN.B_k
1✔
647

648
            # print('########### MAJOR ITERATION ENDS ############')
649

650
            # # <<<<<<<<<<<<<<<<<<<
651
            # # ALGORITHM ENDS HERE
652

653
            # opt_satisfied, opt = self.opt_check(pi_k, c_k, g_k, J_k)
654
            # if self.nc > 0:
655
            #     feas_satisfied, feas = self.feas_check(x_k, c_k)
656
            # else:
657
            #     feas_satisfied, feas = True, 0.
658
            # tol_satisfied = (opt_satisfied and feas_satisfied)
659

660
            # Update arrays inside outputs dict with new values from the current iteration
661

662
            self.update_outputs(
1✔
663
                major=itr,
664
                x=x_k,
665
                lag_mult=pi_k,
666
                # slacks=s_k,
667
                obj=f_k,
668
                constraints=c_k,
669
                # opt=opt,
670
                # feas=feas,
671
                time=time.time() - start_time,
672
                nfev=nfev,
673
                ngev=ngev,
674
                # rho=rho_k,
675
                step=alpha,
676
                # step=0.5**ls_count,
677
                merit=mf_k)
678
                # merit=penalty_k)
679

680
            if np.linalg.norm(p_x) < self.options['tol']:
1✔
681
                break
1✔
682

683
        self.total_time = time.time() - start_time
1✔
684
        # converged = tol_satisfied
685

686
        self.results = {
1✔
687
            'x': x_k,
688
            'objective': f_k,
689
            'c': c_k,
690
            'pi': pi_k,
691
            # 'optimality': opt,
692
            # 'feasibility': feas,
693
            'nfev': nfev,
694
            'ngev': ngev,
695
            'niter': itr,
696
            'time': self.total_time,
697
            'converged': np.linalg.norm(p_x) < self.options['tol']
698
        }
699

700
        # Run post-processing for the Optimizer() base class
701
        self.run_post_processing()
1✔
702

703
        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