1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

"""Trust-region interior point method. 

 

References 

---------- 

.. [1] Byrd, Richard H., Mary E. Hribar, and Jorge Nocedal. 

"An interior point algorithm for large-scale nonlinear 

programming." SIAM Journal on Optimization 9.4 (1999): 877-900. 

.. [2] Byrd, Richard H., Guanghui Liu, and Jorge Nocedal. 

"On the local behavior of an interior point method for 

nonlinear programming." Numerical analysis 1997 (1997): 37-56. 

.. [3] Nocedal, Jorge, and Stephen J. Wright. "Numerical optimization" 

Second Edition (2006). 

""" 

 

from __future__ import division, print_function, absolute_import 

import scipy.sparse as sps 

import numpy as np 

from .equality_constrained_sqp import equality_constrained_sqp 

from scipy.sparse.linalg import LinearOperator 

 

__all__ = ['tr_interior_point'] 

 

 

class BarrierSubproblem: 

""" 

Barrier optimization problem: 

minimize fun(x) - barrier_parameter*sum(log(s)) 

subject to: constr_eq(x) = 0 

constr_ineq(x) + s = 0 

""" 

 

def __init__(self, x0, s0, fun, grad, lagr_hess, n_vars, n_ineq, n_eq, 

constr, jac, barrier_parameter, tolerance, 

enforce_feasibility, global_stop_criteria, 

xtol, fun0, grad0, constr_ineq0, jac_ineq0, constr_eq0, 

jac_eq0): 

# Store parameters 

self.n_vars = n_vars 

self.x0 = x0 

self.s0 = s0 

self.fun = fun 

self.grad = grad 

self.lagr_hess = lagr_hess 

self.constr = constr 

self.jac = jac 

self.barrier_parameter = barrier_parameter 

self.tolerance = tolerance 

self.n_eq = n_eq 

self.n_ineq = n_ineq 

self.enforce_feasibility = enforce_feasibility 

self.global_stop_criteria = global_stop_criteria 

self.xtol = xtol 

self.fun0 = self._compute_function(fun0, constr_ineq0, s0) 

self.grad0 = self._compute_gradient(grad0) 

self.constr0 = self._compute_constr(constr_ineq0, constr_eq0, s0) 

self.jac0 = self._compute_jacobian(jac_eq0, jac_ineq0, s0) 

self.terminate = False 

 

def update(self, barrier_parameter, tolerance): 

self.barrier_parameter = barrier_parameter 

self.tolerance = tolerance 

 

def get_slack(self, z): 

return z[self.n_vars:self.n_vars+self.n_ineq] 

 

def get_variables(self, z): 

return z[:self.n_vars] 

 

def function_and_constraints(self, z): 

"""Returns barrier function and constraints at given point. 

 

For z = [x, s], returns barrier function: 

function(z) = fun(x) - barrier_parameter*sum(log(s)) 

and barrier constraints: 

constraints(z) = [ constr_eq(x) ] 

[ constr_ineq(x) + s ] 

 

""" 

# Get variables and slack variables 

x = self.get_variables(z) 

s = self.get_slack(z) 

# Compute function and constraints 

f = self.fun(x) 

c_eq, c_ineq = self.constr(x) 

# Return objective function and constraints 

return (self._compute_function(f, c_ineq, s), 

self._compute_constr(c_ineq, c_eq, s)) 

 

def _compute_function(self, f, c_ineq, s): 

# Use technique from Nocedal and Wright book, ref [3]_, p.576, 

# to guarantee constraints from `enforce_feasibility` 

# stay feasible along iterations. 

s[self.enforce_feasibility] = -c_ineq[self.enforce_feasibility] 

log_s = [np.log(s_i) if s_i > 0 else -np.inf for s_i in s] 

# Compute barrier objective function 

return f - self.barrier_parameter*np.sum(log_s) 

 

def _compute_constr(self, c_ineq, c_eq, s): 

# Compute barrier constraint 

return np.hstack((c_eq, 

c_ineq + s)) 

 

def scaling(self, z): 

"""Returns scaling vector. 

Given by: 

scaling = [ones(n_vars), s] 

""" 

s = self.get_slack(z) 

diag_elements = np.hstack((np.ones(self.n_vars), s)) 

 

# Diagonal Matrix 

def matvec(vec): 

return diag_elements*vec 

return LinearOperator((self.n_vars+self.n_ineq, 

self.n_vars+self.n_ineq), 

matvec) 

 

def gradient_and_jacobian(self, z): 

"""Returns scaled gradient. 

 

Return scalled gradient: 

gradient = [ grad(x) ] 

[ -barrier_parameter*ones(n_ineq) ] 

and scaled Jacobian Matrix: 

jacobian = [ jac_eq(x) 0 ] 

[ jac_ineq(x) S ] 

Both of them scaled by the previously defined scaling factor. 

""" 

# Get variables and slack variables 

x = self.get_variables(z) 

s = self.get_slack(z) 

# Compute first derivatives 

g = self.grad(x) 

J_eq, J_ineq = self.jac(x) 

# Return gradient and jacobian 

return (self._compute_gradient(g), 

self._compute_jacobian(J_eq, J_ineq, s)) 

 

def _compute_gradient(self, g): 

return np.hstack((g, -self.barrier_parameter*np.ones(self.n_ineq))) 

 

def _compute_jacobian(self, J_eq, J_ineq, s): 

if self.n_ineq == 0: 

return J_eq 

else: 

if sps.issparse(J_eq) or sps.issparse(J_ineq): 

# It is expected that J_eq and J_ineq 

# are already `csr_matrix` because of 

# the way ``BoxConstraint``, ``NonlinearConstraint`` 

# and ``LinearConstraint`` are defined. 

J_eq = sps.csr_matrix(J_eq) 

J_ineq = sps.csr_matrix(J_ineq) 

return self._assemble_sparse_jacobian(J_eq, J_ineq, s) 

else: 

S = np.diag(s) 

zeros = np.zeros((self.n_eq, self.n_ineq)) 

# Convert to matrix 

if sps.issparse(J_ineq): 

J_ineq = J_ineq.toarray() 

if sps.issparse(J_eq): 

J_eq = J_eq.toarray() 

# Concatenate matrices 

return np.block([[J_eq, zeros], 

[J_ineq, S]]) 

 

def _assemble_sparse_jacobian(self, J_eq, J_ineq, s): 

"""Assemble sparse jacobian given its components. 

 

Given ``J_eq``, ``J_ineq`` and ``s`` returns: 

jacobian = [ J_eq, 0 ] 

[ J_ineq, diag(s) ] 

 

It is equivalent to: 

sps.bmat([[ J_eq, None ], 

[ J_ineq, diag(s) ]], "csr") 

but significantly more efficient for this 

given structure. 

""" 

n_vars, n_ineq, n_eq = self.n_vars, self.n_ineq, self.n_eq 

J_aux = sps.vstack([J_eq, J_ineq], "csr") 

indptr, indices, data = J_aux.indptr, J_aux.indices, J_aux.data 

new_indptr = indptr + np.hstack((np.zeros(n_eq, dtype=int), 

np.arange(n_ineq+1, dtype=int))) 

size = indices.size+n_ineq 

new_indices = np.empty(size) 

new_data = np.empty(size) 

mask = np.full(size, False, bool) 

mask[new_indptr[-n_ineq:]-1] = True 

new_indices[mask] = n_vars+np.arange(n_ineq) 

new_indices[~mask] = indices 

new_data[mask] = s 

new_data[~mask] = data 

J = sps.csr_matrix((new_data, new_indices, new_indptr), 

(n_eq + n_ineq, n_vars + n_ineq)) 

return J 

 

def lagrangian_hessian_x(self, z, v): 

"""Returns Lagrangian Hessian (in relation to `x`) -> Hx""" 

x = self.get_variables(z) 

# Get lagrange multipliers relatated to nonlinear equality constraints 

v_eq = v[:self.n_eq] 

# Get lagrange multipliers relatated to nonlinear ineq. constraints 

v_ineq = v[self.n_eq:self.n_eq+self.n_ineq] 

lagr_hess = self.lagr_hess 

return lagr_hess(x, v_eq, v_ineq) 

 

def lagrangian_hessian_s(self, z, v): 

"""Returns scaled Lagrangian Hessian (in relation to`s`) -> S Hs S""" 

s = self.get_slack(z) 

# Using the primal formulation: 

# S Hs S = diag(s)*diag(barrier_parameter/s**2)*diag(s). 

# Reference [1]_ p. 882, formula (3.1) 

primal = self.barrier_parameter 

# Using the primal-dual formulation 

# S Hs S = diag(s)*diag(v/s)*diag(s) 

# Reference [1]_ p. 883, formula (3.11) 

primal_dual = v[-self.n_ineq:]*s 

# Uses the primal-dual formulation for 

# positives values of v_ineq, and primal 

# formulation for the remaining ones. 

return np.where(v[-self.n_ineq:] > 0, primal_dual, primal) 

 

def lagrangian_hessian(self, z, v): 

"""Returns scaled Lagrangian Hessian""" 

# Compute Hessian in relation to x and s 

Hx = self.lagrangian_hessian_x(z, v) 

if self.n_ineq > 0: 

S_Hs_S = self.lagrangian_hessian_s(z, v) 

 

# The scaled Lagragian Hessian is: 

# [ Hx 0 ] 

# [ 0 S Hs S ] 

def matvec(vec): 

vec_x = self.get_variables(vec) 

vec_s = self.get_slack(vec) 

if self.n_ineq > 0: 

return np.hstack((Hx.dot(vec_x), S_Hs_S*vec_s)) 

else: 

return Hx.dot(vec_x) 

return LinearOperator((self.n_vars+self.n_ineq, 

self.n_vars+self.n_ineq), 

matvec) 

 

def stop_criteria(self, state, z, last_iteration_failed, 

optimality, constr_violation, 

trust_radius, penalty, cg_info): 

"""Stop criteria to the barrier problem. 

The criteria here proposed is similar to formula (2.3) 

from [1]_, p.879. 

""" 

x = self.get_variables(z) 

if self.global_stop_criteria(state, x, 

last_iteration_failed, 

trust_radius, penalty, 

cg_info, 

self.barrier_parameter, 

self.tolerance): 

self.terminate = True 

return True 

else: 

g_cond = (optimality < self.tolerance and 

constr_violation < self.tolerance) 

x_cond = trust_radius < self.xtol 

return g_cond or x_cond 

 

 

def tr_interior_point(fun, grad, lagr_hess, n_vars, n_ineq, n_eq, 

constr, jac, x0, fun0, grad0, 

constr_ineq0, jac_ineq0, constr_eq0, 

jac_eq0, stop_criteria, 

enforce_feasibility, xtol, state, 

initial_barrier_parameter, 

initial_tolerance, 

initial_penalty, 

initial_trust_radius, 

factorization_method): 

"""Trust-region interior points method. 

 

Solve problem: 

minimize fun(x) 

subject to: constr_ineq(x) <= 0 

constr_eq(x) = 0 

using trust-region interior point method described in [1]_. 

""" 

# BOUNDARY_PARAMETER controls the decrease on the slack 

# variables. Represents ``tau`` from [1]_ p.885, formula (3.18). 

BOUNDARY_PARAMETER = 0.995 

# BARRIER_DECAY_RATIO controls the decay of the barrier parameter 

# and of the subproblem toloerance. Represents ``theta`` from [1]_ p.879. 

BARRIER_DECAY_RATIO = 0.2 

# TRUST_ENLARGEMENT controls the enlargement on trust radius 

# after each iteration 

TRUST_ENLARGEMENT = 5 

 

# Default enforce_feasibility 

if enforce_feasibility is None: 

enforce_feasibility = np.zeros(n_ineq, bool) 

# Initial Values 

barrier_parameter = initial_barrier_parameter 

tolerance = initial_tolerance 

trust_radius = initial_trust_radius 

# Define initial value for the slack variables 

s0 = np.maximum(-1.5*constr_ineq0, np.ones(n_ineq)) 

# Define barrier subproblem 

subprob = BarrierSubproblem( 

x0, s0, fun, grad, lagr_hess, n_vars, n_ineq, n_eq, constr, jac, 

barrier_parameter, tolerance, enforce_feasibility, 

stop_criteria, xtol, fun0, grad0, constr_ineq0, jac_ineq0, 

constr_eq0, jac_eq0) 

# Define initial parameter for the first iteration. 

z = np.hstack((x0, s0)) 

fun0_subprob, constr0_subprob = subprob.fun0, subprob.constr0 

grad0_subprob, jac0_subprob = subprob.grad0, subprob.jac0 

# Define trust region bounds 

trust_lb = np.hstack((np.full(subprob.n_vars, -np.inf), 

np.full(subprob.n_ineq, -BOUNDARY_PARAMETER))) 

trust_ub = np.full(subprob.n_vars+subprob.n_ineq, np.inf) 

 

# Solves a sequence of barrier problems 

while True: 

# Solve SQP subproblem 

z, state = equality_constrained_sqp( 

subprob.function_and_constraints, 

subprob.gradient_and_jacobian, 

subprob.lagrangian_hessian, 

z, fun0_subprob, grad0_subprob, 

constr0_subprob, jac0_subprob, subprob.stop_criteria, 

state, initial_penalty, trust_radius, 

factorization_method, trust_lb, trust_ub, subprob.scaling) 

if subprob.terminate: 

break 

# Update parameters 

trust_radius = max(initial_trust_radius, 

TRUST_ENLARGEMENT*state.tr_radius) 

# TODO: Use more advanced strategies from [2]_ 

# to update this parameters. 

barrier_parameter *= BARRIER_DECAY_RATIO 

tolerance *= BARRIER_DECAY_RATIO 

# Update Barrier Problem 

subprob.update(barrier_parameter, tolerance) 

# Compute initial values for next iteration 

fun0_subprob, constr0_subprob = subprob.function_and_constraints(z) 

grad0_subprob, jac0_subprob = subprob.gradient_and_jacobian(z) 

 

# Get x and s 

x = subprob.get_variables(z) 

return x, state