Skip to content

Commit 24aac02

Browse files
authored
remove unnecessary point in dom + clean up C_problem + clean up oracle (#168)
* remove unnecessary point in dom + clean up C_problem + clean up oracle * fix william's comments
1 parent 31b7ab3 commit 24aac02

13 files changed

Lines changed: 88 additions & 173 deletions

File tree

cvxpy/atoms/affine/binary_operators.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -560,9 +560,6 @@ def is_decr(self, idx) -> bool:
560560
else:
561561
return self.args[0].is_nonneg()
562562

563-
def point_in_domain(self):
564-
return np.ones(self.args[1].shape)
565-
566563
def graph_implementation(
567564
self, arg_objs, shape: Tuple[int, ...], data=None
568565
) -> Tuple[lo.LinOp, List[Constraint]]:

cvxpy/atoms/atom.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -550,10 +550,6 @@ def _domain(self) -> List['Constraint']:
550550
# Default is no constraints.
551551
return []
552552

553-
def point_in_domain(self) -> np.ndarray:
554-
"""default point in domain of zero"""
555-
return np.zeros(self.shape)
556-
557553
@staticmethod
558554
def numpy_numeric(numeric_func):
559555
"""Wraps an atom's numeric function that requires numpy ndarrays as input.

cvxpy/atoms/elementwise/log.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,5 +109,4 @@ def _domain(self) -> List[Constraint]:
109109
def point_in_domain(self) -> np.ndarray:
110110
"""Returns a point in the domain of the node.
111111
"""
112-
dim = (1, ) if self.size == 1 else self.shape
113-
return np.ones(dim)
112+
return np.ones(self.shape)

cvxpy/atoms/elementwise/logistic.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,3 @@ def _grad(self, values):
8282
cols = self.size
8383
grad_vals = np.exp(values[0] - np.logaddexp(0, values[0]))
8484
return [logistic.elemwise_grad_to_diag(grad_vals, rows, cols)]
85-
86-
def point_in_domain(self):
87-
return np.zeros(self.shape)

cvxpy/atoms/elementwise/power.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -400,11 +400,6 @@ def _domain(self) -> List[Constraint]:
400400
else:
401401
return []
402402

403-
def point_in_domain(self) -> np.ndarray:
404-
"""Returns a point in the domain of the node.
405-
"""
406-
return np.ones(self.shape)
407-
408403
def get_data(self):
409404
return [self._p_orig, self.max_denom]
410405

cvxpy/reductions/solvers/nlp_solvers/copt_nlpif.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,11 @@ def solve_via_data(self, data, warm_start: bool, verbose: bool, solver_opts, sol
122122
use_hessian = True
123123

124124
if solver_cache is None:
125-
oracles = Oracles(bounds.new_problem, bounds.x0, len(bounds.cl),
126-
verbose=verbose, use_hessian=use_hessian)
125+
oracles = Oracles(bounds.new_problem, verbose=verbose, use_hessian=use_hessian)
127126
elif 'oracles' in solver_cache:
128127
oracles = solver_cache['oracles']
129128
else:
130-
oracles = Oracles(bounds.new_problem, bounds.x0, len(bounds.cl),
131-
verbose=verbose, use_hessian=use_hessian)
129+
oracles = Oracles(bounds.new_problem, verbose=verbose, use_hessian=use_hessian)
132130
solver_cache['oracles'] = oracles
133131

134132
class COPTNlpCallbackCVXPY(copt.NlpCallbackBase):

cvxpy/reductions/solvers/nlp_solvers/diff_engine/c_problem.py

Lines changed: 27 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
"""
1717

1818
import numpy as np
19-
from scipy import sparse
2019

2120
import cvxpy as cp
2221

@@ -42,31 +41,21 @@ def __init__(self, cvxpy_problem: cp.Problem, verbose: bool = True):
4241
c_obj = convert_expr(cvxpy_problem.objective.expr, var_dict, n_vars)
4342
c_constraints = [convert_expr(c.expr, var_dict, n_vars) for c in cvxpy_problem.constraints]
4443
self._capsule = _diffengine.make_problem(c_obj, c_constraints, verbose)
45-
self._jacobian_allocated = False
46-
self._hessian_allocated = False
4744

48-
def init_jacobian(self):
49-
"""Initialize Jacobian structures only. Must be called before jacobian()."""
50-
_diffengine.problem_init_jacobian(self._capsule)
51-
self._jacobian_allocated = True
52-
5345
def init_jacobian_coo(self):
54-
"""Initialize Jacobian COO structures only.
46+
"""Fill sparsity for the constraint Jacobian in COO format.
5547
56-
Must be called before get_jacobian_sparsity_coo().
48+
Must be called once before get_jacobian_sparsity_coo() or eval_jacobian_vals().
5749
"""
5850
_diffengine.problem_init_jacobian_coo(self._capsule)
59-
self._jacobian_allocated = True
60-
61-
def init_hessian(self):
62-
"""Initialize Hessian structures only. Must be called before hessian()."""
63-
_diffengine.problem_init_hessian(self._capsule)
64-
self._hessian_allocated = True
6551

6652
def init_hessian_coo_lower_tri(self):
67-
"""Initialize Hessian COO structures only. Must be called before get_hessian()."""
53+
"""Fill sparsity for the Lagrangian Hessian (lower triangle, COO).
54+
55+
Must be called once before get_problem_hessian_sparsity_coo() or
56+
eval_hessian_vals_coo_lower_tri().
57+
"""
6858
_diffengine.problem_init_hessian_coo_lower_triangular(self._capsule)
69-
self._hessian_allocated = True
7059

7160
def objective_forward(self, u: np.ndarray) -> float:
7261
"""Evaluate objective. Returns obj_value float."""
@@ -79,63 +68,43 @@ def constraint_forward(self, u: np.ndarray) -> np.ndarray:
7968
def gradient(self) -> np.ndarray:
8069
"""Compute gradient of objective. Call objective_forward first. Returns gradient array."""
8170
return _diffengine.problem_gradient(self._capsule)
82-
83-
def jacobian(self) -> sparse.csr_matrix:
84-
"""Compute constraint Jacobian. Call constraint_forward first."""
85-
data, indices, indptr, shape = _diffengine.problem_jacobian(self._capsule)
86-
return sparse.csr_matrix((data, indices, indptr), shape=shape)
8771

8872
def get_jacobian_sparsity_coo(self) -> tuple[np.ndarray, np.ndarray]:
89-
"""Get Jacobian sparsity pattern as COO. This function does not evaluate the jacobian."""
73+
"""Return the sparsity pattern (row, col) of the constraint Jacobian.
74+
75+
Does not evaluate the Jacobian; only returns structural nonzero indices.
76+
Call init_jacobian_coo() first.
77+
"""
9078
rows, cols, unused_shape = _diffengine.get_jacobian_sparsity_coo(self._capsule)
9179
return rows, cols
9280

9381
def eval_jacobian_vals(self) -> np.ndarray:
94-
"""Evaluate Jacobian values only.
82+
"""Evaluate the constraint Jacobian and return its nonzero values.
9583
96-
Call constraint_forward first. Returns jacobian values array.
84+
The values correspond to the sparsity pattern from get_jacobian_sparsity_coo().
85+
Call constraint_forward() first to set the evaluation point.
9786
"""
9887
return _diffengine.problem_eval_jacobian_vals(self._capsule)
9988

100-
def get_jacobian(self) -> sparse.csr_matrix:
101-
"""Get constraint Jacobian. This function does not evaluate the jacobian. """
102-
data, indices, indptr, shape = _diffengine.get_jacobian(self._capsule)
103-
return sparse.csr_matrix((data, indices, indptr), shape=shape)
104-
10589
def get_problem_hessian_sparsity_coo(self) -> tuple[np.ndarray, np.ndarray]:
106-
"""Get Hessian sparsity pattern as COO. This function does not evaluate the hessian."""
90+
"""Return the sparsity pattern (row, col) of the lower-triangular Lagrangian Hessian.
91+
92+
Does not evaluate the Hessian; only returns structural nonzero indices.
93+
Call init_hessian_coo_lower_tri() first.
94+
"""
10795
rows, cols, unused_shape = _diffengine.get_problem_hessian_sparsity_coo(self._capsule)
10896
return rows, cols
109-
97+
11098
def eval_hessian_vals_coo_lower_tri(
11199
self, obj_factor: float, lagrange: np.ndarray
112100
) -> np.ndarray:
113-
"""Evaluate Hessian values only for lower triangular part.
114-
115-
Call objective_forward and constraint_forward first.
116-
"""
117-
return _diffengine.problem_eval_hessian_vals_coo(self._capsule, obj_factor, lagrange)
101+
"""Evaluate the lower-triangular Lagrangian Hessian and return its nonzero values.
118102
119-
def hessian(self, obj_factor: float, lagrange: np.ndarray) -> sparse.csr_matrix:
120-
"""Compute Lagrangian Hessian.
103+
Computes obj_factor * hess_f + sum(lagrange[i] * hess_gi), where f is the objective
104+
and gi are the constraints. The values correspond to the sparsity pattern from
105+
get_problem_hessian_sparsity_coo(). Only the lower triangle is returned.
121106
122-
Computes: obj_factor * H_obj + sum(lagrange_i * H_constraint_i)
123-
124-
Call objective_forward and constraint_forward before this.
125-
126-
Args:
127-
obj_factor: Weight for objective Hessian
128-
lagrange: Array of Lagrange multipliers (length = total_constraint_size)
129-
130-
Returns:
131-
scipy CSR matrix of shape (n_vars, n_vars)
107+
Call objective_forward() and constraint_forward() first to set the evaluation point.
132108
"""
133-
data, indices, indptr, shape = _diffengine.problem_hessian(
134-
self._capsule, obj_factor, lagrange
135-
)
136-
return sparse.csr_matrix((data, indices, indptr), shape=shape)
109+
return _diffengine.problem_eval_hessian_vals_coo(self._capsule, obj_factor, lagrange)
137110

138-
def get_hessian(self) -> sparse.csr_matrix:
139-
"""Get Lagrangian Hessian. This function does not evaluate the hessian."""
140-
data, indices, indptr, shape = _diffengine.get_hessian(self._capsule)
141-
return sparse.csr_matrix((data, indices, indptr), shape=shape)

cvxpy/reductions/solvers/nlp_solvers/ipopt_nlpif.py

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,14 @@ def invert(self, solution, inverse_data):
7676
"""
7777
Returns the solution to the original problem given the inverse_data.
7878
"""
79-
attr = {}
79+
attr = {
80+
s.NUM_ITERS: solution.get('num_iters'),
81+
}
8082
status = self.STATUS_MAP[solution['status']]
8183
# the info object does not contain all the attributes we want
8284
# see https://github.com/mechmotum/cyipopt/issues/17
8385
# attr[s.SOLVE_TIME] = solution.solve_time
84-
attr[s.NUM_ITERS] = solution['iterations']
86+
#attr[s.NUM_ITERS] = solution['iterations']
8587
# more detailed statistics here when available
8688
# attr[s.EXTRA_STATS] = solution.extra.FOO
8789
if 'all_objs_from_best_of' in solution:
@@ -150,13 +152,11 @@ def solve_via_data(self, data, warm_start: bool, verbose: bool, solver_opts, sol
150152
use_hessian = (hessian_approx == 'exact')
151153

152154
if solver_cache is None:
153-
oracles = Oracles(bounds.new_problem, bounds.x0, len(bounds.cl),
154-
verbose=verbose, use_hessian=use_hessian)
155+
oracles = Oracles(bounds.new_problem, verbose=verbose, use_hessian=use_hessian)
155156
elif 'oracles' in solver_cache:
156157
oracles = solver_cache['oracles']
157158
else:
158-
oracles = Oracles(bounds.new_problem, bounds.x0, len(bounds.cl),
159-
verbose=verbose, use_hessian=use_hessian)
159+
oracles = Oracles(bounds.new_problem, verbose=verbose, use_hessian=use_hessian)
160160
solver_cache['oracles'] = oracles
161161

162162
nlp = cyipopt.Problem(
@@ -186,16 +186,19 @@ def solve_via_data(self, data, warm_start: bool, verbose: bool, solver_opts, sol
186186
for option_name, option_value in default_options.items():
187187
nlp.add_option(option_name, option_value)
188188

189+
# ipopt will evaluate the gradient of the Lagrangian at the initial point to decide
190+
# without doing the forward pass for the objective and constraints, so we need to do
191+
# a forward pass here to fill in any necessary values for the derivative evaluation.
192+
oracles.objective(data["x0"])
193+
oracles.constraints(data["x0"])
194+
189195
_, info = nlp.solve(data["x0"])
190196

191-
if oracles.iterations == 0 and info['status'] == s.OPTIMAL:
192-
print("Warning: IPOPT returned after 0 iterations. This may indicate that\n"
193-
"the initial point passed to Ipopt is a stationary point, and it is\n"
194-
"quite unlikely that the initial point is also a local minimizer. \n"
195-
"Perturb the initial point and try again.")
196-
197-
# add number of iterations to info dict from oracles
198-
info['iterations'] = oracles.iterations
197+
# cyipopt does currently not expose the number of iterations, see
198+
# https://github.com/mechmotum/cyipopt/issues/17. We set it to "Not available" for now,
199+
# but we should update this when the information becomes available.
200+
info['num_iters'] = "Not available"
201+
199202
return info
200203

201204
def cite(self, data):

cvxpy/reductions/solvers/nlp_solvers/knitro_nlpif.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,13 +177,11 @@ def solve_via_data(self, data, warm_start: bool, verbose: bool, solver_opts, sol
177177
use_hessian = (hessopt == 1)
178178

179179
if solver_cache is None:
180-
oracles = Oracles(bounds.new_problem, bounds.x0, len(bounds.cl),
181-
verbose=verbose, use_hessian=use_hessian)
180+
oracles = Oracles(bounds.new_problem, verbose=verbose, use_hessian=use_hessian)
182181
elif 'oracles' in solver_cache:
183182
oracles = solver_cache['oracles']
184183
else:
185-
oracles = Oracles(bounds.new_problem, bounds.x0, len(bounds.cl),
186-
verbose=verbose, use_hessian=use_hessian)
184+
oracles = Oracles(bounds.new_problem, verbose=verbose, use_hessian=use_hessian)
187185
solver_cache['oracles'] = oracles
188186

189187
# Extract data from the data dictionary

cvxpy/reductions/solvers/nlp_solvers/nlp_solver.py

Lines changed: 7 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,6 @@ class Oracles:
174174
def __init__(
175175
self,
176176
problem: Problem,
177-
initial_point: np.ndarray,
178-
num_constraints: int,
179177
verbose: bool = True,
180178
use_hessian: bool = True,
181179
) -> None:
@@ -191,40 +189,24 @@ def __init__(
191189
if use_hessian:
192190
self.c_problem.init_hessian_coo_lower_tri()
193191

194-
self.initial_point = initial_point
195-
self.num_constraints = num_constraints
196-
self.iterations = 0
197-
198192
# Cached sparsity structures
199193
self._jac_structure: tuple[np.ndarray, np.ndarray] | None = None
200194
self._hess_structure: tuple[np.ndarray, np.ndarray] | None = None
201-
self.constraints_forward_passed = False
202-
self.objective_forward_passed = False
203195

204196
def objective(self, x: np.ndarray) -> float:
205197
"""Returns the scalar value of the objective given x."""
206-
self.objective_forward_passed = True
207198
return self.c_problem.objective_forward(x)
208199

209200
def gradient(self, x: np.ndarray) -> np.ndarray:
210201
"""Returns the gradient of the objective with respect to x."""
211-
212-
if not self.objective_forward_passed:
213-
self.objective(x)
214-
215202
return self.c_problem.gradient()
216203

217204
def constraints(self, x: np.ndarray) -> np.ndarray:
218205
"""Returns the constraint values."""
219-
self.constraints_forward_passed = True
220206
return self.c_problem.constraint_forward(x)
221207

222208
def jacobian(self, x: np.ndarray) -> np.ndarray:
223209
"""Returns the Jacobian values in COO format at the sparsity structure. """
224-
225-
if not self.constraints_forward_passed:
226-
self.constraints(x)
227-
228210
return self.c_problem.eval_jacobian_vals()
229211

230212
def jacobianstructure(self) -> tuple[np.ndarray, np.ndarray]:
@@ -239,46 +221,23 @@ def jacobianstructure(self) -> tuple[np.ndarray, np.ndarray]:
239221
def hessian(self, x: np.ndarray, duals: np.ndarray, obj_factor: float) -> np.ndarray:
240222
"""Returns the lower triangular Hessian values in COO format. """
241223
if not self.use_hessian:
242-
# Shouldn't be called when using quasi-Newton, but return empty array
243-
return np.array([])
244-
245-
if not self.objective_forward_passed:
246-
self.objective(x)
247-
if not self.constraints_forward_passed:
248-
self.constraints(x)
249-
224+
raise ValueError("Hessian oracle called but use_hessian is False. "
225+
"This is a bug and should be reported.")
226+
250227
return self.c_problem.eval_hessian_vals_coo_lower_tri(obj_factor, duals)
251228

252229
def hessianstructure(self) -> tuple[np.ndarray, np.ndarray]:
253230
"""Returns the COO sparsity structure of the lower part of the Hessian.
254231
The returned rows are ascending, and within each row the columns are
255232
ascending."""
256233
if not self.use_hessian:
257-
# Return empty structure when using quasi-Newton approximation
258-
return (np.array([], dtype=np.int32), np.array([], dtype=np.int32))
259-
234+
# IPOPT calls this function even when hessian_approximation='limited-memory',
235+
# so return empty structure
236+
return (np.array([]), np.array([]))
237+
260238
if self._hess_structure is not None:
261239
return self._hess_structure
262240

263241
rows, cols = self.c_problem.get_problem_hessian_sparsity_coo()
264242
self._hess_structure = (rows, cols)
265243
return self._hess_structure
266-
267-
def intermediate(
268-
self,
269-
alg_mod: int,
270-
iter_count: int,
271-
obj_value: float,
272-
inf_pr: float,
273-
inf_du: float,
274-
mu: float,
275-
d_norm: float,
276-
regularization_size: float,
277-
alpha_du: float,
278-
alpha_pr: float,
279-
ls_trials: int,
280-
) -> None:
281-
"""Prints information at every Ipopt iteration."""
282-
self.iterations = iter_count
283-
self.objective_forward_passed = False
284-
self.constraints_forward_passed = False

0 commit comments

Comments
 (0)