-
-
Notifications
You must be signed in to change notification settings - Fork 5.1k
/
tr_interior_point.py
346 lines (312 loc) · 13.5 KB
/
tr_interior_point.py
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
"""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).
"""
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 scaled 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