/
bounded.py
130 lines (114 loc) · 4.9 KB
/
bounded.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
"Implements Bounded"
from collections import defaultdict, OrderedDict
import numpy as np
from .. import Variable
from .set import ConstraintSet
from ..small_scripts import mag
def varkey_bounds(varkeys, lower, upper):
"""Returns constraints list bounding all varkeys.
Arguments
---------
varkeys : iterable
list of varkeys to create bounds for
lower : float
lower bound for all varkeys
upper : float
upper bound for all varkeys
"""
constraints = []
for varkey in varkeys:
variable = Variable(**varkey.descr)
if variable.units:
variable.hmap.units = None
variable.units = None
constraint = []
if upper:
constraint.append(upper >= variable)
if lower:
constraint.append(variable >= lower)
constraints.append(constraint)
return constraints
class Bounded(ConstraintSet):
"""Bounds contained variables so as to ensure dual feasibility.
Arguments
---------
constraints : iterable
constraints whose varkeys will be bounded
verbosity : int (default 1)
how detailed of a warning to print
0: nothing
1: print warnings
eps : float (default 1e-30)
default lower bound is eps, upper bound is 1/eps
lower : float (default None)
lower bound for all varkeys, replaces eps
upper : float (default None)
upper bound for all varkeys, replaces 1/eps
"""
sens_threshold = 1e-7
logtol_threshold = 3
def __init__(self, constraints, verbosity=1,
eps=1e-30, lower=None, upper=None):
if not isinstance(constraints, ConstraintSet):
constraints = ConstraintSet(constraints)
self.bound_las = None
self.verbosity = verbosity
self.lowerbound = lower if (lower or upper) else eps
self.upperbound = upper if (lower or upper) else 1/eps
constrained_varkeys = constraints.constrained_varkeys()
self.bound_varkeys = frozenset(vk for vk in constrained_varkeys
if vk not in constraints.substitutions)
bounding_constraints = varkey_bounds(self.bound_varkeys,
self.lowerbound, self.upperbound)
# OrderedDict to keep them in order for sens_from_dual
super(Bounded, self).__init__(OrderedDict([
("original constraints", constraints),
("variable bounds", bounding_constraints)]))
def sens_from_dual(self, las, nus, result):
"Return sensitivities while capturing the relevant lambdas"
n = bool(self.lowerbound) + bool(self.upperbound)
self.bound_las = las[-n*len(self.bound_varkeys):]
return super(Bounded, self).sens_from_dual(las, nus, result)
def process_result(self, result):
"Add boundedness to the model's solution"
ConstraintSet.process_result(self, result)
if "boundedness" not in result:
result["boundedness"] = {}
result["boundedness"].update(self.check_boundaries(result))
def check_boundaries(self, result, verbosity=None):
"Creates (and potentially prints) a dictionary of unbounded variables."
out = defaultdict(set)
verbosity = self.verbosity if verbosity is None else verbosity
for i, varkey in enumerate(self.bound_varkeys):
value = mag(result["variables"][varkey])
if self.bound_las:
# TODO: support sensitive-to bounds for SPs
# by using named variables, returning las,
# or pulling from self.las?
if self.lowerbound and self.upperbound:
lam_gt, lam_lt = self.bound_las[2*i], self.bound_las[2*i+1]
elif self.lowerbound:
lam_lt = self.bound_las[i]
elif self.upperbound:
lam_gt = self.bound_las[i]
if self.lowerbound:
if self.bound_las:
if abs(lam_lt) >= self.sens_threshold:
out["sensitive to lower bound"].add(varkey)
distance_below = np.log(value/self.lowerbound)
if distance_below <= self.logtol_threshold:
out["value near lower bound"].add(varkey)
if self.upperbound:
if self.bound_las:
if abs(lam_gt) >= self.sens_threshold:
out["sensitive to upper bound"].add(varkey)
distance_above = np.log(self.upperbound/value)
if distance_above <= self.logtol_threshold:
out["value near upper bound"].add(varkey)
if verbosity > 0 and out:
print("")
print("Solves with these variables bounded:")
for key, value in sorted(out.items()):
print("% 25s: %s" % (key, ", ".join(map(str, value))))
print("")
return out