diff --git a/docs/source/examples/performance_modeling_output.txt b/docs/source/examples/performance_modeling_output.txt index 7939de518..bab074f6d 100644 --- a/docs/source/examples/performance_modeling_output.txt +++ b/docs/source/examples/performance_modeling_output.txt @@ -6,41 +6,41 @@ Cost Constraints ----------- Mission - "definition of Wburn": - Wfuel[:-1] >= Wfuel[1:] + Wburn[:-1] - "require fuel for the last leg": - Wfuel[3] >= Wburn[3] - - FlightSegment - AircraftP - "fuel burn rate": - Wburn[:] >= 0.1·D[:] - "lift": - Aircraft.W + Wfuel[:] <= 0.5·rho[:]·CL[:]·S·V[:]² - "performance": - WingAero - "definition of D": - D[:] >= 0.5·rho[:]·V[:]²·CD[:]·S - "definition of Re": - Re[:] = rho[:]·V[:]·c/mu[:] - "drag model": - CD[:] >= 0.074/Re[:]^0.2 + CL[:]²/π/A/e[:] - - FlightState - (no constraints) - + "definition of Wburn": + Wfuel[:-1] >= Wfuel[1:] + Wburn[:-1] + "require fuel for the last leg": + Wfuel[3] >= Wburn[3] + + FlightSegment + AircraftP + "fuel burn rate": + Wburn[:] >= 0.1·D[:] + "lift": + Aircraft.W + Wfuel[:] <= 0.5·rho[:]·CL[:]·S·V[:]² + "performance": + WingAero + "definition of D": + D[:] >= 0.5·rho[:]·V[:]²·CD[:]·S + "definition of Re": + Re[:] = rho[:]·V[:]·c/mu[:] + "drag model": + CD[:] >= 0.074/Re[:]^0.2 + CL[:]²/π/A/e[:] + + FlightState + (no constraints) + Aircraft - "definition of W": - Aircraft.W >= Aircraft.Fuselage.W + Aircraft.Wing.W - "components": - Fuselage - (no constraints) - - Wing - "definition of mean chord": - c = (S/A)^0.5 - "parametrization of wing weight": - Aircraft.Wing.W >= S·Aircraft.Wing.rho + "definition of W": + Aircraft.W >= Aircraft.Fuselage.W + Aircraft.Wing.W + "components": + Fuselage + (no constraints) + + Wing + "definition of mean chord": + c = (S/A)^0.5 + "parametrization of wing weight": + Aircraft.Wing.W >= S·Aircraft.Wing.rho Optimal Cost ------------ @@ -108,9 +108,9 @@ Constraint Differences +++ added in argument @@ -41,4 +41,3 @@ - c = (S/A)^0.5 - "parametrization of wing weight": - Aircraft.Wing.W >= S·Aircraft.Wing.rho + c = (S/A)^0.5 + "parametrization of wing weight": + Aircraft.Wing.W >= S·Aircraft.Wing.rho - Wburn[:] >= 0.2·D[:] ********************** diff --git a/docs/source/examples/relaxation_output.txt b/docs/source/examples/relaxation_output.txt index 6af5b8e09..c9c93db6e 100644 --- a/docs/source/examples/relaxation_output.txt +++ b/docs/source/examples/relaxation_output.txt @@ -19,11 +19,11 @@ Cost Constraints ----------- - "minimum relaxation": - C >= 1 - "relaxed constraints": - x <= C·x_max - x_min <= C·x + "minimum relaxation": + C >= 1 + "relaxed constraints": + x <= C·x_max + x_min <= C·x Optimal Cost ------------ @@ -61,11 +61,11 @@ Cost Constraints ----------- - "minimum relaxation": - C[:] >= 1 - "relaxed constraints": - x <= C[0]·x_max - x_min <= C[1]·x + "minimum relaxation": + C[:] >= 1 + "relaxed constraints": + x <= C[0]·x_max + x_min <= C[1]·x Optimal Cost ------------ @@ -105,18 +105,18 @@ Cost Constraints ----------- Relax2 - "original constraints": - x <= x_max - x >= x_min - "relaxation constraints": - "x_max": - Relax2.x_max >= 1 - x_max >= Relax2.OriginalValues.x_max/Relax2.x_max - x_max <= Relax2.OriginalValues.x_max·Relax2.x_max - "x_min": - Relax2.x_min >= 1 - x_min >= Relax2.OriginalValues.x_min/Relax2.x_min - x_min <= Relax2.OriginalValues.x_min·Relax2.x_min + "original constraints": + x <= x_max + x >= x_min + "relaxation constraints": + "x_max": + Relax2.x_max >= 1 + x_max >= Relax2.OriginalValues.x_max/Relax2.x_max + x_max <= Relax2.OriginalValues.x_max·Relax2.x_max + "x_min": + Relax2.x_min >= 1 + x_min >= Relax2.OriginalValues.x_min/Relax2.x_min + x_min <= Relax2.OriginalValues.x_min·Relax2.x_min Optimal Cost ------------ diff --git a/gpkit/constraints/array.py b/gpkit/constraints/array.py index 4ad452cc7..1ffe11538 100644 --- a/gpkit/constraints/array.py +++ b/gpkit/constraints/array.py @@ -1,9 +1,8 @@ "Implements ArrayConstraint" -from .set import ConstraintSet from .single_equation import SingleEquationConstraint -class ArrayConstraint(SingleEquationConstraint, ConstraintSet): +class ArrayConstraint(SingleEquationConstraint, list): """A ConstraintSet for prettier array-constraint printing. ArrayConstraint gets its `sub` method from ConstrainSet, @@ -14,10 +13,16 @@ class ArrayConstraint(SingleEquationConstraint, ConstraintSet): """ def __init__(self, constraints, left, oper, right): SingleEquationConstraint.__init__(self, left, oper, right) - ConstraintSet.__init__(self, constraints) + list.__init__(self, constraints) + self.constraints = constraints + + def __iter__(self): + yield from self.constraints.flat + + def lines_without(self, excluded): + "Returns lines for indentation in hierarchical printing." + return self.str_without(excluded).split("\n") def __bool__(self): "Allows the use of '=' NomialArrays as truth elements." - if self.oper != "=": - return NotImplemented - return all(bool(p) for p in self.flat()) + return False if self.oper != "=" else bool(self.constraints.all()) diff --git a/gpkit/constraints/gp.py b/gpkit/constraints/gp.py index 7a1ea6d1a..8eede8ad9 100644 --- a/gpkit/constraints/gp.py +++ b/gpkit/constraints/gp.py @@ -90,7 +90,7 @@ def __init__(self, cost, constraints, substitutions, 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.flathmaps(self.substitutions)) + self.hmaps = [cost_hmap] + list(self.as_hmapslt1(self.substitutions)) ## Generate various maps into the posy- and monomials # k [j]: number of monomials (rows of A) present in each constraint self.k = [len(hm) for hm in self.hmaps] diff --git a/gpkit/constraints/set.py b/gpkit/constraints/set.py index 755006342..3100e8b6d 100644 --- a/gpkit/constraints/set.py +++ b/gpkit/constraints/set.py @@ -2,10 +2,9 @@ from collections import defaultdict, OrderedDict from itertools import chain import numpy as np -from ..small_classes import Numbers from ..keydict import KeySet, KeyDict from ..small_scripts import try_str_without -from ..repr_conventions import GPkitObject +from ..repr_conventions import ReprMixin from .single_equation import SingleEquationConstraint @@ -14,11 +13,11 @@ def add_meq_bounds(bounded, meq_bounded): #TODO: collapse with GP version? still_alive = True while still_alive: still_alive = False # if no changes are made, the loop exits - for bound, conditions in list(meq_bounded.items()): - if bound in bounded: # bound exists in an inequality + for bound in list(meq_bounded): + if bound in bounded: # bound already exists del meq_bounded[bound] continue - for condition in conditions: + for condition in meq_bounded[bound]: if condition.issubset(bounded): # bound's condition is met del meq_bounded[bound] bounded.add(bound) @@ -29,80 +28,75 @@ def _sort_by_name_and_idx(var): "return tuple for Variable sorting" return (var.key.str_without(["units", "idx"]), var.key.idx or ()) -def _sort_constrs(item): +def _sort_constraints(item): "return tuple for Constraint sorting" label, constraint = item return (not isinstance(constraint, SingleEquationConstraint), - hasattr(constraint, "lineage") and bool(constraint.lineage), label) - - -class ConstraintSet(list, GPkitObject): + bool(getattr(constraint, "lineage", None)), label) + +def sort_constraints_dict(iterable): + "Sort a dictionary of {k: constraint} and return its keys and values" + if isinstance(iterable, OrderedDict): + return iterable.keys(), iterable.values() + items = sorted(list(iterable.items()), key=_sort_constraints) + return (item[0] for item in items), (item[1] for item in items) + +def flatiter(iterable, yield_if_hasattr=None): + "Yields contained constraints, optionally including constraintsets." + if isinstance(iterable, dict): + _, iterable = sort_constraints_dict(iterable) + for constraint in iterable: + if (not hasattr(constraint, "__iter__") + or (yield_if_hasattr + and hasattr(constraint, yield_if_hasattr))): + yield constraint + else: + try: # numpy array + yield from constraint.flat + except TypeError: # ConstrainSet + yield from constraint.flat(yield_if_hasattr) + except AttributeError: # probably a list or dict + yield from flatiter(constraint, yield_if_hasattr) + + +class ConstraintSet(list, ReprMixin): "Recursive container for ConstraintSets and Inequalities" unique_varkeys, idxlookup = frozenset(), {} - varkeys = _name_collision_varkeys = None + _name_collision_varkeys = None def __init__(self, constraints, substitutions=None): # pylint: disable=too-many-branches,too-many-statements - if isinstance(constraints, ConstraintSet): + if isinstance(constraints, dict): + keys, constraints = sort_constraints_dict(constraints) + self.idxlookup = {k: i for i, k in enumerate(keys)} + elif isinstance(constraints, ConstraintSet): constraints = [constraints] # put it one level down - elif isinstance(constraints, dict): - if isinstance(constraints, OrderedDict): - items = constraints.items() - else: - items = sorted(list(constraints.items()), key=_sort_constrs) - self.idxlookup = {k: i for i, (k, _) in enumerate(items)} - constraints = list(zip(*items))[1] list.__init__(self, constraints) - # initializations for attributes used elsewhere - self.numpy_bools = False - # get substitutions and convert all members to ConstraintSets self.varkeys = KeySet(self.unique_varkeys) self.substitutions = KeyDict({k: k.value for k in self.unique_varkeys if "value" in k.descr}) self.substitutions.varkeys = self.varkeys self.bounded, self.meq_bounded = set(), defaultdict(set) for i, constraint in enumerate(self): - if not isinstance(constraint, ConstraintSet): - if hasattr(constraint, "__iter__"): - list.__setitem__(self, i, ConstraintSet(constraint)) - elif not hasattr(constraint, "as_hmapslt1"): - if not isinstance(constraint, np.bool_): - raise_badelement(self, i, constraint) - else: # allow NomialArray equalities (arr == "a", etc.) - self.numpy_bools = True # but mark them so - elif not hasattr(constraint, "numpy_bools"): # we can catch them! - raise ValueError("a ConstraintSet of type %s was included in" - " another ConstraintSet before being" - " initialized." % type(constraint)) - elif constraint.numpy_bools: - raise_elementhasnumpybools(constraint) - if hasattr(self[i], "varkeys"): - self.varkeys.update(self[i].varkeys) - if hasattr(self[i], "substitutions"): - self.substitutions.update(self[i].substitutions) - else: - self.substitutions.update({k: k.value \ - for k in self[i].varkeys if "value" in k.descr}) - self.bounded.update(self[i].bounded) - for bound, solutionset in self[i].meq_bounded.items(): - self.meq_bounded[bound].update(solutionset) - if type(self[i]) is ConstraintSet: # pylint: disable=unidiomatic-typecheck - del self[i].varkeys - del self[i].substitutions - del self[i].bounded - del self[i].meq_bounded - # TODO, speedup: make constraintset more and more a list; - # don't turn every sub-element into its own dang set. - # keep a flattened list of those with hmapslt1, - # process_result, and Thats It. + if hasattr(constraint, "varkeys"): + self._update(constraint) + elif not hasattr(constraint, "as_hmapslt1"): + try: + for subconstraint in flatiter(constraint, "varkeys"): + self._update(subconstraint) + except Exception as e: + raise badelement(self, i, constraint) from e + elif isinstance(constraint, ConstraintSet): + raise badelement(self, i, constraint, + " It had not yet been initialized!") if substitutions: self.substitutions.update(substitutions) - updated_veckeys = False # vector subs need to find each indexed varkey for subkey in self.substitutions: - if not updated_veckeys and subkey.shape and not subkey.idx: + if subkey.shape and not subkey.idx: # vector sub found for key in self.varkeys: if key.veckey: self.varkeys.keymap[key.veckey].add(key) - updated_veckeys = True + break # vectorkeys need to be mapped only once + for subkey in self.substitutions: for key in self.varkeys[subkey]: self.bounded.add((key, "upper")) self.bounded.add((key, "lower")) @@ -112,6 +106,18 @@ def __init__(self, constraints, substitutions=None): # pylint: disable=too-many del key.veckey.descr["value"] add_meq_bounds(self.bounded, self.meq_bounded) + def _update(self, constraint): + "Update parameters with a given constraint" + self.varkeys.update(constraint.varkeys) + if hasattr(constraint, "substitutions"): + self.substitutions.update(constraint.substitutions) + else: + self.substitutions.update({k: k.value \ + for k in constraint.varkeys if "value" in k.descr}) + self.bounded.update(constraint.bounded) + for bound, solutionset in constraint.meq_bounded.items(): + self.meq_bounded[bound].update(solutionset) + def __getitem__(self, key): if key in self.idxlookup: key = self.idxlookup[key] @@ -145,23 +151,16 @@ def variables_byname(self, key): def constrained_varkeys(self): "Return all varkeys in non-ConstraintSet constraints" constrained_varkeys = set() - for constraint in self.flat(): + for constraint in self.flat(yield_if_hasattr="varkeys"): constrained_varkeys.update(constraint.varkeys) return constrained_varkeys - def flat(self): - "Yields contained constraints, optionally including constraintsets." - for constraint in self: - if isinstance(constraint, ConstraintSet): - yield from constraint.flat() - elif hasattr(constraint, "__iter__"): - yield from constraint - else: - yield constraint - - def flathmaps(self, subs): + flat = flatiter + + def as_hmapslt1(self, subs): "Yields hmaps<=1 from self.flat()" - yield from chain(*(l.as_hmapslt1(subs) for l in self.flat())) + yield from chain(*(l.as_hmapslt1(subs) + for l in self.flat(yield_if_hasattr="as_hmapslt1"))) def process_result(self, result): """Does arbitrary computation / manipulation of a program's result @@ -176,7 +175,7 @@ def process_result(self, result): - add values computed from solved variables """ - for constraint in self: + for constraint in self.flat(yield_if_hasattr="process_result"): if hasattr(constraint, "process_result"): constraint.process_result(result) for v in self.unique_varkeys: @@ -198,50 +197,29 @@ def __repr__(self): def name_collision_varkeys(self): "Returns the set of contained varkeys whose names are not unique" if self._name_collision_varkeys is None: - self._name_collision_varkeys = set() - for key in self.varkeys: - if len(self.varkeys[key.str_without(["lineage", "vec"])]) > 1: - self._name_collision_varkeys.add(key) + self._name_collision_varkeys = { + key for key in self.varkeys + if len(self.varkeys[key.str_without(["lineage", "vec"])]) > 1} return self._name_collision_varkeys def lines_without(self, excluded): "Lines representation of a ConstraintSet." - root = "root" not in excluded - rootlines, lines = [], [] - indent = " "*2 if (len(self) > 1 - or getattr(self, "lineage", None)) else "" + excluded = frozenset(excluded) + root, rootlines = "root" not in excluded, [] if root: - excluded += ("root",) + excluded = excluded.union(["root"]) if "unnecessary lineage" in excluded: for key in self.name_collision_varkeys(): key.descr["necessarylineage"] = True if hasattr(self, "_rootlines"): rootlines = self._rootlines(excluded) # pylint: disable=no-member - if self.idxlookup: - named_constraints = {v: k for k, v in self.idxlookup.items()} - for i, constraint in enumerate(self): - clines = try_str_without(constraint, excluded).split("\n") - if (getattr(constraint, "lineage", None) - and isinstance(constraint, ConstraintSet)): - name, num = constraint.lineage[-1] - if not any(clines): - clines = [indent + "(no constraints)"] - if lines: - lines.append("") - lines.append(name if not num else name + str(num)) - elif ("constraint names" not in excluded - and self.idxlookup and i in named_constraints): - lines.append("\"%s\":" % named_constraints[i]) - for j, line in enumerate(clines): - if clines[j][:len(indent)] != indent: - clines[j] = indent + line # must be indented - lines.extend(clines) - if root: - indent = " " - if "unnecessary lineage" in excluded: - for key in self.name_collision_varkeys(): - del key.descr["necessarylineage"] - return rootlines + [indent+line for line in lines] + lines = recursively_line(self, excluded) + indent = " " if getattr(self, "lineage", None) else "" + if root and "unnecessary lineage" in excluded: + indent += " " + for key in self.name_collision_varkeys(): + del key.descr["necessarylineage"] + return rootlines + [(indent+line).rstrip() for line in lines] def str_without(self, excluded=("unnecessary lineage", "units")): "String representation of a ConstraintSet." @@ -269,6 +247,38 @@ def as_view(self): "Return a ConstraintSetView of this ConstraintSet." return ConstraintSetView(self) +def recursively_line(iterable, excluded): + "Generates lines in a recursive tree-like fashion, the better to indent." + named_constraints = {} + if isinstance(iterable, dict): + keys, iterable = sort_constraints_dict(iterable) + named_constraints = dict(enumerate(keys)) + elif hasattr(iterable, "idxlookup"): + named_constraints = {i: k for k, i in iterable.idxlookup.items()} + lines = [] + for i, constraint in enumerate(iterable): + if hasattr(constraint, "lines_without"): + clines = constraint.lines_without(excluded) + elif not hasattr(constraint, "__iter__"): + clines = try_str_without(constraint, excluded).split("\n") + elif iterable is constraint: + clines = ["(constraint contained itself)"] + else: + clines = recursively_line(constraint, excluded) + if (getattr(constraint, "lineage", None) + and isinstance(constraint, ConstraintSet)): + name, num = constraint.lineage[-1] + if not any(clines): + clines = [" " + "(no constraints)"] # named model indent + if lines: + lines.append("") + lines.append(name if not num else name + str(num)) + elif "constraint names" not in excluded and i in named_constraints: + lines.append("\"%s\":" % named_constraints[i]) + clines = [" " + line for line in clines] # named constraint indent + lines.extend(clines) + return lines + class ConstraintSetView: "Class to access particular views on a set's variables" @@ -312,31 +322,17 @@ def __getattr__(self, attr): -def raise_badelement(cns, i, constraint): +def badelement(cns, i, constraint, cause=""): "Identify the bad element and raise a ValueError" - cause = "" if not isinstance(constraint, bool) else ( + cause = cause if not isinstance(constraint, bool) else ( " Did the constraint list contain an accidental equality?") if len(cns) == 1: - loc = "as the only constraint" + loc = "the only constraint" elif i == 0: loc = "at the start, before %s" % cns[i+1] elif i == len(cns) - 1: loc = "at the end, after %s" % cns[i-1] else: loc = "between %s and %s" % (cns[i-1], cns[i+1]) - raise ValueError("%s was found %s.%s" - % (type(constraint), loc, cause)) - - -def raise_elementhasnumpybools(constraint): - "Identify the bad subconstraint array and raise a ValueError" - cause = ("An constraint was created containing numpy.bools.") - for side in [constraint.left, constraint.right]: - if not (isinstance(side, Numbers) - or hasattr(side, "hmap") - or hasattr(side, "__iter__")): - cause += (" NomialArray comparison with %.10s %s" - " does not return a valid constraint." - % (repr(side), type(side))) - raise ValueError("%s\nFull constraint: %s" - % (cause, constraint)) + return ValueError("Invalid ConstraintSet element '%s' %s was %s.%s" + % (repr(constraint), type(constraint), loc, cause)) diff --git a/gpkit/constraints/sgp.py b/gpkit/constraints/sgp.py index 5d2b813ac..4ac511d47 100644 --- a/gpkit/constraints/sgp.py +++ b/gpkit/constraints/sgp.py @@ -95,7 +95,7 @@ def __init__(self, cost, model, substitutions, *, self._gp = self.init_gp(**initgpargs) self.blackboxconstraints = False return - except AttributeError: + except AttributeError: # TODO: this is too broad of a catch pass # some constraint lacked self.blackboxconstraints = True self.__bare_init__(cost, model, substitutions) @@ -236,7 +236,7 @@ def results(self): def _fill_x0(self, x0): "Returns a copy of x0 with subsitutions added." x0kd = KeyDict() - x0kd.varkeys = self.varkeys + x0kd.varkeys = self.model.varkeys if x0: x0kd.update(x0) # has to occur after the setting of varkeys x0kd.update(self.substitutions) diff --git a/gpkit/constraints/single_equation.py b/gpkit/constraints/single_equation.py index 39b20a877..a230b77bf 100644 --- a/gpkit/constraints/single_equation.py +++ b/gpkit/constraints/single_equation.py @@ -1,10 +1,10 @@ "Implements SingleEquationConstraint" from operator import le, ge, eq from ..small_scripts import try_str_without -from ..repr_conventions import GPkitObject +from ..repr_conventions import ReprMixin -class SingleEquationConstraint(GPkitObject): +class SingleEquationConstraint(ReprMixin): "Constraint expressible in a single equation." latex_opers = {"<=": "\\leq", ">=": "\\geq", "=": "="} func_opers = {"<=": le, ">=": ge, "=": eq} @@ -14,9 +14,13 @@ def __init__(self, left, oper, right): def str_without(self, excluded=("units")): "String representation without attributes in excluded list" - return "%s %s %s" % (try_str_without(self.left, excluded), - self.oper, - try_str_without(self.right, excluded)) + leftstr = try_str_without(self.left, excluded) + rightstr = try_str_without(self.right, excluded) + rlines = rightstr.split("\n") + if len(rlines) > 1: + indent = len("%s %s " % (leftstr.split("\n")[-1], self.oper)) + rightstr = ("\n" + " "*indent).join(rlines) + return "%s %s %s" % (leftstr, self.oper, rightstr) def latex(self, excluded=("units")): "Latex representation without attributes in excluded list" diff --git a/gpkit/nomials/array.py b/gpkit/nomials/array.py index e9809d2e8..d7ff280f7 100644 --- a/gpkit/nomials/array.py +++ b/gpkit/nomials/array.py @@ -1,4 +1,3 @@ -# -*coding: utf-8 -*- """Module for creating NomialArray instances. Example @@ -14,7 +13,7 @@ from ..small_classes import Numbers, HashVector, EMPTY_HV from ..small_scripts import try_str_without, mag from ..constraints import ArrayConstraint -from ..repr_conventions import GPkitObject +from ..repr_conventions import ReprMixin from ..exceptions import DimensionalityError @np.vectorize @@ -37,7 +36,7 @@ def wrapped_func(self, other): return wrapped_func -class NomialArray(GPkitObject, np.ndarray): +class NomialArray(ReprMixin, np.ndarray): """A Numpy array with elementwise inequalities and substitutions. Arguments @@ -97,7 +96,7 @@ def str_without(self, excluded=()): if self.ast: return self.parse_ast(excluded) if hasattr(self, "key"): - return self.key.str_without(excluded) + return self.key.str_without(excluded) # pylint: disable=no-member if not self.shape: return try_str_without(self.flatten()[0], excluded) @@ -109,7 +108,7 @@ def latex(self, excluded=()): "Returns latex representation without certain fields." units = self.latex_unitstr() if "units" not in excluded else "" if hasattr(self, "key"): - return self.key.latex(excluded) + units + return self.key.latex(excluded) + units # pylint: disable=no-member return np.ndarray.__str__(self) def __hash__(self): diff --git a/gpkit/nomials/data.py b/gpkit/nomials/data.py index 14b26fa5f..d0e7f3ab0 100644 --- a/gpkit/nomials/data.py +++ b/gpkit/nomials/data.py @@ -1,11 +1,11 @@ """Machinery for exps, cs, varlocs data -- common to nomials and programs""" import numpy as np from ..keydict import KeySet -from ..repr_conventions import GPkitObject +from ..repr_conventions import ReprMixin from ..varkey import VarKey -class NomialData(GPkitObject): +class NomialData(ReprMixin): """Object for holding cs, exps, and other basic 'nomial' properties. cs: array (coefficient of each monomial term) diff --git a/gpkit/repr_conventions.py b/gpkit/repr_conventions.py index 33a272d44..93abd3cee 100644 --- a/gpkit/repr_conventions.py +++ b/gpkit/repr_conventions.py @@ -80,7 +80,7 @@ def parenthesize(string, addi=True, mult=True): return string -class GPkitObject: +class ReprMixin: "This class combines various printing methods for easier adoption." lineagestr = lineagestr unitstr = unitstr diff --git a/gpkit/solution_array.py b/gpkit/solution_array.py index 07059c14e..24fd794d9 100644 --- a/gpkit/solution_array.py +++ b/gpkit/solution_array.py @@ -688,19 +688,20 @@ def var_table(data, title, printunits=True, latex=False, rawlines=False, return [] decorated, models = [], set() for i, (k, v) in enumerate(data.items()): - if np.nanmax(np.abs(v)) > minval: - if minval and hidebelowminval and getattr(v, "shape", None): - v[np.abs(v) <= minval] = np.nan - model = lineagestr(k.lineage) if sortbymodel else "" - models.add(model) - b = bool(getattr(v, "shape", None)) - s = k.str_without(("lineage", "vec")) - if not sortbyvals: - decorated.append((model, b, (varfmt % s), i, k, v)) - else: # for consistent sorting, add small offset to negative vals - val = np.nanmean(np.abs(v)) - (1e-9 if np.nanmean(v) < 0 else 0) - sort = (float("%.4g" % -val), k.name) - decorated.append((model, sort, b, (varfmt % s), i, k, v)) + if np.isnan(v).all() or np.nanmax(np.abs(v)) <= minval: + continue # no values below minval + if minval and hidebelowminval and getattr(v, "shape", None): + v[np.abs(v) <= minval] = np.nan + model = lineagestr(k.lineage) if sortbymodel else "" + models.add(model) + b = bool(getattr(v, "shape", None)) + s = k.str_without(("lineage", "vec")) + if not sortbyvals: + decorated.append((model, b, (varfmt % s), i, k, v)) + else: # for consistent sorting, add small offset to negative vals + val = np.nanmean(np.abs(v)) - (1e-9 if np.nanmean(v) < 0 else 0) + sort = (float("%.4g" % -val), k.name) + decorated.append((model, sort, b, (varfmt % s), i, k, v)) if not decorated and skipifempty: return [] if included_models: diff --git a/gpkit/tests/t_model.py b/gpkit/tests/t_model.py index dc311ebd5..1cf4a1201 100644 --- a/gpkit/tests/t_model.py +++ b/gpkit/tests/t_model.py @@ -600,7 +600,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.flathmaps({}))[0] + first_gp_constr_posy_exp, = list(gp.as_hmapslt1({}))[0] self.assertEqual(first_gp_constr_posy_exp[x.key], -1./3) def test_becomes_signomial(self): diff --git a/gpkit/tests/t_nomial_array.py b/gpkit/tests/t_nomial_array.py index 5ab08248b..874c8938d 100644 --- a/gpkit/tests/t_nomial_array.py +++ b/gpkit/tests/t_nomial_array.py @@ -2,6 +2,7 @@ import unittest import numpy as np from gpkit import Variable, Posynomial, NomialArray, VectorVariable, Monomial +from gpkit.constraints.set import ConstraintSet import gpkit @@ -66,7 +67,8 @@ def test_constraint_gen(self): x_2 = Variable('x', idx=(2,), shape=(3,), label='dummy variable') v = NomialArray([1, 2, 3]).T p = [x_0, x_1/2, x_2/3] - self.assertEqual(list((x <= v).flathmaps({})), [e.hmap for e in p]) + constraint = ConstraintSet([x <= v]) + self.assertEqual(list(constraint.as_hmapslt1({})), [e.hmap for e in p]) def test_substition(self): # pylint: disable=no-member x = VectorVariable(3, 'x', label='dummy variable') diff --git a/gpkit/varkey.py b/gpkit/varkey.py index f8e4f5c09..a5bbd83cb 100644 --- a/gpkit/varkey.py +++ b/gpkit/varkey.py @@ -1,9 +1,9 @@ """Defines the VarKey class""" from .small_classes import HashVector, Count, qty -from .repr_conventions import GPkitObject +from .repr_conventions import ReprMixin -class VarKey(GPkitObject): # pylint:disable=too-many-instance-attributes +class VarKey(ReprMixin): # pylint:disable=too-many-instance-attributes """An object to correspond to each 'variable name'. Arguments