Skip to content

Commit d147964

Browse files
authored
[Ready for review] Initialization order (#114)
* initialization * clean up * sparse matrix variable fix (thanks William) * huber comment * removed assertion * minor * fixed to WZZ's comments
1 parent 46d7c05 commit d147964

File tree

17 files changed

+349
-64
lines changed

17 files changed

+349
-64
lines changed

cvxpy/problems/problem.py

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1246,6 +1246,7 @@ def _solve(self,
12461246

12471247
# standard solve
12481248
if best_of == 1:
1249+
self.set_NLP_initial_point()
12491250
canon_problem, inverse_data = nlp_chain.apply(problem=self)
12501251
solution = nlp_chain.solver.solve_via_data(canon_problem, warm_start,
12511252
verbose, solver_opts=kwargs)
@@ -1646,52 +1647,95 @@ def unpack_results(self, solution, chain: SolvingChain, inverse_data) -> None:
16461647
self._solver_stats = SolverStats.from_dict(self._solution.attr,
16471648
chain.solver.name())
16481649

1650+
1651+
def set_NLP_initial_point(self) -> dict:
1652+
""" Constructs an initial point for the optimization problem. If no
1653+
initial value is specified, look at the bounds. If both lb and ub are
1654+
specified, we initialize the variables to be their midpoints. If only
1655+
one of them is specified, we initialize the variable one unit from
1656+
the bound. If none of them is specified, we initialize it to zero.
1657+
"""
1658+
for var in self.variables():
1659+
if var.value is not None:
1660+
continue
1661+
1662+
bounds = var.bounds
1663+
is_nonneg = var.is_nonneg()
1664+
is_nonpos = var.is_nonpos()
1665+
1666+
if bounds is None:
1667+
if is_nonneg:
1668+
x0 = np.ones(var.shape)
1669+
elif is_nonpos:
1670+
x0 = -np.ones(var.shape)
1671+
else:
1672+
x0 = np.zeros(var.shape)
1673+
1674+
var.save_value(x0)
1675+
else:
1676+
lb, ub = bounds
1677+
1678+
if is_nonneg:
1679+
lb = np.maximum(lb, 0)
1680+
elif is_nonpos:
1681+
ub = np.maximum(ub, 0)
1682+
1683+
lb_finite = np.isfinite(lb)
1684+
ub_finite = np.isfinite(ub)
1685+
# Replace infs with zero for arithmetic
1686+
lb0 = np.where(lb_finite, lb, 0.0)
1687+
ub0 = np.where(ub_finite, ub, 0.0)
1688+
# Midpoint if both finite, one from bound if only one finite, zero if none
1689+
init = (lb_finite * ub_finite * 0.5 * (lb0 + ub0) +
1690+
lb_finite * (~ub_finite) * (lb0 + 1.0) +
1691+
(~lb_finite) * ub_finite * (ub0 - 1.0))
1692+
var.save_value(init)
1693+
1694+
16491695
def set_random_NLP_initial_point(self, run) -> dict:
16501696
""" Generates a random initial point for DNLP problems.
16511697
A variable is initialized randomly in the following cases:
1652-
1. the initial value specified by the user is None and
1653-
'sample_bounds' is set for that variable.
1698+
1. 'sample_bounds' is set for that variable.
16541699
2. the initial value specified by the user is None,
16551700
'sample_bounds' is not set for that variable, but the
16561701
variable has both finite lower and upper bounds.
1657-
1658-
In other words, a variable that has already been
1659-
initialized by the user will not be changed, even if
1660-
sample_bounds is set for that variable.
16611702
"""
16621703

1663-
# store user-specified initial values
1704+
# store user-specified initial values for variables that do
1705+
# not have sample bounds assigned
16641706
if run == 0:
16651707
self._user_initials = {}
16661708
for var in self.variables():
1667-
self._user_initials[var.id] = var.value
1668-
1709+
if var.sample_bounds is not None:
1710+
self._user_initials[var.id] = None
1711+
else:
1712+
self._user_initials[var.id] = var.value
1713+
16691714
for var in self.variables():
16701715

16711716
# skip variables with user-specified initial value
1717+
# (note that any variable with sample bounds set will have
1718+
# _user_initials[var.id] == None)
16721719
if self._user_initials[var.id] is not None:
1673-
# reset to user-specified initial value
1674-
# from last solve
1720+
# reset to user-specified initial value from last solve
16751721
var.value = self._user_initials[var.id]
16761722
continue
16771723
else:
16781724
# reset to None from last solve
16791725
var.value = None
16801726

16811727
# set sample_bounds to variable bounds if sample_bounds is None
1682-
# and variable has finite bounds
1728+
# and variable bounds (possibly infinite) are set
16831729
if var.sample_bounds is None and var.bounds is not None:
1684-
low, high = var.bounds
1685-
if np.all(np.isfinite(low)) and np.all(np.isfinite(high)):
1686-
var.sample_bounds = var.bounds
1687-
1730+
var.sample_bounds = var.bounds
1731+
16881732
# sample initial value if sample_bounds is set
16891733
if var.sample_bounds is not None:
16901734
low, high = var.sample_bounds
16911735
if not np.all(np.isfinite(low)) or not np.all(np.isfinite(high)):
16921736
raise ValueError(
1693-
"Variable %s has non-finite sample_bounds %s."
1694-
" Cannot generate random initial point."
1737+
"Variable %s has non-finite sample_bounds %s. Cannot generate"
1738+
" random initial point. Either add sample bounds or set the value. "
16951739
% (var.name(), var.sample_bounds)
16961740
)
16971741

cvxpy/reductions/cvx_attr2constr.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ def apply(self, problem):
156156
elif var.attributes['sparsity']:
157157
n = len(var.sparse_idx[0])
158158
sparse_var = Variable(n, var_id=var.id, **new_attr)
159+
if var.value_sparse is not None:
160+
sparse_var.value = var.value_sparse.data
159161
sparse_var.set_variable_of_provenance(var)
160162
id2new_var[var.id] = sparse_var
161163
row_idx = np.ravel_multi_index(var.sparse_idx, var.shape, order='F')

cvxpy/reductions/dnlp2smooth/canonicalizers/geo_mean_canon.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ def geo_mean_canon(expr, args):
3030
"""
3131
t = Variable(expr.shape, nonneg=True)
3232

33-
if args[0].value is not None and args[0].value > MIN_INIT:
34-
t.value = expr.numeric(args[0].value)
33+
if args[0].value is not None:
34+
t.value = np.max((expr.numeric(args[0].value), MIN_INIT))
3535
else:
3636
t.value = np.ones(expr.shape)
3737

cvxpy/reductions/dnlp2smooth/canonicalizers/huber_canon.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
# This file is identical to the dcp2cone version, except that it uses the
1818
# dnlp2smooth canonicalizer for the power atom. This is necessary.
1919

20+
import numpy as np
21+
2022
from cvxpy.atoms.elementwise.abs import abs
2123
from cvxpy.atoms.elementwise.power import power
2224
from cvxpy.expressions.variable import Variable
@@ -31,6 +33,14 @@ def huber_canon(expr, args):
3133
n = Variable(shape)
3234
s = Variable(shape)
3335

36+
if x.value is None:
37+
x.value = np.zeros(x.shape)
38+
39+
# this choice of initial value follows from how the smooth epigraph
40+
# form of the huber function is constructed
41+
n.value = np.minimum(np.abs(x.value), M.value) * np.sign(x.value)
42+
s.value = x.value - n.value
43+
3444
# n**2 + 2*M*|s|
3545
power_expr = power(n, 2)
3646
n2, constr_sq = power_canon(power_expr, power_expr.args)
@@ -41,4 +51,5 @@ def huber_canon(expr, args):
4151
# x == s + n
4252
constraints = constr_sq + constr_abs
4353
constraints.append(x == s + n)
54+
4455
return obj, constraints

cvxpy/reductions/dnlp2smooth/canonicalizers/pnorm_canon.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ def pnorm_canon(expr, args):
2424
p = expr.p
2525
shape = expr.shape
2626
t = Variable(shape, nonneg=True)
27+
28+
# expression will always have a value here in DNLP
29+
t.value = expr.value
30+
2731
# we canonicalize 2-norm as follows:
2832
# ||x||_2 <= t <=> quad_over_lin(x, t) <= t
2933
if p == 2:

cvxpy/reductions/dnlp2smooth/canonicalizers/power_canon.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from cvxpy.expressions.constants import Constant
2121
from cvxpy.expressions.variable import Variable
2222

23+
MIN_INIT = 1e-4
2324

2425
def power_canon(expr, args):
2526
x = args[0]
@@ -45,7 +46,7 @@ def power_canon(expr, args):
4546
t = Variable(shape, nonneg=True)
4647

4748
if x.value is not None:
48-
t.value = x.value
49+
t.value = np.maximum(x.value, MIN_INIT)
4950
else:
5051
t.value = expr.point_in_domain()
5152

cvxpy/reductions/eliminate_pwl/canonicalizers/abs_canon.py

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

17+
1718
from cvxpy.expressions.variable import Variable
1819

1920

2021
def abs_canon(expr, args):
2122
x = args[0]
2223
t = Variable(expr.shape)
2324
constraints = [t >= x, t >= -x]
25+
26+
# for DNLP we must initialize the new variable (DNLP guarantees that
27+
# x.value will be set when this function is called)
28+
if expr.value is not None:
29+
t.value = expr.value
30+
2431
return t, constraints

cvxpy/reductions/eliminate_pwl/canonicalizers/max_canon.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,10 @@ def max_canon(expr, args):
3535
promoted_t = reshape(t, (x.shape[0], 1), order='F') @ Constant(np.ones((1, x.shape[1])))
3636

3737
constraints = [x <= promoted_t]
38+
39+
# for DNLP we must initialize the new variable (DNLP guarantees that
40+
# x.value will be set when this function is called)
41+
if expr.value is not None:
42+
t.value = expr.value
43+
3844
return t, constraints

cvxpy/reductions/eliminate_pwl/canonicalizers/maximum_canon.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,17 @@
2121
def maximum_canon(expr, args):
2222
shape = expr.shape
2323
t = Variable(shape)
24-
24+
25+
# for DNLP we must initialize the new variable (DNLP guarantees that
26+
# x.value will be set when this function is called)
27+
if expr.value is not None:
28+
t.value = expr.value
29+
2530
if expr.is_nonneg():
2631
t = nonneg_wrap(t)
2732
if expr.is_nonpos():
2833
t = nonpos_wrap(t)
2934

3035
constraints = [t >= elem for elem in args]
36+
3137
return t, constraints

cvxpy/reductions/eliminate_pwl/canonicalizers/norm_inf_canon.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,9 @@ def norm_inf_canon(expr, args):
3434
else: # shape = (m, 1)
3535
promoted_t = reshape(t, (x.shape[0], 1), order='F') @ Constant(np.ones((1, x.shape[1])))
3636

37+
# for DNLP we must initialize the new variable (DNLP guarantees that
38+
# x.value will be set when this function is called)
39+
if expr.value is not None:
40+
t.value = expr.value
41+
3742
return t, [x <= promoted_t, x + promoted_t >= 0]

0 commit comments

Comments
 (0)