Skip to content

Commit

Permalink
approx_as_posyslt1
Browse files Browse the repository at this point in the history
  • Loading branch information
bqpd committed Mar 6, 2020
1 parent f30498c commit 1b2039b
Show file tree
Hide file tree
Showing 8 changed files with 29 additions and 116 deletions.
2 changes: 1 addition & 1 deletion docs/source/advancedcommands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Note that this variable should not be used in constructing your model!
self.assertAlmostEqual(sol(x2), sol(x)**2)
For evaluated variables that can be used during a solution, see ``externalfn`` under :ref:`sgp`.
For evaluated variables that can be used during a solution, see :ref:`sgp`.


.. _Sweeps:
Expand Down
10 changes: 4 additions & 6 deletions docs/source/examples/external_constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,13 @@ def as_hmapslt1(self, _):
"Ensures this is treated as an SGP constraint"
raise InvalidGPConstraint("ExternalConstraint cannot solve as a GP.")

def as_gpconstr(self, x0):
def approx_as_posyslt1(self, x0):
"Returns locally-approximating GP constraint"
# Creating a default constraint for the first solve
if not x0:
return (self.y >= self.x)
return (res*self.x/x_star/self.y)
# Otherwise calls external code at the current position...
x_star = x0[self.x]
res = external_code(x_star)
# ...and returns a linearized constraint
posynomial_constraint = (self.y >= res*self.x/x_star)
posynomial_constraint.generated_by = self
return posynomial_constraint
# ...and returns a linearized posy <= 1
return [res*self.x/x_star/self.y]
17 changes: 0 additions & 17 deletions docs/source/examples/external_sp2.py

This file was deleted.

15 changes: 0 additions & 15 deletions docs/source/examples/external_sp2_output.txt

This file was deleted.

10 changes: 0 additions & 10 deletions docs/source/signomialprogramming.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,6 @@ This problem is not GP compatible due to the sin(x) constraint. One approach mi

.. literalinclude:: examples/sin_approx_example_output.txt

We can do better, however, by utilizing some built in functionality of GPkit.
For simple cases with a single Variable, GPkit looks for ``externalfn`` metadata:

.. literalinclude:: examples/external_sp2.py

.. literalinclude:: examples/external_sp2_output.txt

However, for external functions not intrinsically tied to a single variable it's best to
use the full ConstraintSet API, as follows:

Assume we have some external code which is capable of evaluating our incompatible function:

.. literalinclude:: examples/external_function.py
Expand Down
56 changes: 14 additions & 42 deletions gpkit/constraints/sgp.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class SequentialGeometricProgram(CostedConstraintSet):
>>> gp.solve()
"""
gps = solver_outs = _results = result = model = None
_gp = _spvars = _lt_approxs = pccp_penalty = None
_gp = _spvars = pccp_penalty = None
with NamedVariables("SGP"):
slack = Variable("PCCPslack")

Expand All @@ -60,34 +60,18 @@ def __init__(self, cost, model, substitutions, *,
self._original_cost = cost
self.model = model
self.substitutions = substitutions
self.externalfn_vars = \
frozenset(Variable(v) for v in self.model.varkeys if v.externalfn)
if self.externalfn_vars: # a non-SP-constraint generating variable
self.blackboxconstraints = True
super().__init__(cost, model, substitutions)
return

self._lt_approxs = []
sgpconstraints = {"SP constraints": [], "GP constraints": []}
for cs in model.flat():
try:
if not isinstance(cs, PosynomialInequality):
cs.as_hmapslt1(substitutions) # gp-compatible?
sgpconstraints["GP constraints"].append(cs)
except InvalidGPConstraint:
if not hasattr(cs, "approx_as_posyslt1"):
raise InvalidSGPConstraint()
sgpconstraints["SP constraints"].append(cs)
try:
if use_pccp:
lts = [lt/self.slack for lt in cs.as_approxlts()]
else:
lts = cs.as_approxlts()
self._lt_approxs.append(lts)
except AttributeError: # some custom non-SP constraint
self.blackboxconstraints = True
super().__init__(cost, model, substitutions)
return
# all constraints seem SP-compatible
self.blackboxconstraints = False
if not sgpconstraints["SP constraints"]:
raise UnnecessarySGP("""Model valid as a Geometric Program.
Expand All @@ -97,6 +81,8 @@ def __init__(self, cost, model, substitutions, *,

if not use_pccp:
self.cost = cost
from ..nomials import Monomial
self.slack = Monomial(1)
else:
self.pccp_penalty = pccp_penalty
self.cost = cost * self.slack**pccp_penalty
Expand Down Expand Up @@ -141,8 +127,6 @@ def localsolve(self, solver=None, *, verbosity=1, x0=None, reltol=1e-4,
A dictionary containing the translated solver result.
"""
self.gps, self.solver_outs, self._results = [], [], []
# if there's external functions we can't mutate the GP
mutategp = mutategp and not self.blackboxconstraints
if not mutategp and not x0:
raise ValueError("Solves with arbitrary constraint generators"
" must specify an initial starting point x0.")
Expand All @@ -153,10 +137,7 @@ def localsolve(self, solver=None, *, verbosity=1, x0=None, reltol=1e-4,
starttime = time()
if verbosity > 0:
print("Starting a sequence of GP solves")
if self.externalfn_vars:
print(" for %i variables defined by externalfns"
% len(self.externalfn_vars))
elif mutategp:
if mutategp:
print(" for %i free variables" % len(self._spvars))
print(" in %i signomial constraints"
% len(self["SP constraints"]))
Expand Down Expand Up @@ -210,10 +191,7 @@ def localsolve(self, solver=None, *, verbosity=1, x0=None, reltol=1e-4,
print("Solving took %.3g seconds and %i GP solves."
% (self.result["soltime"], len(self.gps)))
self.model.process_result(self.result)
if self.externalfn_vars:
for v in self.externalfn_vars:
self[0].insert(0, v.key.externalfn) # for constraint senss
if self.slack.key in self.result["variables"]:
if getattr(self.slack, "key", None) in self.result["variables"]:
excess_slack = self.result["variables"][self.slack.key] - 1
if excess_slack <= EPS:
del self.result["freevariables"][self.slack.key]
Expand Down Expand Up @@ -254,12 +232,12 @@ def init_gp(self, x0=None, **initgpargs):
constraints = OrderedDict({"SP approximations": []})
constraints["GP constraints"] = self["GP constraints"]
self._spvars = set([self.slack])
for cs, lts in zip(self["SP constraints"], self._lt_approxs):
for lt, gt in zip(lts, cs.as_approxgts(x0)):
constraint = (lt <= gt)
for cs in self["SP constraints"]:
for posylt1 in cs.approx_as_posyslt1(x0):
constraint = (posylt1 <= self.slack)
constraint.generated_by = cs
constraints["SP approximations"].append(constraint)
self._spvars.update({vk for vk in gt.varkeys
self._spvars.update({vk for vk in posylt1.varkeys
if vk not in self.substitutions})
gp = GeometricProgram(self.cost, constraints, self.substitutions,
**initgpargs)
Expand All @@ -278,10 +256,10 @@ def update_gp(self, x0):
gp = self._gp
gp.x0.update({k: v for (k, v) in x0.items() if k in self._spvars})
p_idx = 0 # TODO: use .as_gpconstr in the below (it's fast enough)
for sp_constraint, lts in zip(self["SP constraints"], self._lt_approxs):
for lt, gt in zip(lts, sp_constraint.as_approxgts(gp.x0)):
for sp_constraint in self["SP constraints"]:
for posylt1 in sp_constraint.approx_as_posyslt1(gp.x0):
approx_constraint = gp["SP approximations"][p_idx]
approx_constraint.unsubbed = [lt/gt]
approx_constraint.unsubbed = [posylt1/self.slack]
p_idx += 1 # p_idx=0 is the cost; sp constraints are after it
hmap, = approx_constraint.as_hmapslt1(self.substitutions)
gp.hmaps[p_idx] = hmap
Expand All @@ -301,12 +279,6 @@ def gp(self, x0=None, **gpinitargs):
x0 = self._fill_x0(x0)
constraints = OrderedDict(
{"SP constraints": [c.as_gpconstr(x0) for c in self.model.flat()]})
if self.externalfn_vars:
constraints["Generated by externalfns"] = []
for v in self.externalfn_vars:
constraint = v.key.externalfn(v, x0)
constraint.generated_by = v.key.externalfn
constraints["Generated by externalfns"].append(constraint)
gp = GeometricProgram(self._original_cost,
constraints, self.substitutions, **gpinitargs)
gp.x0 = x0
Expand Down
32 changes: 10 additions & 22 deletions gpkit/nomials/math.py
Original file line number Diff line number Diff line change
Expand Up @@ -677,17 +677,12 @@ def as_gpconstr(self, x0):
pconstr.generated_by = self
return pconstr

def as_approxlts(self):
"Returns posynomial-less-than sides of a signomial constraint"
def approx_as_posyslt1(self, x0):
siglt0, = self.unsubbed
posy, self._negy = siglt0.posy_negy() # pylint: disable=attribute-defined-outside-init
return [posy]

def as_approxgts(self, x0):
"Returns monomial-greater-than sides, to be called after as_approxlt1"
posy, negy = siglt0.posy_negy()
# default guess of 1.0 for unspecified negy variables
x0.update({vk: 1.0 for vk in self._negy.varkeys if vk not in x0})
return [self._negy.mono_lower_bound(x0)]
x0.update({vk: 1.0 for vk in negy.vks if vk not in x0})
return [posy/negy.mono_lower_bound(x0)]


class SingleSignomialEquality(SignomialInequality):
Expand All @@ -712,17 +707,10 @@ def as_gpconstr(self, x0):
mec.generated_by = self
return mec

def as_approxlts(self):
"Returns posynomial-less-than sides of a signomial constraint"
siglt0, = self.unsubbed
self._posy, self._negy = siglt0.posy_negy() # pylint: disable=attribute-defined-outside-init
return Monomial(1), Monomial(1) # no 'fixed' posy_lt for a SigEq

def as_approxgts(self, x0):
"Returns monomial-greater-than sides, to be called after as_approxlt1"
# default guess of 1.0 for unspecified variables
def approx_as_posyslt1(self, x0):
siglt0, = self.unsubbed
x0.update({vk: 1.0 for vk in siglt0.varkeys if vk not in x0})
lhs = self._posy.mono_lower_bound(x0)
rhs = self._negy.mono_lower_bound(x0)
return lhs/rhs, rhs/lhs
posy, negy = siglt0.posy_negy()
# assume unspecified variables have a value of 1.0
x0.update({vk: 1.0 for vk in siglt0.vks if vk not in x0})
plb, nlb = posy.mono_lower_bound(x0), negy.mono_lower_bound(x0)
return [plb/nlb, nlb/plb]
3 changes: 0 additions & 3 deletions gpkit/tests/t_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,6 @@ def test_sin_approx_example(self, example):
def test_external_sp(self, example):
pass

def test_external_sp2(self, example):
pass

def test_simpleflight(self, example):
self.assertTrue(example.sol.almost_equal(example.sol_loaded))
for sol in [example.sol, example.sol_loaded]:
Expand Down

0 comments on commit 1b2039b

Please sign in to comment.