Skip to content

Commit 98c6cd6

Browse files
Bounds investigation (#29)
* div canon stuff * some test problems in sandbox * preparing for merge * some progress * weird value check * stress test ML zero mean passes * very subtle implementation details. MLE stress test (with and without zero mean) work * minor * fixed dimension error inside div-canon and added Sharpe ratio as a test * fixing some linting issues * more fixing imports * fixing final imports, giving up --------- Co-authored-by: William Zijie Zhang <[email protected]>
1 parent 5830a8f commit 98c6cd6

26 files changed

+615
-47
lines changed

cvxpy/atoms/elementwise/power.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,11 @@ def _domain(self) -> List[Constraint]:
395395
return [self.args[0] >= 0]
396396
else:
397397
return []
398+
399+
def point_in_domain(self) -> np.ndarray:
400+
"""Returns a point in the domain of the node.
401+
"""
402+
return np.ones(self.shape)
398403

399404
def get_data(self):
400405
return [self._p_orig, self.max_denom]

cvxpy/reductions/expr2smooth/canonicalizers/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,5 @@
3535
log: log_canon,
3636
power: power_canon,
3737
Pnorm : pnorm_canon,
38-
DivExpression: div_canon,
38+
DivExpression: div_canon
3939
}

cvxpy/reductions/expr2smooth/canonicalizers/div_canon.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,33 @@
1414
limitations under the License.
1515
"""
1616

17+
import numpy as np
18+
1719
from cvxpy.expressions.variable import Variable
1820

21+
1922
# We canonicalize div(x, y) as z * y = x.
2023
def div_canon(expr, args):
21-
# TODO: potential bounds here?
22-
# TODO: wrong dimension for z here
23-
z = Variable(args[1].shape)
24+
dim = (1, ) if args[0].shape == () else args[0].shape
25+
sgn_z = args[0].sign
26+
27+
if sgn_z == 'NONNEGATIVE':
28+
z = Variable(dim, bounds=[0, None])
29+
elif sgn_z == 'NONPOSITIVE':
30+
z = Variable(dim, bounds=[None, 0])
31+
else:
32+
z = Variable(dim)
33+
2434
y = Variable(args[1].shape, bounds=[0, None])
2535

26-
if args[1].value is not None and args[1].value != 0.0:
36+
# DCED: perhaps initialize further away from boundary?
37+
if args[1].value is not None and np.all(args[1].value != 0.0):
2738
y.value = args[1].value
2839
else:
2940
y.value = expr.point_in_domain()
3041

3142
if args[0].value is not None:
32-
z.value = args[0].value / y.value
43+
z.value = np.atleast_1d(args[0].value) / y.value
3344
else:
3445
z.value = expr.point_in_domain()
35-
# TODO: should we also include y >= 0 here?
36-
return z, [z * y == args[0], y == args[1]]#], #y >= 0, z >= 0]
46+
return z, [z * y == args[0], y == args[1]]

cvxpy/reductions/expr2smooth/canonicalizers/log_canon.py

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,60 @@
1414
limitations under the License.
1515
"""
1616

17+
import numpy as np
18+
19+
from cvxpy.expressions.constants import Constant
1720
from cvxpy.expressions.variable import Variable
1821

1922

23+
def collect_constant_and_variable(expr, constants, variable):
24+
if isinstance(expr, Constant):
25+
constants.append(expr)
26+
elif isinstance(expr, Variable):
27+
variable.append(expr)
28+
elif hasattr(expr, "args"):
29+
for subexpr in expr.args:
30+
collect_constant_and_variable(subexpr, constants, variable)
31+
32+
assert(len(variable) <= 1)
33+
34+
# DCED: Without this lower bound the stress test for ML Gaussian non-zero mean fails.
35+
# Perhaps this should be a parameter exposed to the user?
36+
LOWER_BOUND = 1e-5
37+
2038
def log_canon(expr, args):
21-
t = Variable(args[0].size)
22-
if args[0].value is not None:
39+
t = Variable(args[0].size, bounds=[LOWER_BOUND, None], name='t')
40+
41+
# DCED: if args[0] is a * x for a constant scalar or vector 'a'
42+
# and a vector variable 'x', we want to add bounds to x if x
43+
# does not have any bounds. We also want to initialize x far
44+
# from its bounds.
45+
constants, variable = [], []
46+
collect_constant_and_variable(args[0], constants, variable)
47+
a = 1
48+
is_special_case = True
49+
for constant in constants:
50+
if len(constant.shape) == 2:
51+
is_special_case = False
52+
break
53+
else:
54+
a *= constant.value
55+
56+
if not is_special_case:
57+
if args[0].value is not None and np.all(args[0].value > 0):
58+
t.value = args[0].value
59+
else:
60+
t.value = expr.point_in_domain()
61+
else:
62+
if variable[0].value is None:
63+
variable[0].value = expr.point_in_domain() * np.sign(a)
64+
65+
lbs = -np.inf * np.ones(args[0].size)
66+
ubs = np.inf * np.ones(args[0].size)
67+
lbs[a > 0] = 0
68+
ubs[a < 0] = 0
69+
variable[0].bounds = [lbs, ubs]
70+
assert(args[0].value is not None and np.all(args[0].value > 0.0))
2371
t.value = args[0].value
24-
else:
25-
t.value = expr.point_in_domain()
72+
2673
return expr.copy([t]), [t==args[0]]

cvxpy/reductions/expr2smooth/canonicalizers/pnorm_canon.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
limitations under the License.
1515
"""
1616

17-
from cvxpy.expressions.variable import Variable
1817
from cvxpy.atoms.affine.sum import sum
18+
from cvxpy.expressions.variable import Variable
19+
1920

2021
def pnorm_canon(expr, args):
2122
x = args[0]

cvxpy/reductions/expr2smooth/canonicalizers/power_canon.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@
2323
def power_canon(expr, args):
2424
x = args[0]
2525
p = expr.p_rational
26-
#w = expr.w
27-
26+
2827
if p == 1:
2928
return x, []
3029

@@ -35,16 +34,22 @@ def power_canon(expr, args):
3534
else:
3635
t = Variable(shape)
3736
if 0 < p < 1:
38-
if x.value is not None:
39-
# TODO: check if this initialization is correct
40-
t.value = np.power(np.abs(x.value), p)
41-
return t, [t**(1/p) == x, t >= 0]
37+
raise NotImplementedError('This power is not yet supported.')
38+
39+
# DCED: This is not a smooth implementation so we raise an error for now
40+
#if x.value is not None:
41+
# t.value = np.power(np.abs(x.value), p)
42+
#return t, [t**(1/p) == x, t >= 0]
4243
elif p > 1:
4344
t = Variable(args[0].shape)
44-
if args[0].value is not None:
45+
46+
# DCED: for Gaussian ML it works better if we include the second
47+
# condition here
48+
if args[0].value is not None and np.all(args[0].value >= 1):
4549
t.value = args[0].value
4650
else:
4751
t.value = expr.point_in_domain()
52+
4853
return expr.copy([t]), [t==args[0]]
4954
else:
50-
pass
55+
raise NotImplementedError('This power is not yet supported.')

cvxpy/reductions/expr2smooth/expr2smooth.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
from cvxpy import problems
2020
from cvxpy.expressions.expression import Expression
21-
from cvxpy.expressions.variable import Variable
2221
from cvxpy.problems.objective import Minimize
2322
from cvxpy.reductions.canonicalization import Canonicalization
2423
from cvxpy.reductions.expr2smooth.canonicalizers import CANON_METHODS as smooth_canon_methods

cvxpy/reductions/solvers/nlp_solvers/ipopt_nlpif.py

Lines changed: 78 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
import numpy as np
1818
import scipy.sparse as sp
19-
from time import time
2019

2120
import cvxpy.settings as s
2221
from cvxpy.constraints import (
@@ -134,14 +133,7 @@ def solve_via_data(self, data, warm_start: bool, verbose: bool, solver_opts, sol
134133
"""
135134
import cyipopt
136135
bounds = self.Bounds(data["problem"])
137-
initial_values = []
138-
for var in bounds.main_var:
139-
if var.value is not None:
140-
initial_values.append(var.value.flatten(order='F'))
141-
else:
142-
# If no initial value, use zero
143-
initial_values.append(np.zeros(var.size))
144-
x0 = np.concatenate(initial_values, axis=0)
136+
x0 = self.construct_initial_point(bounds)
145137
nlp = cyipopt.Problem(
146138
n=len(x0),
147139
m=len(bounds.cl),
@@ -153,9 +145,14 @@ def solve_via_data(self, data, warm_start: bool, verbose: bool, solver_opts, sol
153145
)
154146
nlp.add_option('mu_strategy', 'adaptive')
155147
nlp.add_option('tol', 1e-7)
156-
nlp.add_option('honor_original_bounds', 'yes')
148+
#nlp.add_option('honor_original_bounds', 'yes')
149+
nlp.add_option('bound_relax_factor', 0.0)
157150
nlp.add_option('hessian_approximation', "limited-memory")
158151
nlp.add_option('derivative_test', 'first-order')
152+
nlp.add_option('least_square_init_duals', 'yes')
153+
#nlp.add_option('constr_mult_init_max', 1e10)
154+
#nlp.add_option('derivative_test_perturbation', 1e-5)
155+
#nlp.add_option('point_perturbation_radius', 0.1)
159156
_, info = nlp.solve(x0)
160157
return info
161158

@@ -168,6 +165,43 @@ def cite(self, data):
168165
Data generated via an apply call.
169166
"""
170167
return CITATION_DICT["IPOPT"]
168+
169+
# TODO (DCED): Ask WZ where to put this.
170+
def construct_initial_point(self, bounds):
171+
initial_values = []
172+
offset = 0
173+
lbs = bounds.lb
174+
ubs = bounds.ub
175+
for var in bounds.main_var:
176+
if var.value is not None:
177+
initial_values.append(var.value.flatten(order='F'))
178+
else:
179+
# If no initial value is specified, look at the bounds.
180+
# If both lb and ub are specified, we initialize the
181+
# variables to be their midpoints. If only one of them
182+
# is specified, we initialize the variable one unit
183+
# from the bound. If none of them is specified, we
184+
# initialize it to zero.
185+
lb = lbs[offset:offset + var.size]
186+
ub = ubs[offset:offset + var.size]
187+
188+
lb_finite = np.isfinite(lb)
189+
ub_finite = np.isfinite(ub)
190+
191+
# Replace infs with zero for arithmetic
192+
lb0 = np.where(lb_finite, lb, 0.0)
193+
ub0 = np.where(ub_finite, ub, 0.0)
194+
195+
# Midpoint if both finite, one from bound if only one finite, zero if none
196+
init = (lb_finite * ub_finite * 0.5 * (lb0 + ub0) +
197+
lb_finite * (~ub_finite) * (lb0 + 1.0) +
198+
(~lb_finite) * ub_finite * (ub0 - 1.0))
199+
200+
initial_values.append(init)
201+
202+
offset += var.size
203+
x0 = np.concatenate(initial_values, axis=0)
204+
return x0
171205

172206
class Oracles():
173207
def __init__(self, problem, inital_point):
@@ -176,7 +210,7 @@ def __init__(self, problem, inital_point):
176210
self.initial_point = inital_point
177211
for var in self.problem.variables():
178212
self.main_var.append(var)
179-
213+
180214
def objective(self, x):
181215
"""Returns the scalar value of the objective given x."""
182216
# Set the variable value
@@ -190,6 +224,8 @@ def objective(self, x):
190224
return obj_value
191225

192226
def gradient(self, x):
227+
#import pdb
228+
#pdb.set_trace()
193229
"""Returns the gradient of the objective with respect to x."""
194230
# compute the gradient using _grad
195231
offset = 0
@@ -214,6 +250,8 @@ def constraints(self, x):
214250
"""Returns the constraint values."""
215251
# Set the variable value
216252
offset = 0
253+
#import pdb
254+
#pdb.set_trace()
217255
for var in self.main_var:
218256
size = var.size
219257
var.value = x[offset:offset+size].reshape(var.shape, order='F')
@@ -222,12 +260,13 @@ def constraints(self, x):
222260
# Evaluate all constraints
223261
constraint_values = []
224262
for constraint in self.problem.constraints:
225-
constraint_values.append(np.asarray(constraint.args[0].value).flatten())
263+
constraint_values.append(np.asarray(constraint.args[0].value).flatten(order='F'))
226264
return np.concatenate(constraint_values)
227265

228266
def jacobian(self, x):
229267
"""Returns only the non-zero values of the Jacobian."""
230-
268+
#import pdb
269+
#pdb.set_trace()
231270
# Set variable values
232271
offset = 0
233272
for var in self.main_var:
@@ -245,7 +284,10 @@ def jacobian(self, x):
245284
jacobian = grad_dict[var].T
246285
if sp.issparse(jacobian):
247286
jacobian = sp.dok_matrix(jacobian)
248-
data = np.array([jacobian.get((r, c), 0) for r, c in zip(rows, cols)])
287+
data = np.array([
288+
jacobian.get((r, c), 0)
289+
for r, c in zip(rows, cols)
290+
])
249291
values.append(np.atleast_1d(data))
250292
else:
251293
values.append(np.atleast_1d(jacobian))
@@ -328,12 +370,29 @@ def get_variable_bounds(self):
328370
for var in self.main_var:
329371
size = var.size
330372
if var.bounds:
331-
var_lower.extend(var.bounds[0].flatten(order='F'))
332-
var_upper.extend(var.bounds[1].flatten(order='F'))
373+
lb = var.bounds[0].flatten(order='F')
374+
ub = var.bounds[1].flatten(order='F')
375+
376+
if var.is_nonneg():
377+
lb = np.maximum(lb, 0)
378+
379+
if var.is_nonpos():
380+
ub = np.minimum(ub, 0)
381+
382+
var_lower.extend(lb)
383+
var_upper.extend(ub)
333384
else:
334-
# No bounds specified, use infinite bounds
335-
var_lower.extend([-np.inf] * size)
336-
var_upper.extend([np.inf] * size)
385+
# No bounds specified, use infinite bounds or bounds
386+
# set by the nonnegative or nonpositive attribute
387+
if var.is_nonneg():
388+
var_lower.extend([0.0] * size)
389+
else:
390+
var_lower.extend([-np.inf] * size)
391+
392+
if var.is_nonpos():
393+
var_upper.extend([0.0] * size)
394+
else:
395+
var_upper.extend([np.inf] * size)
337396

338397
self.lb = np.array(var_lower)
339398
self.ub = np.array(var_upper)

cvxpy/sandbox/control_of_car_matrix.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import cvxpy as cp
2-
import numpy as np
31
import matplotlib.pyplot as plt
2+
import numpy as np
3+
4+
import cvxpy as cp
45

56

67
def solve_car_control(x_final, L=0.1, N=2, h=0.1, gamma=10):
@@ -91,7 +92,7 @@ def plot_trajectory(x_opt, u_opt, L, h, title="Car Trajectory"):
9192

9293
# Plot car position and orientation at several time steps
9394
car_length = L
94-
car_width = L * 0.6
95+
# car_width = L * 0.6
9596

9697
# Select time steps to show car outline (every 5th step)
9798
steps_to_show = range(0, len(x_opt), 5)
@@ -153,7 +154,11 @@ def plot_trajectory(x_opt, u_opt, L, h, title="Car Trajectory"):
153154

154155
if x_opt is not None and u_opt is not None:
155156
print("Optimization successful!")
156-
print(f"Final position: p1={x_opt[-1, 0]:.3f}, p2={x_opt[-1, 1]:.3f}, theta={x_opt[-1, 2]:.3f}")
157+
print(
158+
f"Final position: p1={x_opt[-1, 0]:.3f}, "
159+
f"p2={x_opt[-1, 1]:.3f}, "
160+
f"theta={x_opt[-1, 2]:.3f}"
161+
)
157162

158163
# Plot the trajectory
159164
fig, ax = plot_trajectory(x_opt, u_opt, L=0.1, h=0.1, title=description)

cvxpy/sandbox/entr_max.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import cvxpy as cp
21
import numpy as np
32

3+
import cvxpy as cp
4+
45
k = 5
56
x = cp.Variable((k, k))
67
x.value = np.ones((k, k)) / k**2

0 commit comments

Comments
 (0)