From 96e36c7cdb1a8cdb730d3852c33c380466642a2c Mon Sep 17 00:00:00 2001 From: Edward Burnell Date: Sat, 7 Mar 2020 11:38:58 -0800 Subject: [PATCH] major refactor --- docs/source/examples/boundschecking.py | 2 +- docs/source/examples/external_constraint.py | 6 +- gpkit/constraints/gp.py | 38 ++---- gpkit/constraints/model.py | 6 +- gpkit/constraints/prog_factories.py | 7 +- gpkit/constraints/set.py | 5 +- gpkit/constraints/sgp.py | 136 ++++++++------------ gpkit/exceptions.py | 20 +-- gpkit/nomials/math.py | 83 +++++------- gpkit/tests/t_model.py | 10 +- 10 files changed, 126 insertions(+), 187 deletions(-) diff --git a/docs/source/examples/boundschecking.py b/docs/source/examples/boundschecking.py index e1ba32fde..1e3f2e5a8 100644 --- a/docs/source/examples/boundschecking.py +++ b/docs/source/examples/boundschecking.py @@ -49,7 +49,7 @@ def setup(self): m.solve() except UnboundedGP: gp = m.gp(checkbounds=False) - missingbounds = gp.check_bounds(err_on_missing_bounds=False) + missingbounds = gp.check_bounds() try: sol = gp.solve(verbosity=0) # Errors on mosek_cli diff --git a/docs/source/examples/external_constraint.py b/docs/source/examples/external_constraint.py index d97e13e9f..e2e26be82 100644 --- a/docs/source/examples/external_constraint.py +++ b/docs/source/examples/external_constraint.py @@ -17,13 +17,13 @@ def as_hmapslt1(self, _): "Ensures this is treated as an SGP constraint" raise InvalidGPConstraint("ExternalConstraint cannot solve as a GP.") - def approx_as_posyslt1(self, x0): + def as_gpconstr(self, x0): "Returns locally-approximating GP constraint" # Creating a default constraint for the first solve if self.x not in x0: - return [self.x/self.y] + return (self.y >= self.x) # Otherwise calls external code at the current position... x_star = x0[self.x] res = external_code(x_star) # ...and returns a linearized posy <= 1 - return [res*self.x/x_star/self.y] + return (self.y >= res * self.x/x_star) diff --git a/gpkit/constraints/gp.py b/gpkit/constraints/gp.py index db051909b..34dcbddf0 100644 --- a/gpkit/constraints/gp.py +++ b/gpkit/constraints/gp.py @@ -8,7 +8,6 @@ from ..keydict import KeyDict from ..solution_array import SolutionArray from .set import ConstraintSet -from .costed import CostedConstraintSet from ..exceptions import (InvalidPosynomial, Infeasible, UnknownInfeasible, PrimalInfeasible, DualInfeasible, UnboundedGP) @@ -48,7 +47,7 @@ def _get_solver(solver, kwargs): return solver, optimize -class GeometricProgram(CostedConstraintSet): +class GeometricProgram: # pylint: disable=too-many-instance-attributes """Standard mathematical representation of a GP. @@ -69,16 +68,8 @@ class GeometricProgram(CostedConstraintSet): """ _result = solve_log = solver_out = model = v_ss = nu_by_posy = None - def __init__(self, cost, constraints, substitutions, - *, checkbounds=True): - # pylint:disable=super-init-not-called,non-parent-init-called + def __init__(self, cost, constraints, substitutions, *, checkbounds=True): self.cost, self.substitutions = cost, substitutions - if isinstance(constraints, dict): - self.idxlookup = {k: i for i, k in enumerate(constraints)} - constraints = list(constraints.values()) - elif isinstance(constraints, ConstraintSet): - constraints = [constraints] - list.__init__(self, constraints) for key, sub in self.substitutions.items(): if isinstance(sub, FixedScalar): sub = sub.value @@ -90,8 +81,9 @@ def __init__(self, cost, constraints, substitutions, " %s." % (key, sub, type(sub))) cost_hmap = cost.hmap.sub(self.substitutions, cost.varkeys) if any(c <= 0 for c in cost_hmap.values()): - raise InvalidPosynomial("cost must be a Posynomial") - self.hmaps = [cost_hmap] + list(self.as_hmapslt1(self.substitutions)) + raise InvalidPosynomial("a GP's cost must be Posynomial") + hmapgen = ConstraintSet.as_hmapslt1(constraints, self.substitutions) + self.hmaps = [cost_hmap] + list(hmapgen) self.gen() # Generate various maps into the posy- and monomials if checkbounds: self.check_bounds(err_on_missing_bounds=True) @@ -166,8 +158,7 @@ def gen(self): self.A = CootMatrix(row, col, data) # pylint: disable=too-many-statements, too-many-locals - def solve(self, solver=None, *, verbosity=1, - process_result=True, gen_result=True, **kwargs): + def solve(self, solver=None, *, verbosity=1, gen_result=True, **kwargs): """Solves a GeometricProgram and returns the solution. Arguments @@ -233,10 +224,9 @@ def solve(self, solver=None, *, verbosity=1, if gen_result: # NOTE: SIDE EFFECTS self._result = self.generate_result(solver_out, - verbosity=verbosity-2, - process_result=process_result) + verbosity=verbosity-2) return self.result - + # TODO: remove this "generate_result" closure solver_out["generate_result"] = \ lambda: self.generate_result(solver_out, dual_check=False) @@ -249,20 +239,17 @@ def result(self): self._result = self.generate_result(self.solver_out) return self._result - def generate_result(self, solver_out, *, verbosity=0, - process_result=True, dual_check=True): + def generate_result(self, solver_out, *, verbosity=0, dual_check=True): "Generates a full SolutionArray and checks it." if verbosity > 0: soltime = solver_out["soltime"] tic = time() - # result packing # result = self._compile_result(solver_out) # NOTE: SIDE EFFECTS if verbosity > 0: print("Result packing took %.2g%% of solve time." % ((time() - tic) / soltime * 100)) tic = time() - # solution checking # try: tol = SOLUTION_TOL.get(solver_out["solver"], 1e-5) @@ -276,13 +263,6 @@ def generate_result(self, solver_out, *, verbosity=0, print("Solution checking took %.2g%% of solve time." % ((time() - tic) / soltime * 100)) tic = time() - - # result processing # - if process_result: - self.process_result(result) - if verbosity > 0: - print("Processing results took %.2g%% of solve time." % - ((time() - tic) / soltime * 100)) return result def _generate_nula(self, solver_out): diff --git a/gpkit/constraints/model.py b/gpkit/constraints/model.py index 48b13cfa1..6bbac12a3 100644 --- a/gpkit/constraints/model.py +++ b/gpkit/constraints/model.py @@ -166,11 +166,7 @@ def autosweep(self, sweeps, tol=0.01, samplepoints=100, **solveargs): # pylint: disable=too-many-locals,too-many-branches,too-many-statements def debug(self, solver=None, verbosity=1, **solveargs): - """Attempts to diagnose infeasible models. - - If a model debugs but errors in a process_result call, debug again - with `process_results=False` - """ + "Attempts to diagnose infeasible models." from .relax import ConstantsRelaxed, ConstraintsRelaxed from .bounded import Bounded diff --git a/gpkit/constraints/prog_factories.py b/gpkit/constraints/prog_factories.py index 8810d8a1e..311e55796 100644 --- a/gpkit/constraints/prog_factories.py +++ b/gpkit/constraints/prog_factories.py @@ -124,6 +124,8 @@ def solvefn(self, solver=None, *, verbosity=1, skipsweepfailures=False, else: self.program, progsolve = genfunction(self) result = progsolve(solver, verbosity=verbosity, **solveargs) + if solveargs.get("process_result", True): + self.process_result(result) solution.append(result) solution.to_arrays() self.solution = solution @@ -162,7 +164,10 @@ def run_sweep(genfunction, self, solution, skipsweepfailures, program, solvefn = genfunction(self, constants) self.program.append(program) # NOTE: SIDE EFFECTS try: - solution.append(solvefn(solver, verbosity=verbosity-1, **solveargs)) + result = solvefn(solver, verbosity=verbosity-1, **solveargs) + if solveargs.get("process_result", True): + self.process_result(result) + solution.append(result) except Infeasible as e: last_error = e if not skipsweepfailures: diff --git a/gpkit/constraints/set.py b/gpkit/constraints/set.py index a1b93d746..b5ca733c7 100644 --- a/gpkit/constraints/set.py +++ b/gpkit/constraints/set.py @@ -159,8 +159,9 @@ def constrained_varkeys(self): def as_hmapslt1(self, subs): "Yields hmaps<=1 from self.flat()" - yield from chain(*(l.as_hmapslt1(subs) - for l in self.flat(yield_if_hasattr="as_hmapslt1"))) + yield from chain(*(c.as_hmapslt1(subs) + for c in flatiter(self, + yield_if_hasattr="as_hmapslt1"))) def process_result(self, result): """Does arbitrary computation / manipulation of a program's result diff --git a/gpkit/constraints/sgp.py b/gpkit/constraints/sgp.py index 20713d45c..5c06b31b2 100644 --- a/gpkit/constraints/sgp.py +++ b/gpkit/constraints/sgp.py @@ -1,22 +1,20 @@ """Implement the SequentialGeometricProgram class""" from time import time -from collections import OrderedDict, defaultdict +from collections import defaultdict import numpy as np from ..exceptions import (InvalidGPConstraint, Infeasible, UnnecessarySGP, - InvalidSGP) + InvalidPosynomial, InvalidSGPConstraint) from ..keydict import KeyDict from ..nomials import Variable from .gp import GeometricProgram -from .set import sort_constraints_dict -from ..nomials import PosynomialInequality +from ..nomials import PosynomialInequality, Posynomial from .. import NamedVariables -from .costed import CostedConstraintSet EPS = 1e-6 # 1 +/- this is used in a few relative differences # pylint: disable=too-many-instance-attributes -class SequentialGeometricProgram(CostedConstraintSet): +class SequentialGeometricProgram: """Prepares a collection of signomials for a SP solve. Arguments @@ -46,53 +44,58 @@ class SequentialGeometricProgram(CostedConstraintSet): >>> gp.solve() """ gps = solver_outs = _results = result = model = None - _gp = _spvars = pccp_penalty = None with NamedVariables("SGP"): slack = Variable("PCCPslack") def __init__(self, cost, model, substitutions, *, use_pccp=True, pccp_penalty=2e2, checkbounds=True): + self.substitutions = substitutions + self.pccp_penalty = pccp_penalty if cost.any_nonpositive_cs: - raise InvalidSGP("""Sequential GPs need Posynomial objectives. + raise InvalidPosynomial("""an SGP's cost must be Posynomial The equivalent of a Signomial objective can be constructed by constraining a dummy variable `z` to be greater than the desired Signomial objective `s` (z >= s) and then minimizing that dummy variable.""") - self._original_cost = cost - self.model = model - self.substitutions = substitutions - - sgpconstraints = {"SP constraints": [], "GP constraints": []} + self.gpconstraints, self.sgpconstraints = [], [] + if not use_pccp: + self.slack = 1 + else: + self.gpconstraints.append(self.slack >= 1) + cost *= self.slack**pccp_penalty + self.approxconstraints = [] + self.sgpvks = set() for cs in model.flat(): try: if not isinstance(cs, PosynomialInequality): cs.as_hmapslt1(substitutions) # gp-compatible? - sgpconstraints["GP constraints"].append(cs) + self.gpconstraints.append(cs) except InvalidGPConstraint: - if not hasattr(cs, "approx_as_posyslt1"): - raise InvalidSGPConstraint() - sgpconstraints["SP constraints"].append(cs) - # all constraints seem SP-compatible - if not sgpconstraints["SP constraints"]: + if not hasattr(cs, "as_gpconstr"): + raise InvalidSGPConstraint(cs) + self.sgpconstraints.append(cs) + for hmaplt1 in cs.as_gpconstr({}).as_hmapslt1({}): + constraint = (Posynomial(hmaplt1) <= self.slack) + constraint.generated_by = cs + self.approxconstraints.append(constraint) + self.sgpvks.update(constraint.varkeys) + if not self.sgpconstraints: raise UnnecessarySGP("""Model valid as a Geometric Program. SequentialGeometricPrograms should only be created with Models containing Signomial Constraints, since Models without Signomials have global solutions and can be solved with 'Model.solve()'.""") - - 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 - sgpconstraints["GP constraints"].append(self.slack >= 1) - - keys, sgpconstraints = sort_constraints_dict(sgpconstraints) - self.idxlookup = {k: i for i, k in enumerate(keys)} - list.__init__(self, sgpconstraints) # pylint: disable=non-parent-init-called - self._gp = self.init_gp(checkbounds=checkbounds) + self._gp = GeometricProgram( + cost, self.approxconstraints + self.gpconstraints, + substitutions, checkbounds=checkbounds) + self._gp.x0 = KeyDict() + self._gp.x0.varkeys = model.varkeys # for string access, etc. + self.a_idxs = defaultdict(list) + cost_mons = self._gp.k[0] + sp_mons = sum(self._gp.k[:1+len(self.approxconstraints)]) + for row_idx, m_idx in enumerate(self._gp.A.row): + if cost_mons <= m_idx <= sp_mons: + self.a_idxs[self._gp.p_idxs[m_idx]].append(row_idx) # pylint: disable=too-many-locals,too-many-branches,too-many-statements def localsolve(self, solver=None, *, verbosity=1, x0=None, reltol=1e-4, @@ -131,9 +134,8 @@ def localsolve(self, solver=None, *, verbosity=1, x0=None, reltol=1e-4, starttime = time() if verbosity > 0: print("Starting a sequence of GP solves") - print(" for %i free variables" % len(self._spvars)) - print(" in %i signomial constraints" - % len(self["SP constraints"])) + print(" for %i free variables" % len(self.sgpvks)) + print(" in %i locally-GP constraints" % len(self.sgpconstraints)) print(" and for %i free variables" % len(self._gp.varlocs)) print(" in %i posynomial inequalities." % len(self._gp.k)) prevcost, cost, rel_improvement = None, None, None @@ -145,7 +147,6 @@ def localsolve(self, solver=None, *, verbosity=1, x0=None, reltol=1e-4, " if they're converging, try `.localsolve(...," " iteration_limit=NEWLIMIT)`." % len(self.gps)) gp = self.gp(x0, cleanx0=True) - gp.model = self.model self.gps.append(gp) # NOTE: SIDE EFFECTS if verbosity > 1: print("\nGP Solve %i" % len(self.gps)) @@ -159,7 +160,7 @@ def localsolve(self, solver=None, *, verbosity=1, x0=None, reltol=1e-4, if verbosity > 2: result = gp.generate_result(solver_out, verbosity=verbosity-3) self._results.append(result) - print(result.table(self._spvars)) + print(result.table(self.sgpvks)) elif verbosity > 1: print("Solved cost was %.4g." % cost) if prevcost is None: @@ -180,14 +181,13 @@ def localsolve(self, solver=None, *, verbosity=1, x0=None, reltol=1e-4, if verbosity > 0: print("Solving took %.3g seconds and %i GP solves." % (self.result["soltime"], len(self.gps))) - self.model.process_result(self.result) - if getattr(self.slack, "key", None) in self.result["variables"]: + if hasattr(self.slack, "key"): excess_slack = self.result["variables"][self.slack.key] - 1 if excess_slack <= EPS: del self.result["freevariables"][self.slack.key] del self.result["variables"][self.slack.key] del self.result["sensitivities"]["variables"][self.slack.key] - slackconstraint = self["GP constraints"][-1] + slackconstraint = self.gpconstraints[0] del self.result["sensitivities"]["constraints"][slackconstraint] elif verbosity > -1: print("Final solution let signomial constraints slacken by" @@ -206,55 +206,29 @@ def results(self): self._results = [o["generate_result"]() for o in self.solver_outs] return self._results - def init_gp(self, checkbounds=True): - "Generates a simplified GP representation for later modification" - x0 = KeyDict() - x0.varkeys = self.model.varkeys - x0.update(self.substitutions) - # OrderedDict so that SP constraints are at the first indices - constraints = OrderedDict({"SP approximations": []}) - constraints["GP constraints"] = self["GP constraints"] - self._spvars = set() - 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(posylt1.varkeys) - gp = GeometricProgram(self.cost, constraints, self.substitutions, - checkbounds=checkbounds) - gp.x0 = x0 - self.a_idxs = defaultdict(list) - cost_mons, sp_mons = gp.k[0], sum(gp.k[:1+len(self["SP constraints"])]) - for row_idx, m_idx in enumerate(gp.A.row): - if cost_mons <= m_idx <= sp_mons: - self.a_idxs[gp.p_idxs[m_idx]].append(row_idx) - return gp - - def gp(self, x0={}, *, cleanx0=False): + def gp(self, x0=None, *, cleanx0=False): "Update self._gp for x0 and return it." if not x0: return self._gp # return last generated if not cleanx0: x0 = KeyDict(x0) - gp = self._gp - gp.x0.update({vk: x0[vk] for vk in self._spvars if vk in x0}) + self._gp.x0.update({vk: x0[vk] for vk in self.sgpvks if vk in x0}) p_idx = 0 - 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 = [posylt1/self.slack] + for sp_constraint in self.sgpconstraints: + for hmaplt1 in sp_constraint.as_gpconstr(self._gp.x0).as_hmapslt1({}): + approx_constraint = self.approxconstraints[p_idx] + approx_constraint.unsubbed = [Posynomial(hmaplt1)/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 - m_idx = gp.m_idxs[p_idx].start + self._gp.hmaps[p_idx] = hmap + m_idx = self._gp.m_idxs[p_idx].start a_idxs = list(self.a_idxs[p_idx]) # A's entries we can modify for i, (exp, c) in enumerate(hmap.items()): - gp.exps[m_idx + i] = exp - gp.cs[m_idx + i] = c + self._gp.exps[m_idx + i] = exp + self._gp.cs[m_idx + i] = c for var, x in exp.items(): row_idx = a_idxs.pop() # modify a particular A entry - gp.A.row[row_idx] = m_idx + i - gp.A.col[row_idx] = gp.varidxs[var] - gp.A.data[row_idx] = x - return gp + self._gp.A.row[row_idx] = m_idx + i + self._gp.A.col[row_idx] = self._gp.varidxs[var] + self._gp.A.data[row_idx] = x + return self._gp diff --git a/gpkit/exceptions.py b/gpkit/exceptions.py index 0c809f15f..42a92d36a 100644 --- a/gpkit/exceptions.py +++ b/gpkit/exceptions.py @@ -5,30 +5,30 @@ class MathematicallyInvalid(TypeError): "Raised whenever something violates a mathematical definition." class InvalidPosynomial(MathematicallyInvalid): - "Raised when a Posynomial would be created with a negative coefficient" + "Raised if a Posynomial would be created with a negative coefficient" class InvalidGPConstraint(MathematicallyInvalid): - "Raised when a non-GP-compatible constraint is used in a GP" + "Raised if a non-GP-compatible constraint is used in a GP" -class InvalidSGP(MathematicallyInvalid): - "Raised when a non-GP-compatible constraint is used in a GP" +class InvalidSGPConstraint(MathematicallyInvalid): + "Raised if a non-SGP-compatible constraint is used in an SGP" class UnnecessarySGP(ValueError): - "Raised when an SGP is fully GP-compatible" + "Raised if an SGP is fully GP-compatible" class UnboundedGP(ValueError): - "Raise when a GP is not fully bounded" + "Raise if a GP is not fully bounded" class Infeasible(RuntimeWarning): - "Raised when a model does not solve" + "Raised if a model does not solve" class UnknownInfeasible(Infeasible): - "Raised when a model does not solve for unknown reasons" + "Raised if a model does not solve for unknown reasons" class PrimalInfeasible(Infeasible): - "Raised when a model returns a certificate of primal infeasibility" + "Raised if a model returns a certificate of primal infeasibility" class DualInfeasible(Infeasible): - "Raised when a model returns a certificate of dual infeasibility" + "Raised if a model returns a certificate of dual infeasibility" diff --git a/gpkit/nomials/math.py b/gpkit/nomials/math.py index ad604be34..6ac2f7da0 100644 --- a/gpkit/nomials/math.py +++ b/gpkit/nomials/math.py @@ -595,37 +595,35 @@ def as_hmapslt1(self, substitutions=None): " constraint 0 <= %s after substitution." % (self, negy)) return [] if negy is 0: # pylint: disable=literal-comparison - raise ValueError("SignomialConstraint %s became the infeasible" - " constraint %s <= 0 after substitution." % - (self, posy)) - if not hasattr(negy, "cs") or len(negy.cs) == 1: - # all but one of the negy terms becomes compatible with the posy - p_ineq = PosynomialInequality(posy, "<=", negy) - p_ineq.parent = self - siglt0_us, = self.unsubbed - siglt0_hmap = siglt0_us.hmap.sub(substitutions, siglt0_us.varkeys) - negy_hmap = NomialMap() - posy_hmaps = defaultdict(NomialMap) - for o_exp, exp in siglt0_hmap.expmap.items(): - if exp == negy.exp: - negy_hmap[o_exp] = -siglt0_us.hmap[o_exp] - else: - posy_hmaps[exp-negy.exp][o_exp] = siglt0_us.hmap[o_exp] - # pylint: disable=attribute-defined-outside-init - self._mons = [Monomial(NomialMap({k: v})) - for k, v in (posy/negy).hmap.items()] - self._negysig = Signomial(negy_hmap, require_positive=False) - self._coeffsigs = {exp: Signomial(hmap, require_positive=False) - for exp, hmap in posy_hmaps.items()} - self._sigvars = {exp: (list(self._negysig.varkeys) - + list(sig.varkeys)) - for exp, sig in self._coeffsigs.items()} - return p_ineq.as_hmapslt1(substitutions) - - raise InvalidGPConstraint("SignomialInequality could not simplify to a" - " PosynomialInequality; try calling" - " `.localsolve` instead of `.solve` to form" - " your Model as a SequentialGeometricProgram") + raise ValueError("%s became the infeasible constraint %s <= 0" + " after substitution." % (self, posy)) + if hasattr(negy, "cs") and len(negy.cs) > 1: + raise InvalidGPConstraint( + "%s did not simplify to a PosynomialInequality; try calling" + " `.localsolve` instead of `.solve` to form your Model as a" + " SequentialGeometricProgram." % self) + # all but one of the negy terms becomes compatible with the posy + p_ineq = PosynomialInequality(posy, "<=", negy) + p_ineq.parent = self + siglt0_us, = self.unsubbed + siglt0_hmap = siglt0_us.hmap.sub(substitutions, siglt0_us.varkeys) + negy_hmap = NomialMap() + posy_hmaps = defaultdict(NomialMap) + for o_exp, exp in siglt0_hmap.expmap.items(): + if exp == negy.exp: + negy_hmap[o_exp] = -siglt0_us.hmap[o_exp] + else: + posy_hmaps[exp-negy.exp][o_exp] = siglt0_us.hmap[o_exp] + # pylint: disable=attribute-defined-outside-init + self._mons = [Monomial(NomialMap({k: v})) + for k, v in (posy/negy).hmap.items()] + self._negysig = Signomial(negy_hmap, require_positive=False) + self._coeffsigs = {exp: Signomial(hmap, require_positive=False) + for exp, hmap in posy_hmaps.items()} + self._sigvars = {exp: (list(self._negysig.varkeys) + + list(sig.varkeys)) + for exp, sig in self._coeffsigs.items()} + return p_ineq.as_hmapslt1(substitutions) def sens_from_dual(self, la, nu, result): """ We want to do the following chain: @@ -672,18 +670,11 @@ def as_gpconstr(self, x0): siglt0, = self.unsubbed posy, negy = siglt0.posy_negy() # default guess of 1.0 for unspecified negy variables - x0.update({vk: 1.0 for vk in negy.vks if vk not in x0}) + x0 = {vk: x0.get(vk, 1) for vk in negy.vks} pconstr = PosynomialInequality(posy, "<=", negy.mono_lower_bound(x0)) pconstr.generated_by = self return pconstr - def approx_as_posyslt1(self, x0): - siglt0, = self.unsubbed - posy, negy = siglt0.posy_negy() - # default guess of 1.0 for unspecified negy variables - 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): "A constraint of the general form posynomial == posynomial" @@ -695,22 +686,14 @@ def __init__(self, left, right): def as_hmapslt1(self, substitutions=None): "Returns the posys <= 1 representation of this constraint." - raise InvalidGPConstraint("SignomialEquality is not GP compatible.") + raise InvalidGPConstraint(self) def as_gpconstr(self, x0): "Returns GP approximation of an SP constraint at x0" siglt0, = self.unsubbed 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}) + # default guess of 1.0 for unspecified negy variables + x0 = {vk: x0.get(vk, 1) for vk in siglt0.vks} mec = (posy.mono_lower_bound(x0) == negy.mono_lower_bound(x0)) mec.generated_by = self return mec - - def approx_as_posyslt1(self, x0): - siglt0, = self.unsubbed - 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] diff --git a/gpkit/tests/t_model.py b/gpkit/tests/t_model.py index 3ea27a198..5e181089a 100644 --- a/gpkit/tests/t_model.py +++ b/gpkit/tests/t_model.py @@ -10,8 +10,8 @@ from gpkit.constraints.relax import ConstraintsRelaxed from gpkit.constraints.relax import ConstraintsRelaxedEqually from gpkit.constraints.relax import ConstantsRelaxed -from gpkit.exceptions import (UnknownInfeasible, - InvalidGPConstraint, UnnecessarySGP, InvalidSGP, +from gpkit.exceptions import (UnknownInfeasible, InvalidPosynomial, + InvalidGPConstraint, UnnecessarySGP, PrimalInfeasible, DualInfeasible, UnboundedGP) @@ -92,7 +92,7 @@ def test_601(self): [x >= 1, y == 2]) m.solve(solver=self.solver, verbosity=0) - self.assertEqual(len(m.program[0]), 2) # pylint:disable=unsubscriptable-object + self.assertEqual(len(list(m.as_hmapslt1({}))), 3) self.assertEqual(len(m.program.hmaps), 2) def test_cost_freeing(self): @@ -588,7 +588,7 @@ def test_sigs_not_allowed_in_cost(self): y = Variable("y") J = 0.01*((x - 1)**2 + (y - 1)**2) + (x*y - 1)**2 m = Model(J) - with self.assertRaises(InvalidSGP): + with self.assertRaises(InvalidPosynomial): m.localsolve(verbosity=0, solver=self.solver) def test_partial_sub_signomial(self): @@ -598,7 +598,7 @@ def test_partial_sub_signomial(self): with SignomialsEnabled(): m = Model(x, [x + y >= 1, y <= 0.5]) gp = m.sp().gp(x0={x: 0.5}) # pylint: disable=no-member - first_gp_constr_posy_exp, = list(gp.as_hmapslt1({}))[0] + first_gp_constr_posy_exp, = gp.hmaps[1] # first after cost self.assertEqual(first_gp_constr_posy_exp[x.key], -1./3) def test_becomes_signomial(self):