Skip to content

Commit

Permalink
cleanup solution array
Browse files Browse the repository at this point in the history
  • Loading branch information
bqpd committed Mar 4, 2020
1 parent 9d92839 commit be7c840
Show file tree
Hide file tree
Showing 16 changed files with 81 additions and 100 deletions.
6 changes: 5 additions & 1 deletion docs/source/examples/autosweep_output.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Table of solutions used in the autosweep:

Cost
----
[ 0.333 1 123 ] [m⁴]
[ 0.333 1 123 ]

Free Variables
--------------
Expand All @@ -23,3 +23,7 @@ Sensitivities
-------------
l : [ +1 +2.5 +4 ]

Tightest Constraints in last sweep
----------------------------------
+2 : A >= (l/3)²

6 changes: 3 additions & 3 deletions docs/source/gettingstarted.rst
Original file line number Diff line number Diff line change
Expand Up @@ -185,15 +185,15 @@ From the dual solution GPkit computes the sensitivities for every fixed variable
Using variable sensitivities
----------------------------

Fixed variable sensitivities can be accessed from a SolutionArray’s ``["sensitivities"]["constants"]`` dict, as in this example:
Fixed variable sensitivities can be accessed from a SolutionArray’s ``["sensitivities"]["variables"]`` dict, as in this example:

.. code-block:: python
import gpkit
x = gpkit.Variable("x")
x_min = gpkit.Variable("x_{min}", 2)
sol = gpkit.Model(x, [x_min <= x]).solve()
assert sol["sensitivities"]["constants"][x_min] == 1
assert sol["sensitivities"]["variables"][x_min] == 1
These sensitivities are actually log derivatives (:math:`\frac{d \mathrm{log}(y)}{d \mathrm{log}(x)}`); whereas a regular derivative is a tangent line, these are tangent monomials, so the ``1`` above indicates that ``x_min`` has a linear relation with the objective. This is confirmed by a further example:

Expand All @@ -203,6 +203,6 @@ These sensitivities are actually log derivatives (:math:`\frac{d \mathrm{log}(y)
x = gpkit.Variable("x")
x_squared_min = gpkit.Variable("x^2_{min}", 2)
sol = gpkit.Model(x, [x_squared_min <= x**2]).solve()
assert sol["sensitivities"]["constants"][x_squared_min] == 2
assert sol["sensitivities"]["variables"][x_squared_min] == 2
.. add a plot of a monomial approximation vs a tangent approximation
2 changes: 1 addition & 1 deletion docs/source/ipynb/Box/Box.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@
"cell_type": "code",
"collapsed": false,
"input": [
"sol["sensitivities"]["constants"][A_floor]"
"sol["sensitivities"]["variables"][A_floor]"
],
"language": "python",
"metadata": {},
Expand Down
2 changes: 1 addition & 1 deletion docs/source/ipynb/Box/Box.rst
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ Hmm, why didn't :math:`A_{floor}` show up in the sensitivities list?

.. code:: python

sol["sensitivities"]["constants"][A_floor]
sol["sensitivities"]["variables"][A_floor]



Expand Down
11 changes: 6 additions & 5 deletions gpkit/constraints/bounded.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import numpy as np
from .. import Variable
from .set import ConstraintSet
from ..small_scripts import mag


def varkey_bounds(varkeys, lower, upper):
Expand Down Expand Up @@ -87,15 +86,17 @@ def check_boundaries(self, result, *, verbosity=0):
"Creates (and potentially prints) a dictionary of unbounded variables."
out = defaultdict(set)
for i, varkey in enumerate(self.bound_varkeys):
value = mag(result["variables"][varkey])
constraints = self["variable bounds"][i]
value = result["variables"][varkey]
# import pdb; pdb.set_trace()
c_senss = [result["sensitivities"]["constraints"][c]
for c in self["variable bounds"][i]]
if self.lowerbound:
if constraints[0].relax_sensitivity >= self.sens_threshold:
if c_senss[0] >= self.sens_threshold:
out["sensitive to lower bound"].add(varkey)
if np.log(value/self.lowerbound) <= self.logtol_threshold:
out["value near lower bound"].add(varkey)
if self.upperbound:
if constraints[-1].relax_sensitivity >= self.sens_threshold:
if c_senss[-1] >= self.sens_threshold:
out["sensitive to upper bound"].add(varkey)
if np.log(self.upperbound/value) <= self.logtol_threshold:
out["value near upper bound"].add(varkey)
Expand Down
21 changes: 13 additions & 8 deletions gpkit/constraints/gp.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,22 +328,27 @@ def _compile_result(self, solver_out):
if not self.varlocs and len(primal) == 1 and primal[0] == 0:
primal = [] # an empty result, as returned by MOSEK
assert len(self.varlocs) == len(primal)
result = {"freevariables": KeyDict(zip(self.varlocs, np.exp(primal)))}
result = {"gp": self}
# get cost & variables #
result["cost"] = float(solver_out["objective"])
result["constants"] = KeyDict(self.substitutions)
result["freevariables"] = KeyDict(zip(self.varlocs, np.exp(primal)))
result["variables"] = KeyDict(result["freevariables"])
result["variables"].update(result["constants"])
# get sensitivities #
result["sensitivities"] = {"nu": nu, "la": la}
result["sensitivities"] = {"constraints": {}}
# add cost's sensitivity in (nu could be self.nu_by_posy[0])
cost_senss = sum(nu_i*exp
for (nu_i, exp) in zip(nu, self.cost.hmap.keys()))
self.v_ss = cost_senss.copy()
for las, nus, leaf in zip(la[1:], self.nu_by_posy[1:], self.hmaps[1:]):
while hasattr(leaf, "parent") and leaf.parent is not None:
leaf = leaf.parent
self.v_ss += leaf.sens_from_dual(las, nus, result)
for las, nus, c in zip(la[1:], self.nu_by_posy[1:], self.hmaps[1:]):
while hasattr(c, "parent") and c.parent is not None:
c = c.parent
v_ss, c_senss = c.sens_from_dual(las, nus, result)
self.v_ss += v_ss
while hasattr(c, "generated_by") and c.generated_by is not None:
c = c.generated_by
result["sensitivities"]["constraints"][c] = c_senss
# carry linked sensitivities over to their constants
for v in list(v for v in self.v_ss if v.gradients):
dlogcost_dlogv = self.v_ss.pop(v)
Expand All @@ -365,8 +370,8 @@ def _compile_result(self, solver_out):
cost_senss[c] = dlogcost_dlogv*dlogv_dlogc + accum
result["sensitivities"]["cost"] = cost_senss
result["sensitivities"]["variables"] = KeyDict(self.v_ss)
result["sensitivities"]["constants"] = KeyDict(
{k: v for k, v in self.v_ss.items() if k in result["constants"]})
result["sensitivities"]["constants"] = \
result["sensitivities"]["variables"] # NOTE: backwards compat.
result["soltime"] = solver_out["soltime"]
return SolutionArray(result)

Expand Down
14 changes: 6 additions & 8 deletions gpkit/constraints/loose.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,14 @@ def process_result(self, result):
"Checks that all constraints are satisfied with equality"
super().process_result(result)
for constraint in self.flat():
cstr = ("Constraint [ %.100s... %s %.100s... )"
% (constraint.left, constraint.oper, constraint.right))
if not hasattr(constraint, "relax_sensitivity"):
print("%s lacks a `relax_sensitivity` parameter and"
" so can't be checked for looseness." % cstr)
continue
if constraint.relax_sensitivity >= self.senstol:
c_senss = result["sensitivities"]["constraints"][constraint]
if c_senss >= self.senstol:
cstr = ("Constraint [ %.100s... %s %.100s... )"
% (constraint.left, constraint.oper, constraint.right))
msg = ("%s is not loose: it has a sensitivity of %+.4g."
" (Allowable sensitivity: %.4g)" %
(cstr, constraint.relax_sensitivity, self.senstol))
(cstr, c_senss, self.senstol))
constraint.relax_sensitivity = c_senss
appendsolwarning(msg, constraint, result,
"Unexpectedly Tight Constraints")
if self.raiseerror:
Expand Down
2 changes: 1 addition & 1 deletion gpkit/constraints/prog_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ def solvefn(self, solver=None, *, verbosity=1, skipsweepfailures=False,
"""
constants, sweep, linked = parse_subs(self.varkeys, self.substitutions)
solution = SolutionArray()
solution.modelstr = str(self)

# NOTE SIDE EFFECTS: self.program is set below
if sweep:
Expand All @@ -125,7 +126,6 @@ def solvefn(self, solver=None, *, verbosity=1, skipsweepfailures=False,
result = progsolve(solver, verbosity=verbosity, **kwargs)
solution.append(result)
solution.to_arrays()
solution.program = self.program
if self.cost.units:
solution["cost"] = solution["cost"] * self.cost.units
self.solution = solution # NOTE: SIDE EFFECTS
Expand Down
2 changes: 1 addition & 1 deletion gpkit/constraints/relax.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ def __init__(self, constraints, *, include_only=None, exclude=None):
def process_result(self, result):
"Transfers the constant sensitivities back to the original constants"
ConstraintSet.process_result(self, result)
constant_senss = result["sensitivities"]["constants"]
constant_senss = result["sensitivities"]["variables"]
for new_constant, former_constant in self._derelax_map.items():
constant_senss[former_constant] = constant_senss[new_constant]
del constant_senss[new_constant]
26 changes: 5 additions & 21 deletions gpkit/nomials/math.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,8 +398,6 @@ def relaxed(self, relaxvar):
else:
raise ValueError(
"Constraint %s had unknown operator %s." % self.oper, self)
for constr in relaxed:
constr.generated_by = self
return relaxed


Expand All @@ -410,7 +408,6 @@ class PosynomialInequality(ScalarSingleEquationConstraint):
Usually initialized via operator overloading, e.g. cc = (y**2 >= 1 + x)
"""
feastol = 1e-3
relax_sensitivity = None
# NOTE: follows .check_result's max default, but 1e-3 seems a bit lax...

def __init__(self, left, oper, right):
Expand Down Expand Up @@ -494,11 +491,6 @@ def as_hmapslt1(self, substitutions):

def sens_from_dual(self, la, nu, result): # pylint: disable=unused-argument
"Returns the variable/constraint sensitivities from lambda/nu"
self.relax_sensitivity = la
if self.generated_by:
self.generated_by.relax_sensitivity = la
if getattr(self.generated_by, "generated_by", None):
self.generated_by.generated_by.relax_sensitivity = la
presub, = self.unsubbed
if hasattr(self, "pmap"):
nu_ = np.zeros(len(presub.hmap))
Expand All @@ -511,7 +503,7 @@ def sens_from_dual(self, la, nu, result): # pylint: disable=unused-argument
nu_[idx] += percentage * la*scale
nu = nu_
self.v_ss = sum(nu_i*exp for (nu_i, exp) in zip(nu, presub.hmap.keys()))
return self.v_ss
return self.v_ss, la

def as_gpconstr(self, _):
"The GP version of a Posynomial constraint is itself"
Expand All @@ -530,7 +522,6 @@ def __init__(self, left, right):
self.nomials.extend(self.unsubbed)
self.bounded = set()
self.meq_bounded = {}
self.relax_sensitivity = 0 # don't count equality sensitivities
self._las = []
if self.unsubbed and len(self.varkeys) > 1:
exp, = list(self.unsubbed[0].hmap.keys())
Expand Down Expand Up @@ -570,17 +561,11 @@ def sens_from_dual(self, la, nu, result):
"Returns the variable/constraint sensitivities from lambda/nu"
self._las.append(la)
if len(self._las) < 2:
return {}
la = self._las
return {}, 0
la = self._las[0] - self._las[1]
self._las = []
self.relax_sensitivity = la[0] - la[1]
if self.generated_by:
self.generated_by.relax_sensitivity = self.relax_sensitivity
if getattr(self.generated_by, "generated_by", None):
self.generated_by.generated_by.relax_sensitivity = \
self.relax_sensitivity
exp, = self.unsubbed[0].hmap
return (la[0]-la[1])*exp
return la*exp, la


class SignomialInequality(ScalarSingleEquationConstraint):
Expand Down Expand Up @@ -660,7 +645,6 @@ def sens_from_dual(self, la, nu, result):
= d(coeff)/d(var)*1/negy - d(negy)/d(var)*coeff*1/negy**2
"""
# pylint: disable=too-many-locals, attribute-defined-outside-init
self.relax_sensitivity = la

# pylint: disable=no-member
def subval(posy):
Expand All @@ -685,7 +669,7 @@ def subval(posy):
sens = (nu_i*inv_mon_val*d_mon_d_var*var_val)
assert isinstance(sens, float)
var_senss[var] = sens + var_senss.get(var, 0)
return var_senss
return var_senss, la

def as_gpconstr(self, x0):
"Returns GP approximation of an SP constraint at x0"
Expand Down
Loading

0 comments on commit be7c840

Please sign in to comment.