diff --git a/LICENSE b/LICENSE index 3d36bf819..8650cc1e3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2018 Edward Burnell +Copyright (c) 2019 Edward Burnell Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MANIFEST b/MANIFEST index d4b241aa2..70c5b78a3 100644 --- a/MANIFEST +++ b/MANIFEST @@ -1,33 +1,27 @@ -gpkit/__init__.py -gpkit/_cvxopt.py gpkit/_mosek/__init__.py gpkit/_mosek/cli_expopt.py gpkit/_mosek/expopt.py -gpkit/build.py +gpkit/_pint/__init__.py +gpkit/_pint/usd_cpi.txt gpkit/constraints/__init__.py gpkit/constraints/array.py gpkit/constraints/bounded.py gpkit/constraints/costed.py gpkit/constraints/gp.py -gpkit/constraints/model.py gpkit/constraints/loose.py +gpkit/constraints/model.py gpkit/constraints/prog_factories.py gpkit/constraints/relax.py gpkit/constraints/set.py -gpkit/constraints/sigeq.py gpkit/constraints/sgp.py +gpkit/constraints/sigeq.py gpkit/constraints/single_equation.py gpkit/constraints/tight.py -gpkit/exceptions.py gpkit/interactive/__init__.py -gpkit/interactive/chartjs.py -gpkit/interactive/sankey.py -gpkit/interactive/plotting.py gpkit/interactive/plot_sweep.py -gpkit/interactive/ractor.py +gpkit/interactive/plotting.py +gpkit/interactive/sankey.py gpkit/interactive/widgets.py -gpkit/keydict.py -gpkit/modified_ctypesgen.py gpkit/nomials/__init__.py gpkit/nomials/array.py gpkit/nomials/data.py @@ -36,13 +30,6 @@ gpkit/nomials/math.py gpkit/nomials/map.py gpkit/nomials/substitution.py gpkit/nomials/variables.py -gpkit/_pint/__init__.py -gpkit/_pint/usd_cpi.txt -gpkit/globals.py -gpkit/repr_conventions.py -gpkit/small_classes.py -gpkit/small_scripts.py -gpkit/solution_array.py gpkit/tests/__init__.py gpkit/tests/from_paths.py gpkit/tests/helpers.py @@ -62,7 +49,17 @@ gpkit/tests/test_repo.py gpkit/tools/__init__.py gpkit/tools/autosweep.py gpkit/tools/docstring.py -gpkit/tools/spdata.py gpkit/tools/tools.py +gpkit/__init__.py +gpkit/_cvxopt.py +gpkit/build.py +gpkit/exceptions.py +gpkit/globals.py +gpkit/keydict.py +gpkit/modified_ctypesgen.py +gpkit/repr_conventions.py +gpkit/small_classes.py +gpkit/small_scripts.py +gpkit/solution_array.py gpkit/varkey.py setup.py diff --git a/checkpy3.sh b/checkpy3.sh index 676ab63f7..8daf27a2d 100755 --- a/checkpy3.sh +++ b/checkpy3.sh @@ -1,5 +1,7 @@ cp gpkit/env/settings . -sed -i '1s/.*/installed_solvers : mosek_cli/' gpkit/env/settings +sed -i '1s/.*/installed_solvers : mosek, cvxopt/' gpkit/env/settings cat gpkit/env/settings -python3 docs/source/examples/simpleflight.py +python3 gpkit/tests/run_tests.py mv settings gpkit/env +rm *.pkl +rm solution.* diff --git a/docs/source/citinggpkit.rst b/docs/source/citinggpkit.rst index 9cb8ca33f..72d21a79c 100644 --- a/docs/source/citinggpkit.rst +++ b/docs/source/citinggpkit.rst @@ -7,6 +7,6 @@ If you use GPkit, please cite it with the following bibtex:: author={Edward Burnell and Warren Hoburg}, title={GPkit software for geometric programming}, howpublished={\url{https://github.com/convexengineering/gpkit}}, - year={2018}, - note={Version 0.8.0} + year={2014}, + note={Version 0.9.0} } diff --git a/docs/source/conf.py b/docs/source/conf.py index 980fe3df3..f6069fccc 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -48,16 +48,16 @@ # General information about the project. project = u'gpkit' -copyright = u'2018 Edward Burnell' +copyright = u'2019 Edward Burnell' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '0.8' +version = '0.9' # The full version, including alpha/beta/rc tags. -release = '0.8.0' +release = '0.9.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/source/examples/autosweep.py b/docs/source/examples/autosweep.py index 184bea7cd..27b2a64d3 100644 --- a/docs/source/examples/autosweep.py +++ b/docs/source/examples/autosweep.py @@ -1,5 +1,5 @@ "Show autosweep_1d functionality" -import cPickle as pickle +import pickle import numpy as np import gpkit from gpkit import units, Variable, Model @@ -12,17 +12,17 @@ m1 = Model(A**2, [A >= l**2 + units.m**2]) tol1 = 1e-3 bst1 = autosweep_1d(m1, tol1, l, [1, 10], verbosity=0) -print "Solved after %2i passes, cost logtol +/-%.3g" % (bst1.nsols, bst1.tol) +print("Solved after %2i passes, cost logtol +/-%.3g" % (bst1.nsols, bst1.tol)) # autosweep solution accessing l_vals = np.linspace(1, 10, 10) sol1 = bst1.sample_at(l_vals) -print "values of l:", l_vals -print "values of A:", sol1("A") +print("values of l: %s" % l_vals) +print("values of A: %s" % sol1("A")) cost_estimate = sol1["cost"] cost_lb, cost_ub = sol1.cost_lb(), sol1.cost_ub() -print "cost lower bound:", cost_lb -print "cost estimate: ", cost_estimate -print "cost upper bound:", cost_ub +print("cost lower bound: %s" % cost_lb) +print("cost estimate: %s" % cost_estimate) +print("cost upper bound: %s" % cost_ub) # you can evaluate arbitrary posynomials np.testing.assert_allclose(mag(2*sol1(A)), mag(sol1(2*A))) assert (sol1["cost"] == sol1(A**2)).all() @@ -31,7 +31,7 @@ np.log(mag(cost_estimate))) # save autosweep to a file and retrieve it bst1.save("autosweep.pkl") -bst1_loaded = pickle.load(open("autosweep.pkl")) +bst1_loaded = pickle.load(open("autosweep.pkl", "rb")) # this problem is two intersecting lines in logspace m2 = Model(A**2, [A >= (l/3)**2, A >= (l/3)**0.5 * units.m**1.5]) @@ -40,6 +40,6 @@ # test Model method sol2 = m2.autosweep({l: [1, 10]}, tol2, verbosity=0) bst2 = sol2.bst -print "Solved after %2i passes, cost logtol +/-%.3g" % (bst2.nsols, bst2.tol) -print "Table of solutions used in the autosweep:" -print bst2.solarray.table() +print("Solved after %2i passes, cost logtol +/-%.3g" % (bst2.nsols, bst2.tol)) +print("Table of solutions used in the autosweep:") +print(bst2.solarray.table()) diff --git a/docs/source/examples/beam.py b/docs/source/examples/beam.py index fcff43507..919377cac 100644 --- a/docs/source/examples/beam.py +++ b/docs/source/examples/beam.py @@ -40,7 +40,7 @@ class Beam(Model): """ def setup(self, N=4): - exec parse_variables(self.__doc__) + exec(parse_variables(self.__doc__)) # minimize tip displacement (the last w) self.cost = self.w_tip = w[-1] return { @@ -68,7 +68,7 @@ def setup(self, N=4): b = Beam(N=6, substitutions={"L": 6, "EI": 1.1e4, "q": 110*np.ones(6)}) sol = b.solve(verbosity=0) -print sol.summary(maxcolumns=6) +print(sol.summary(maxcolumns=6)) w_gp = sol("w") # deflection along beam L, EI, q = sol("L"), sol("EI"), sol("q") diff --git a/docs/source/examples/boundschecking.py b/docs/source/examples/boundschecking.py index f81f52b89..a667a0eb7 100644 --- a/docs/source/examples/boundschecking.py +++ b/docs/source/examples/boundschecking.py @@ -29,7 +29,7 @@ class BoundsChecking(Model): """ def setup(self): - exec parse_variables(BoundsChecking.__doc__) + exec(parse_variables(BoundsChecking.__doc__)) self.cost = F return [ F >= D + T, @@ -43,7 +43,7 @@ def setup(self): m = BoundsChecking() -print m.str_without(["lineage"]) +print(m.str_without(["lineage"])) try: m.solve() except ValueError: diff --git a/docs/source/examples/debug.py b/docs/source/examples/debug.py index 9fbb97e0b..5c8a51e29 100644 --- a/docs/source/examples/debug.py +++ b/docs/source/examples/debug.py @@ -1,5 +1,4 @@ "Debug examples" - from gpkit import Variable, Model, units x = Variable("x", "ft") @@ -10,11 +9,11 @@ m = Model(x/y, [x <= x_max, x >= x_min]) m.debug() -print "# Now let's try a model unsolvable with relaxed constants\n" +print("# Now let's try a model unsolvable with relaxed constants\n") Model(x, [x <= units("inch"), x >= units("yard")]).debug() -print "# And one that's only unbounded\n" +print("# And one that's only unbounded\n") # the value of x_min was used up in the previous model! x_min = Variable("x_min", 2, "ft") diff --git a/docs/source/examples/docstringparsing.py b/docs/source/examples/docstringparsing.py index 3acedb3dc..544757971 100644 --- a/docs/source/examples/docstringparsing.py +++ b/docs/source/examples/docstringparsing.py @@ -34,14 +34,14 @@ class Cube(Model): way that makes the most sense to someone else reading your model. """ def setup(self): - exec parse_variables(Cube.__doc__) + exec(parse_variables(Cube.__doc__)) return [A >= 2*(s[0]*s[1] + s[1]*s[2] + s[2]*s[0]), s.prod() >= V, s[2] >= h] -print parse_variables(Cube.__doc__) +print(parse_variables(Cube.__doc__)) c = Cube() c.cost = c.A -print c.solve(verbosity=0).table() +print(c.solve(verbosity=0).table()) diff --git a/docs/source/examples/docstringparsing_output.txt b/docs/source/examples/docstringparsing_output.txt index 22f66425c..ee401f53b 100644 --- a/docs/source/examples/docstringparsing_output.txt +++ b/docs/source/examples/docstringparsing_output.txt @@ -2,25 +2,25 @@ from gpkit import Variable, VectorVariable try: A = self.A = Variable('A', 'm^2', 'surface area') -except Exception, e: +except Exception as e: raise ValueError("`"+e.__class__.__name__+": "+str(e)+"` was raised" " while executing the parsed line `A = self.A = Variable('A', 'm^2', 'surface area')`. Is this line following the format `Name (optional Value) [Units] (Optional Description)` without any whitespace in the Name or Value fields?") try: V = self.V = Variable('V', 100, 'L', 'minimum volume') -except Exception, e: +except Exception as e: raise ValueError("`"+e.__class__.__name__+": "+str(e)+"` was raised" " while executing the parsed line `V = self.V = Variable('V', 100, 'L', 'minimum volume')`. Is this line following the format `Name (optional Value) [Units] (Optional Description)` without any whitespace in the Name or Value fields?") try: h = self.h = Variable('h', 1, 'm', 'minimum height') -except Exception, e: +except Exception as e: raise ValueError("`"+e.__class__.__name__+": "+str(e)+"` was raised" " while executing the parsed line `h = self.h = Variable('h', 1, 'm', 'minimum height')`. Is this line following the format `Name (optional Value) [Units] (Optional Description)` without any whitespace in the Name or Value fields?") try: s = self.s = VectorVariable(3, 's', 'm', 'side length') -except Exception, e: +except Exception as e: raise ValueError("`"+e.__class__.__name__+": "+str(e)+"` was raised" " while executing the parsed line `s = self.s = VectorVariable(3, 's', 'm', 'side length')`. Is this line following the format `Name (optional Value) [Units] (Optional Description)` without any whitespace in the Name or Value fields?") diff --git a/docs/source/examples/external_sp.py b/docs/source/examples/external_sp.py index 70b2c7019..3c3d0c34c 100644 --- a/docs/source/examples/external_sp.py +++ b/docs/source/examples/external_sp.py @@ -1,5 +1,4 @@ "Can be found in gpkit/docs/source/examples/external_sp.py" - import numpy as np from gpkit import Variable, Model from external_constraint import ExternalConstraint @@ -15,4 +14,4 @@ ] m = Model(objective, constraints) -print m.localsolve(verbosity=0).summary() +print(m.localsolve(verbosity=0).summary()) diff --git a/docs/source/examples/external_sp2.py b/docs/source/examples/external_sp2.py index e9b4ab936..faf090ae0 100644 --- a/docs/source/examples/external_sp2.py +++ b/docs/source/examples/external_sp2.py @@ -14,4 +14,4 @@ def y_ext(self, x0): y = Variable("y", externalfn=y_ext) m = Model(y, [np.pi/4 <= x, x <= np.pi/2]) -print m.localsolve(verbosity=0).summary() +print(m.localsolve(verbosity=0).summary()) diff --git a/docs/source/examples/model_var_access.py b/docs/source/examples/model_var_access.py index 6992360c9..e2ff979ce 100644 --- a/docs/source/examples/model_var_access.py +++ b/docs/source/examples/model_var_access.py @@ -63,6 +63,6 @@ def setup(self): m >= sum(comp.m for comp in components)] PS = PowerSystem() -print "Getting the only var 'E': ", PS["E"] -print "The top-level var 'm': ", PS.m -print "All the variables 'm': ", PS.variables_byname("m") +print("Getting the only var 'E': %s" % PS["E"]) +print("The top-level var 'm': %s" % PS.m) +print("All the variables 'm': %s" % PS.variables_byname("m")) diff --git a/docs/source/examples/model_var_access_output.txt b/docs/source/examples/model_var_access_output.txt index b48f278b2..e5a58558d 100644 --- a/docs/source/examples/model_var_access_output.txt +++ b/docs/source/examples/model_var_access_output.txt @@ -1,3 +1,3 @@ -Getting the only var 'E': E_PowerSystem/Battery [MJ] -The top-level var 'm': m_PowerSystem [lb] -All the variables 'm': [gpkit.Variable(m_PowerSystem [lb]), gpkit.Variable(m_PowerSystem/Battery [lb]), gpkit.Variable(m_PowerSystem/Motor [lb])] +Getting the only var 'E': E_PowerSystem/Battery [MJ] +The top-level var 'm': m_PowerSystem [lb] +All the variables 'm': [gpkit.Variable(m_PowerSystem [lb]), gpkit.Variable(m_PowerSystem/Battery [lb]), gpkit.Variable(m_PowerSystem/Motor [lb])] diff --git a/docs/source/examples/performance_modeling.py b/docs/source/examples/performance_modeling.py index 111e404d9..ac873d70a 100644 --- a/docs/source/examples/performance_modeling.py +++ b/docs/source/examples/performance_modeling.py @@ -1,5 +1,5 @@ """Modular aircraft concept""" -import cPickle as pickle +import pickle import numpy as np from gpkit import Model, Vectorize, parse_variables @@ -24,7 +24,7 @@ class AircraftP(Model): def setup(self, aircraft, state): self.aircraft = aircraft self.state = state - exec parse_variables(AircraftP.__doc__) + exec(parse_variables(AircraftP.__doc__)) self.wing_aero = aircraft.wing.dynamic(aircraft.wing, state) self.perf_models = [self.wing_aero] @@ -63,7 +63,7 @@ class Aircraft(Model): wing.c, wing.S """ def setup(self): - exec parse_variables(Aircraft.__doc__) + exec(parse_variables(Aircraft.__doc__)) self.fuse = Fuselage() self.wing = Wing() self.components = [self.fuse, self.wing] @@ -88,7 +88,7 @@ class FlightState(Model): """ def setup(self): - exec parse_variables(FlightState.__doc__) + exec(parse_variables(FlightState.__doc__)) class FlightSegment(Model): @@ -168,7 +168,7 @@ class WingAero(Model): def setup(self, wing, state): self.wing = wing self.state = state - exec parse_variables(WingAero.__doc__) + exec(parse_variables(WingAero.__doc__)) c = wing.c A = wing.A @@ -206,7 +206,7 @@ class Wing(Model): c, S """ def setup(self): - exec parse_variables(Wing.__doc__) + exec(parse_variables(Wing.__doc__)) return {"parametrization of wing weight": W >= S*rho, "definition of mean chord": @@ -226,7 +226,7 @@ class Fuselage(Model): """ def setup(self): - exec parse_variables(Fuselage.__doc__) + exec(parse_variables(Fuselage.__doc__)) AC = Aircraft() @@ -247,8 +247,8 @@ def setup(self): is MISSION.fs.aircraftp) vars_of_interest.update(MISSION.fs.aircraftp.unique_varkeys) vars_of_interest.add(M["D"]) -print sol.summary(vars_of_interest) -print sol.table(tables=["loose constraints"]) +print(sol.summary(vars_of_interest)) +print(sol.table(tables=["loose constraints"])) MISSION["flight segment"]["aircraft performance"]["fuel burn rate"] = ( MISSION.fs.aircraftp.Wburn >= 0.2*MISSION.fs.aircraftp.wing_aero.D) diff --git a/docs/source/examples/relaxation.py b/docs/source/examples/relaxation.py index b35eab320..9c94296ca 100644 --- a/docs/source/examples/relaxation.py +++ b/docs/source/examples/relaxation.py @@ -5,39 +5,39 @@ x_min = Variable("x_min", 2) x_max = Variable("x_max", 1) m = Model(x, [x <= x_max, x >= x_min]) -print "Original model" -print "==============" -print m -print +print("Original model") +print("==============") +print(m) +print("") # m.solve() # raises a RuntimeWarning! -print "With constraints relaxed equally" -print "================================" +print("With constraints relaxed equally") +print("================================") from gpkit.constraints.relax import ConstraintsRelaxedEqually allrelaxed = ConstraintsRelaxedEqually(m) mr1 = Model(allrelaxed.relaxvar, allrelaxed) -print mr1 -print mr1.solve(verbosity=0).table() # solves with an x of 1.414 -print +print(mr1) +print(mr1.solve(verbosity=0).table()) # solves with an x of 1.414 +print("") -print "With constraints relaxed individually" -print "=====================================" +print("With constraints relaxed individually") +print("=====================================") from gpkit.constraints.relax import ConstraintsRelaxed constraintsrelaxed = ConstraintsRelaxed(m) mr2 = Model(constraintsrelaxed.relaxvars.prod() * m.cost**0.01, # add a bit of the original cost in constraintsrelaxed) -print mr2 -print mr2.solve(verbosity=0).table() # solves with an x of 1.0 -print +print(mr2) +print(mr2.solve(verbosity=0).table()) # solves with an x of 1.0 +print("") -print "With constants relaxed individually" -print "===================================" +print("With constants relaxed individually") +print("===================================") from gpkit.constraints.relax import ConstantsRelaxed constantsrelaxed = ConstantsRelaxed(m) mr3 = Model(constantsrelaxed.relaxvars.prod() * m.cost**0.01, # add a bit of the original cost in constantsrelaxed) -print mr3 -print mr3.solve(verbosity=0).table() # brings x_min down to 1.0 -print +print(mr3) +print(mr3.solve(verbosity=0).table()) # brings x_min down to 1.0 +print("") diff --git a/docs/source/examples/simple_box.py b/docs/source/examples/simple_box.py index cff261993..b43ef12d5 100644 --- a/docs/source/examples/simple_box.py +++ b/docs/source/examples/simple_box.py @@ -30,4 +30,4 @@ m = Model(objective, constraints) # Solve the Model and print the results table -print m.solve(verbosity=0).table() +print(m.solve(verbosity=0).table()) diff --git a/docs/source/examples/simple_sp.py b/docs/source/examples/simple_sp.py index 0f77968d3..82371535c 100644 --- a/docs/source/examples/simple_sp.py +++ b/docs/source/examples/simple_sp.py @@ -11,9 +11,9 @@ # create and solve the SP m = gpkit.Model(x, constraints) -print m.localsolve(verbosity=0).summary() +print(m.localsolve(verbosity=0).summary()) assert abs(m.solution(x) - 0.9) < 1e-6 # full interim solutions are available -print "x values of each GP solve (note convergence)" -print ", ".join("%.5f" % sol["freevariables"][x] for sol in m.program.results) +print("x values of each GP solve (note convergence)") +print(", ".join("%.5f" % sol["freevariables"][x] for sol in m.program.results)) diff --git a/docs/source/examples/simpleflight.py b/docs/source/examples/simpleflight.py index 8b024339f..83956db8d 100644 --- a/docs/source/examples/simpleflight.py +++ b/docs/source/examples/simpleflight.py @@ -1,5 +1,5 @@ "Minimizes airplane drag for a simple drag and structure model." -import cPickle as pickle +import pickle import numpy as np from gpkit import Variable, Model pi = np.pi @@ -70,5 +70,5 @@ m.substitutions.update(sweeps) sweepsol = m.solve(verbosity=0) print(sweepsol.summary()) -sol_loaded = pickle.load(open("solution.pkl")) +sol_loaded = pickle.load(open("solution.pkl", "rb")) print(sweepsol.diff(sol_loaded)) diff --git a/docs/source/examples/sin_approx_example.py b/docs/source/examples/sin_approx_example.py index ce357a51e..797a8a78b 100644 --- a/docs/source/examples/sin_approx_example.py +++ b/docs/source/examples/sin_approx_example.py @@ -14,4 +14,4 @@ ] m = Model(objective, constraints) -print m.solve(verbosity=0).summary() +print(m.solve(verbosity=0).summary()) diff --git a/docs/source/examples/sp_to_gp_sweep.py b/docs/source/examples/sp_to_gp_sweep.py index 18416582f..ba8ab2ad9 100644 --- a/docs/source/examples/sp_to_gp_sweep.py +++ b/docs/source/examples/sp_to_gp_sweep.py @@ -104,4 +104,4 @@ def SimPleAC(): sa.substitutions.update({"V_f_wing": ("sweep", np.linspace(0.1, 0.5, 3)), "V_f_fuse": 0.5}) sol = sa.solve(verbosity=0) -print sol.summary() +print(sol.summary()) diff --git a/docs/source/examples/unbounded.py b/docs/source/examples/unbounded.py index 867ba5397..87fb65042 100644 --- a/docs/source/examples/unbounded.py +++ b/docs/source/examples/unbounded.py @@ -10,5 +10,5 @@ m = Model(1/x, Bounded(constraints)) # by default, prints bounds warning during solve sol = m.solve(verbosity=0) -print sol.summary() -print "sol['boundedness'] is:", sol["boundedness"] +print(sol.summary()) +print("sol['boundedness'] is: %s" % sol["boundedness"]) diff --git a/docs/source/examples/vectorize.py b/docs/source/examples/vectorize.py index dfabb0278..8070642ae 100644 --- a/docs/source/examples/vectorize.py +++ b/docs/source/examples/vectorize.py @@ -12,15 +12,15 @@ def setup(self): x = self.x = Variable("x") return [x >= 1] -print "SCALAR" +print("SCALAR") m = Test() m.cost = m["x"] -print m.solve(verbosity=0).summary() +print(m.solve(verbosity=0).summary()) -print "__________\n" -print "VECTORIZED" +print("__________\n") +print("VECTORIZED") with Vectorize(3): m = Test() m.cost = m["x"].prod() m.append(m["x"][1] >= 2) -print m.solve(verbosity=0).summary() +print(m.solve(verbosity=0).summary()) diff --git a/docs/source/examples/water_tank.py b/docs/source/examples/water_tank.py index a8a4a8e39..cab9a0890 100644 --- a/docs/source/examples/water_tank.py +++ b/docs/source/examples/water_tank.py @@ -16,4 +16,4 @@ m = Model(A, constraints) sol = m.solve(verbosity=0) -print sol.summary() +print(sol.summary()) diff --git a/docs/source/gpkit101.rst b/docs/source/gpkit101.rst deleted file mode 100644 index 8051c135f..000000000 --- a/docs/source/gpkit101.rst +++ /dev/null @@ -1,40 +0,0 @@ -GPkit Overview -************** - -GPkit is a Python package for defining and manipulating -geometric programming (GP) models, -abstracting away the backend solver. - -Our hopes are to bring the mathematics of Geometric Programming -into the engineering design process -in a disciplined and collaborative way, and to -encourage research with and on GPs by providing an -easily extensible object-oriented framework. - - -Symbolic expressions -==================== - -GPkit is a limited symbolic algebra language, allowing only for the creation of geometric program compatible equations (or signomial program compatible ones, if signomial programming is enabled). As mentioned in :ref:`geometricprogramming`, one can view monomials as posynomials with a single term, and posynomials as signomials that have only positive coefficients. The inheritance structure of these objects in GPkit follows this mathematical basis. - -.. figure:: inheritance.png - :width: 500 px - - -Substitution -============ - -The ``Varkey`` object in the graph above is not a algebraic expression, but what GPkit uses as a variable's "name". It carries the LaTeX representation of a variable and its units, as well as any other information the user wishes to associate with a variable. The use of ``VarKeys`` as opposed to numeric indexing is an important part of the GPkit framework, because it allows a user to keep variable information local and modular. - -GPkit keeps its internal representation of objects entirely symbolic until it solves. This means that any expression or Model object can replace any instance of a variable (as represented by a VarKey) with a number, new VarKey, or even an entire Monomial at any time with the ``.sub()`` method. - - -Model objects -============= - -In GPkit, a ``Model`` object represents a symbolic problem declaration. -That problem may be either GP-compatible or SP-compatible. -To avoid confusion, calling the ``solve()`` method on a model will either attempt to solve it for a global optimum (if it's a GP) or return an error immediately (if it's an SP). Similarly, calling ``localsolve()`` will either start the process of SP-solving (stepping through a sequence of GP-approximations) or return an error for GP-compatible Models. This framework is illustrated below. - -.. figure:: solvemethods.png - :width: 500 px diff --git a/docs/source/index.rst b/docs/source/index.rst index b8208c2df..8fbd636eb 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -24,7 +24,6 @@ Table of contents :maxdepth: 2 gp101 - gpkit101 installation gettingstarted debugging diff --git a/docs/source/installation.rst b/docs/source/installation.rst index f90007cb8..548c19dd7 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -18,21 +18,23 @@ GPkit interfaces with two off the shelf solvers: cvxopt, and MOSEK. Cvxopt is open source and installed by default; MOSEK requires a commercial licence or (free) academic license. +Before any of the steps below, if you are using Python 2 run ``pip install ctypesgen``. + Mac OS X - If ``which gcc`` does not return anything, install the `Apple Command Line Tools `_. - - Download `MOSEK `_, then: + - Download `MOSEK 8 `_, then: - Move the ``mosek`` folder to your home directory - Follow `these steps for Mac `_. - Request an `academic license file `_ and put it in ``~/mosek/`` Linux - - Download `MOSEK `_, then: + - Download `MOSEK 8 `_, then: - Move the ``mosek`` folder to your home directory - Follow `these steps for Linux `_. - Request an `academic license file `_ and put it in ``~/mosek/`` Windows - - Download `MOSEK `_, then: + - Download `MOSEK 8 `_, then: - Follow `these steps for Windows `_. - Request an `academic license file `_ and put it in ``C:\Users\(your_username)\mosek\`` - Make sure ``gcc`` is on your system path. diff --git a/docs/source/modelbuilding.rst b/docs/source/modelbuilding.rst index 17bd75527..9ef77b35f 100644 --- a/docs/source/modelbuilding.rst +++ b/docs/source/modelbuilding.rst @@ -9,7 +9,7 @@ with ``sol1.almost_equal(sol2)`` and/or ``print sol1.diff(sol2)``, as shown belo .. code-block:: python - import cPickle as pickle + import pickle ... # build the model sol = m.solve() # uncomment the line below to verify a new model diff --git a/docs/source/releasenotes.rst b/docs/source/releasenotes.rst index b5dc77f0c..93cfe3846 100644 --- a/docs/source/releasenotes.rst +++ b/docs/source/releasenotes.rst @@ -1,171 +1,4 @@ Release Notes ************* -This page lists the changes made in each point version of gpkit. - -Version 0.8.0 -============= - * Much cruft removal, various speedups and fixes - * Auto-differentiation of linked sensitivities - * Simpler installation process - * Solution differencing and pickling - * Tight/Loose warnings in solution and in solution tables - * Tight/loose constraints in solution tables - -Version 0.7.0 -============= - * Variable's values are now used only in their first ConstraintSet; in other ConstraintSets they're free variables - * Variable values can be preserved by setting ``constant=True`` during variable declaration - * MOSEK home directory can be set by a ``MSKHOME`` environment variable at build time - * ``sol(var)`` now always returns Pint Quantities, even if the variable is dimensionless - * ``sol[...][var]``, on the other hand, now always returns floats / numpy arrays of floats - * Optional boundedness checking in docstring (see usage in `docs `_) - * Automatic boundedness checking for GPs - * `Sankey diagrams `_ - * Many other fixes - -Version 0.6.0 -============= - * new much faster NomialMap data structure (#682) - * Many bug fixes and small improvements. - * 6900 lines of code, 2200 lines of tests, 2100 lines of docstring. - -Version 0.5.3 -============= - * faster SP solves (#1109) - * LinkedConstraintSet deprecated (#1110) - * Fixes to autosweep, ConstraintSet, interactive - * Solution time is now stored with soltutions (including sweeps/SPs) - * Model strings are divided with slashes (e.g. Airplane/Wing) - -Version 0.5.2 -============= - * Added new ``sweep`` and ``autosweep`` methods to Model - * Added ``plot`` routines to the results of those routines to make it easy to plot a 1D sweep. - * Added new ``summary`` method to solution_array. - * It and table accept iterables of vars, will only print vars in that iterable (or, by default, all vars) - * Cleaned up and documented the ``interactive`` submodule - * removed contour and sensitivity plots - * added a 1D-sweep plotting function - * added that plotting function as an option within the control panel interface - * Overhauled and documented three types of variables whose value is determined by functions: - * calculated constants - * post-solve calculated variables - * between-GP-solves calculated variables (for Sequential Geometric Programs) - * Fix ``Bounded`` and implement ``debug()`` for SPs - * Apply ``subinplace`` to substitutions dictionary as well - * Require GP substitutions to be Numbers only - * Extend Bounded to one-sided bounds - * Print model's numbers by default, unless ``"modelnums" in exclude`` - * Implement lazy keymapping, allowing GP/SP results to be KeyDicts - * Handle Signomial Inequalities that become Posynomial Inequalities after substitution - * Various documentation updates - * Various bug fixes - -Version 0.5.1 -============= - * O(N) sums and monomial products - * Warn about invalid ConstraintSet elements - * allow setting Tight tolerance as a class attribute - * full backwards compatibility for __init__ methods - * scripts to test remote repositories - * minor fixes, tests, and refactors - * 3550 lines of code, 1800 lines of tests, 1700 lines of docstring. (not counting `interactive`) - -Version 0.5.0 -============= - * No longer recommend the use of linked variables and subinplace (see below) - * Switched default solver to MOSEK - * Added Linked Variable diagram (PR #915) - * Changed how overloaded operators interact with pint (PR #938) - * Added and documented debugging tools (PR #933) - * Added and documented vectorization tools - * Documented modular model construction - * 3200 lines of code, 1800 lines of tests, 1700 lines of docstring. (not counting `interactive`) - -Changes to named models / Model inheritance -------------------------------------------- -We are deprecating the creation of named submodels with custom ``__init__`` methods. Previously, variables created during ``__init__`` in any class inheriting from Model were replaced by a copy with ``__class__.__name__`` added as varkey metadata. This was slow, a bit irregular, and hacky. - -We're moving to an explicitly-irregular ``setup`` method, which (if declared for a class inheriting from Model) is automatically called during ``Model.__init__`` inside a ``NamedVariables(self.__class__.__name__)`` environment. This 1) handles the naming of variables more explicitly and efficiently, and 2) allows us to capture variables created within ``setup``, so that constants that are not a part of any constraint can be used directly (several examples of such template models are in the new `Building Complex Models` documentation). - -``Model.__init__`` calls ``setup`` with the arguments given to the constructor, with the exception of the reserved keyword ``substitutions``. This allows for the easy creation of a named model with custom parameter values (as in the documentation's Beam example). ``setup`` methods should return an iterable (list, tuple, ConstraintSet, ...) of constraints or nothing if the model contains no constraints. To declare a submodel cost, set ``self.cost`` during ``setup``. However, we often find declaring a model's cost explicitly just before solving to be a more legible practice. - -In addition to permitting us to name variables at creation, and include unconstrained variables in a model, we hope that ``setup`` methods will clarify the side effects of named model creation. - -Version 0.4.2 -============= - * prototype handling of SignomialEquality constraints - * fix an issue where solution tables printed incorrect units (despite the units being correct in the ``SolutionArray`` data structure) - * fix ``controlpanel`` slider display for newer versions of ipywidgets - * fix an issue where identical unit-ed variables could have different hashes - * Make the text of several error messages more informative - * Allow monomial approximation of monomials - * bug fixes and improvements to TightConstraintSet - * Don't print results table automatically (it was unwieldy for large models). To print it, ``print sol.table()``. - * Use cvxopt's ldl kkt solver by default for more robustness to rank issues - * Improved ``ConstraintSet.__getitem__``, only returns top-level Variable - * Move toward the varkeys of a ConstraintSet being an immutable set - * CPI update - * numerous pylint fixes - * BoundedConstraint sets added for dual feasibility debugging - * SP sweep compatibility - -Version 0.4.0 -============= - * New model for considering constraints: all constraints are considered as sets of constraints which may contain other constraints, and are asked for their substitutions / posynomial less than 1 representation as late as possible. - * Support for calling external code during an SP solve. - * New class KeyDict to allow referring to variables by name or with objects. - * Many many other bug fixes, speed ups, and refactors under the hood. - -Version 0.3.4 -============= - * Modular / model composition fixes and improvements - * Working controlpanel() for Model - * ipynb and numpy dependency fixes - * printing fixes - * El Capitan fix - * slider widgets now have units - -Version 0.3.2 -============= - * Assorted bug fixes - * Assorted internal improvements and simplifications - * Refactor signomial constraints, resulting in smarter SP heuristic - * Simplify and strengthen equality testing for nomials - * Not counting submodules, went from 2400 to 2500 lines of code and from 1050 to 1170 lines of docstrings and comments. - -Version 0.3 -=========== - * Integrated GP and SP creation under the Model class - * Improved and simplified under-the-hood internals of GPs and SPs - * New experimental SP heuristic - * Improved test coverage - * Handles vectors which are partially constants, partially free - * Simplified interaction with Model objects and made it more pythonic - * Added SP "step" method to allow single-stepping through an SP - * Isolated and corrected some solver-specific behavior - * Fully allowed substitutions of variables for 0 (commit 4631255) - * Use "with" to create a signomials environment (commit cd8d581) - * Continuous integration improvements, thanks @galbramc ! - * Not counting subpackages, went from 2200 to 2400 lines of code (additions were mostly longer error messages) and from 650 to 1050 lines of docstrings and comments. - * Add automatic feasibility-analysis methods to Model and GP - * Simplified solver logging and printing, making it easier to access solver output. - -Version 0.2 -=========== - -* Various bug fixes -* Python 3 compatibility -* Added signomial programming support (alpha quality, may be wrong) -* Added composite objectives -* Parallelized sweeping -* Better table printing -* Linked sweep variables -* Better error messages -* Closest feasible point capability -* Improved install process (no longer requires ctypesgen; auto-detects MOSEK version) -* Added examples: wind turbine, modular GP, examples from 1967 book, maintenance (part replacement) -* Documentation grew by ~70% -* Added Advanced Commands section to documentation -* Many additional unit tests (more than doubled testing lines of code) +`Release notes are available on Github `_ diff --git a/gpkit/__init__.py b/gpkit/__init__.py index 5e724bd80..bb5493cb5 100644 --- a/gpkit/__init__.py +++ b/gpkit/__init__.py @@ -1,5 +1,6 @@ "GP and SP modeling package" -__version__ = "0.8.0" +from __future__ import print_function +__version__ = "0.9.0" from .build import build from ._pint import units, ureg, DimensionalityError diff --git a/gpkit/_mosek/cli_expopt.py b/gpkit/_mosek/cli_expopt.py index 99c737f09..9d5b970f5 100644 --- a/gpkit/_mosek/cli_expopt.py +++ b/gpkit/_mosek/cli_expopt.py @@ -5,6 +5,7 @@ ``result = _mosek.cli_expopt.imize(cs, A, p_idxs, "gpkit_mosek")`` """ +from __future__ import print_function import os import shutil diff --git a/gpkit/_mosek/expopt.py b/gpkit/_mosek/expopt.py index bd02e7362..685ec5ba8 100644 --- a/gpkit/_mosek/expopt.py +++ b/gpkit/_mosek/expopt.py @@ -11,6 +11,7 @@ If the local MOSEK library could not be loaded """ +from __future__ import print_function from ctypes import pointer as ptr from ctypes import POINTER as ptr_factory @@ -117,7 +118,7 @@ def printcb(void, msg): # pylint: disable=unused-argument result : int 0 indicates success """ - print msg[:-1] + print(msg[:-1]) return 0 diff --git a/gpkit/build.py b/gpkit/build.py index 178e0b02f..3da19cad6 100644 --- a/gpkit/build.py +++ b/gpkit/build.py @@ -154,18 +154,18 @@ class Mosek(SolverBackend): def look(self): # pylint: disable=too-many-return-statements "Looks in default install locations for latest mosek version." - if sys.platform == "win32": + if sys.platform[:3] == "win": rootdir = "C:\\Program Files\\Mosek" mosek_platform = "win64x86" libpattern = "mosek64_?_?.dll" self.flags = "-Wl,--export-all-symbols,-R" - elif sys.platform == "darwin": + elif sys.platform[:6] == "darwin": rootdir = pathjoin(os.path.expanduser("~"), "mosek") mosek_platform = "osx64x86" libpattern = "libmosek64.?.?.dylib" self.flags = "-Wl,-rpath" - elif sys.platform == "linux2": + elif sys.platform[:5] == "linux": rootdir = pathjoin(os.path.expanduser("~"), "mosek") mosek_platform = "linux64x86" libpattern = "libmosek64.so" diff --git a/gpkit/constraints/bounded.py b/gpkit/constraints/bounded.py index 683833305..f3e20d114 100644 --- a/gpkit/constraints/bounded.py +++ b/gpkit/constraints/bounded.py @@ -1,4 +1,5 @@ "Implements Bounded" +from __future__ import print_function from collections import defaultdict, OrderedDict import numpy as np from .. import Variable @@ -121,9 +122,9 @@ def check_boundaries(self, result): if distance_above <= self.logtol_threshold: out["value near upper bound"].add(varkey) if self.verbosity > 0 and out: - print - print "Solves with these variables bounded:" + print("") + print("Solves with these variables bounded:") for key, value in out.items(): - print "% 25s: %s" % (key, ", ".join(map(str, value))) - print + print("% 25s: %s" % (key, ", ".join(map(str, value)))) + print("") return out diff --git a/gpkit/constraints/gp.py b/gpkit/constraints/gp.py index 68097232e..f64081674 100644 --- a/gpkit/constraints/gp.py +++ b/gpkit/constraints/gp.py @@ -1,4 +1,5 @@ """Implement the GeometricProgram class""" +from __future__ import print_function import sys from time import time from collections import defaultdict @@ -300,29 +301,27 @@ def _compile_result(self, solver_out): cost_senss = {var: sum([self.cost.exps[i][var]*nu[i] for i in locs]) for (var, locs) in self.cost.varlocs.items()} var_senss = self.v_ss.copy() - # not using HashVector addition because we want to preseve zeros for key, value in cost_senss.items(): var_senss[key] = value + var_senss.get(key, 0) # carry linked sensitivities over to their constants - for v in var_senss.keys(): - if v.gradients: - dlogcost_dlogv = var_senss.pop(v) - val = result["constants"][v] - for c, dv_dc in v.gradients.items(): - if val != 0: - dlogv_dlogc = dv_dc * result["constants"][c]/val - # make nans / infs explicitly to avoid warnings - elif dlogcost_dlogv == 0: - dlogv_dlogc = np.nan - else: - dlogv_dlogc = np.inf * dv_dc*result["constants"][c] - accum = var_senss.get(c, 0) - var_senss[c] = dlogcost_dlogv*dlogv_dlogc + accum - if v in cost_senss: - if c in self.cost.varkeys: - dlogcost_dlogv = cost_senss.pop(v) - accum = cost_senss.get(c, 0) - cost_senss[c] = dlogcost_dlogv*dlogv_dlogc + accum + for v in list(v for v in var_senss if v.gradients): + dlogcost_dlogv = var_senss.pop(v) + val = result["constants"][v] + for c, dv_dc in v.gradients.items(): + if val != 0: + dlogv_dlogc = dv_dc * result["constants"][c]/val + # make nans / infs explicitly to avoid warnings + elif dlogcost_dlogv == 0: + dlogv_dlogc = np.nan + else: + dlogv_dlogc = np.inf * dv_dc*result["constants"][c] + accum = var_senss.get(c, 0) + var_senss[c] = dlogcost_dlogv*dlogv_dlogc + accum + if v in cost_senss: + if c in self.cost.varkeys: + dlogcost_dlogv = cost_senss.pop(v) + accum = cost_senss.get(c, 0) + cost_senss[c] = dlogcost_dlogv*dlogv_dlogc + accum result["sensitivities"]["cost"] = cost_senss result["sensitivities"]["variables"] = KeyDict(var_senss) @@ -450,10 +449,11 @@ def check_mono_eq_bounds(missingbounds, meq_bounds): still_alive = True while still_alive: still_alive = False # if no changes are made, the loop exits - for bound, conditions in meq_bounds.items(): + for bound in list(meq_bounds): if bound not in missingbounds: del meq_bounds[bound] continue + conditions = meq_bounds[bound] for condition in conditions: if not any(bound in missingbounds for bound in condition): del meq_bounds[bound] diff --git a/gpkit/constraints/loose.py b/gpkit/constraints/loose.py index 4c68062ce..b939c5cdf 100644 --- a/gpkit/constraints/loose.py +++ b/gpkit/constraints/loose.py @@ -1,4 +1,5 @@ "Implements Loose" +from __future__ import print_function from .set import ConstraintSet from ..small_scripts import appendsolwarning diff --git a/gpkit/constraints/model.py b/gpkit/constraints/model.py index 9eb14c4ee..0af455ddc 100644 --- a/gpkit/constraints/model.py +++ b/gpkit/constraints/model.py @@ -1,4 +1,5 @@ "Implements Model" +from __future__ import print_function import numpy as np from .costed import CostedConstraintSet from ..nomials import Monomial @@ -233,7 +234,7 @@ def debug(self, solver=None, verbosity=1, **solveargs): print(" %s: relaxed from %-.4g to %-.4g" % (orig, mag(constsrelaxed.constants[orig.key]), mag(sol(orig)))) - print + print("") if verbosity: print(">> Success!") except (ValueError, RuntimeWarning): @@ -249,12 +250,13 @@ def debug(self, solver=None, verbosity=1, **solveargs): sol = feas.solve(**solveargs) except InvalidGPConstraint: sol = feas.localsolve(**solveargs) + relaxed_constraints = feas[0]["relaxed constraints"] relaxed = get_relaxed(sol(constrsrelaxed.relaxvars), - range(len(feas[0][0]))) + range(len(relaxed_constraints))) if verbosity and relaxed: print("\nSolves with these constraints relaxed:") for relaxval, i in relaxed: - constraint = feas[0][0][i][0] + constraint = relaxed_constraints[i][0] # substitutions of the final relax value conleft = constraint.left.sub( {constrsrelaxed.relaxvars[i]: relaxval}) @@ -273,7 +275,7 @@ def debug(self, solver=None, verbosity=1, **solveargs): if verbosity: print(">> Failure") if verbosity: - print + print("") return sol diff --git a/gpkit/constraints/prog_factories.py b/gpkit/constraints/prog_factories.py index 29f873961..ec67f5841 100644 --- a/gpkit/constraints/prog_factories.py +++ b/gpkit/constraints/prog_factories.py @@ -1,4 +1,5 @@ "Scripts for generating, solving and sweeping programs" +from __future__ import print_function from time import time import numpy as np from ..nomials import parse_subs @@ -20,7 +21,7 @@ def evaluate_linked(constants, linked): kdc = KeyDict({k: adnumber(maybe_flatten(v)) for k, v in constants.items()}) kdc.log_gets = True - kdc_plain = KeyDict(constants) + kdc_plain = None array_calulated, logged_array_gets = {}, {} for v, f in linked.items(): try: @@ -51,6 +52,8 @@ def evaluate_linked(constants, linked): print("Couldn't auto-differentiate linked variable %s.\n " "(to raise the error directly for debugging purposes," " set gpkit.settings[\"ad_errors_raise\"] to True)" % v) + if kdc_plain is None: + kdc_plain = KeyDict(constants) constants[v] = f(kdc_plain) v.descr.pop("gradients", None) finally: @@ -135,14 +138,17 @@ def run_sweep(genfunction, self, solution, skipsweepfailures, constants, sweep, linked, solver, verbosity, **kwargs): "Runs through a sweep." + # sort sweeps by the eqstr of their varkey + sweepvars, sweepvals = zip(*sorted(list(sweep.items()), + key=lambda vkval: vkval[0].eqstr)) if len(sweep) == 1: - sweep_grids = np.array(list(sweep.values())) + sweep_grids = np.array(list(sweepvals)) else: - sweep_grids = np.meshgrid(*list(sweep.values())) + sweep_grids = np.meshgrid(*list(sweepvals)) N_passes = sweep_grids[0].size sweep_vects = {var: grid.reshape(N_passes) - for (var, grid) in zip(sweep, sweep_grids)} + for (var, grid) in zip(sweepvars, sweep_grids)} if verbosity > 0: print("Solving over %i passes." % N_passes) @@ -170,15 +176,12 @@ def run_sweep(genfunction, self, solution, skipsweepfailures, solution["sweepvariables"] = KeyDict() ksweep = KeyDict(sweep) - delvars = set() - for var, val in solution["constants"].items(): + for var, val in list(solution["constants"].items()): if var in ksweep: solution["sweepvariables"][var] = val - delvars.add(var) - else: + del solution["constants"][var] + elif var not in linked: solution["constants"][var] = [val[0]] - for var in delvars: - del solution["constants"][var] if verbosity > 0: soltime = time() - tic diff --git a/gpkit/constraints/relax.py b/gpkit/constraints/relax.py index 92074e3e2..89823794f 100644 --- a/gpkit/constraints/relax.py +++ b/gpkit/constraints/relax.py @@ -118,18 +118,14 @@ def __init__(self, constraints, include_only=None, exclude=None): constrained_varkeys = constraints.constrained_varkeys() if linked: kdc = KeyDict(constants) - combined = {k: f(kdc) for k, f in linked.items() - if k in constrained_varkeys} - combined.update({k: v for k, v in constants.items() - if k in constrained_varkeys}) - else: - combined = constants - self.constants = KeyDict(combined) + constants.update({k: f(kdc) for k, f in linked.items() + if k in constrained_varkeys}) + self.constants = constants relaxvars, relaxation_constraints, self.origvars = [], [], [] with NamedVariables("Relax") as (self.lineage, _): pass self._unrelaxmap = {} - for key, value in combined.items(): + for key, value in constants.items(): if value == 0: continue elif include_only and key.name not in include_only: @@ -138,7 +134,6 @@ def __init__(self, constraints, include_only=None, exclude=None): continue key.descr.pop("gradients", None) descr = key.descr.copy() - descr.pop("value", None) descr.pop("veckey", None) descr["lineage"] = descr.pop("lineage", ())+(self.lineage[-1],) relaxvardescr = descr.copy() diff --git a/gpkit/constraints/set.py b/gpkit/constraints/set.py index 1a78fc10b..37337df5e 100644 --- a/gpkit/constraints/set.py +++ b/gpkit/constraints/set.py @@ -2,7 +2,7 @@ from collections import defaultdict import numpy as np -from ..small_classes import HashVector, Numbers +from ..small_classes import Numbers from ..keydict import KeySet, KeyDict from ..small_scripts import try_str_without from ..repr_conventions import GPkitObject @@ -13,7 +13,7 @@ def add_meq_bounds(bounded, meq_bounded): still_alive = True while still_alive: still_alive = False # if no changes are made, the loop exits - for bound, conditions in meq_bounded.items(): + for bound, conditions in list(meq_bounded.items()): if bound in bounded: # bound exists in an inequality del meq_bounded[bound] continue @@ -27,7 +27,7 @@ def add_meq_bounds(bounded, meq_bounded): def _sort_by_name_and_idx(var): "return tuple for Variable sorting" - return (var.key.str_without(["units", "idx"]), var.key.idx) + return (var.key.str_without(["units", "idx"]), var.key.idx or ()) # pylint: disable=too-many-instance-attributes @@ -202,7 +202,7 @@ def sens_from_dual(self, las, nus, result): var_senss : dict The variable sensitivities of this constraint """ - var_senss = HashVector() + var_senss = {} offset = 0 self.relax_sensitivity = 0 for i, constr in enumerate(self): @@ -211,7 +211,6 @@ def sens_from_dual(self, las, nus, result): nu = nus[offset:offset+n_posys] constr.v_ss = constr.sens_from_dual(la, nu, result) self.relax_sensitivity += constr.relax_sensitivity - # not using HashVector addition because we want to preseve zeros for key, value in constr.v_ss.items(): var_senss[key] = value + var_senss.get(key, 0) offset += n_posys diff --git a/gpkit/constraints/sgp.py b/gpkit/constraints/sgp.py index 0e3ac3799..a5de30f34 100644 --- a/gpkit/constraints/sgp.py +++ b/gpkit/constraints/sgp.py @@ -1,4 +1,5 @@ """Implement the SequentialGeometricProgram class""" +from __future__ import print_function from time import time import numpy as np from collections import OrderedDict @@ -128,7 +129,7 @@ def localsolve(self, solver=None, verbosity=1, x0=None, reltol=1e-4, warn_on_check=True, gen_result=False, **kwargs) self.solver_outs.append(solver_out) - x0 = KeyDict(zip(gp.varlocs, np.exp(solver_out["primal"]))) + x0 = dict(zip(gp.varlocs, np.exp(solver_out["primal"]))) if "objective" in solver_out: cost = float(solver_out["objective"]) else: diff --git a/gpkit/globals.py b/gpkit/globals.py index adf0d9011..7c210106f 100644 --- a/gpkit/globals.py +++ b/gpkit/globals.py @@ -1,5 +1,8 @@ "global mutable variables" +from __future__ import print_function +from six import with_metaclass import os +import sys from collections import defaultdict from . import build @@ -18,6 +21,9 @@ def load_settings(path=None, firstattempt=True): # unless they're the solver list if len(value) == 1 and name != "installed_solvers": settings_[name] = value[0] + if sys.version_info >= (3, 0) and name == "installed_solvers": + if "mosek" in value: + value.remove("mosek") except IOError: settings_ = {"installed_solvers": [""]} if settings_["installed_solvers"] == [""]: @@ -61,7 +67,7 @@ def __bool__(cls): return cls._true -class SignomialsEnabled(object): +class SignomialsEnabled(with_metaclass(SignomialsEnabledMeta)): # pylint: disable=no-init """Class to put up and tear down signomial support in an instance of GPkit. Example @@ -73,7 +79,6 @@ class SignomialsEnabled(object): >>> constraints = [x >= 1-y] >>> gpkit.Model(x, constraints).localsolve() """ - __metaclass__ = SignomialsEnabledMeta _true = False # the current signomial permissions def __enter__(self): diff --git a/gpkit/interactive/sankey.py b/gpkit/interactive/sankey.py index d169b638e..5fdfcca0e 100644 --- a/gpkit/interactive/sankey.py +++ b/gpkit/interactive/sankey.py @@ -1,4 +1,5 @@ "implements Sankey" +from __future__ import print_function from collections import defaultdict import numpy as np from ipysankeywidget import SankeyWidget # pylint: disable=import-error @@ -79,7 +80,7 @@ def varlinks(self, constrset, key, target=None, printing=True): if printing: print ("(objective) adds %+.3g to the sensitivity" " of %s" % (-value, key)) - print "(objective) is", self.gp.cost, "\n" + print("(objective) is", self.gp.cost, "\n") for constr in constrset: if key not in constr.v_ss: continue @@ -102,7 +103,7 @@ def varlinks(self, constrset, key, target=None, printing=True): if printing: print ("%s adds %+.3g to the overall sensitivity of %s" % (source, value, key)) - print source, "is", constr.str_without("units"), "\n" + print(source, "is", constr.str_without("units"), "\n") if ((isinstance(constr, MonomialEquality) or abs(value) >= INSENSITIVE) and all(len(getattr(p, "hmap", [])) == 1 diff --git a/gpkit/interactive/widgets.py b/gpkit/interactive/widgets.py index 879d08c17..1a0aaffc3 100644 --- a/gpkit/interactive/widgets.py +++ b/gpkit/interactive/widgets.py @@ -1,4 +1,5 @@ "Interactive GPkit widgets for iPython notebook" +from __future__ import print_function import ipywidgets as widgets from traitlets import link from ..small_scripts import is_sweepvar @@ -32,7 +33,7 @@ def modelinteract(model, fns_of_sol, ranges=None, **solvekwargs): if not isinstance(ranges, dict): ranges = {k: None for k in ranges} slider_vars = set() - for k in ranges.keys(): + for k in list(ranges): if k in model.substitutions: # only if already a constant for key in model.varkeys[k]: slider_vars.add(key) @@ -85,9 +86,9 @@ def resolve(**subs): sol = model.result for fn in fns_of_sol: fn(sol) - except RuntimeWarning, e: - print "RuntimeWarning:", str(e).split("\n")[0] - print "\n> Running model.debug()" + except RuntimeWarning as e: + print("RuntimeWarning:", str(e).split("\n")[0]) + print("\n> Running model.debug()") model.debug() resolve() @@ -115,7 +116,7 @@ def __defaultfn(solution): "Display function to run when a slider is moved." # NOTE: if there are some freevariables in showvars, filter # the table to show only those and the slider constants - print solution.summary(showvars if freev_in_showvars else ()) + print(solution.summary(showvars if freev_in_showvars else ())) __defaultfntable = __defaultfn diff --git a/gpkit/keydict.py b/gpkit/keydict.py index a2202b1eb..5d65bebd9 100644 --- a/gpkit/keydict.py +++ b/gpkit/keydict.py @@ -1,5 +1,5 @@ "Implements KeyDict and KeySet classes" -from collections import defaultdict +from collections import defaultdict, Hashable import numpy as np from .small_classes import Numbers, Quantity, FixedScalar from .small_scripts import is_sweepvar, isnan, SweepValue @@ -57,6 +57,7 @@ class KeyDict(dict): """ collapse_arrays = True keymapping = True + keymap = [] def __init__(self, *args, **kwargs): "Passes through to dict.__init__ via the `update()` method" @@ -66,6 +67,7 @@ def __init__(self, *args, **kwargs): self._unmapped_keys = set() self.log_gets = False self.logged_gets = set() + self.owned = set() self.update(*args, **kwargs) def get(self, key, alternative=KeyError): @@ -75,15 +77,21 @@ def get(self, key, alternative=KeyError): return alternative return self[key] + def _copyonwrite(self, key): + "Copys arrays before they are written to" + if key not in self.owned: + dict.__setitem__(self, key, dict.__getitem__(self, key).copy()) + self.owned.add(key) + def update(self, *args, **kwargs): "Iterates through the dictionary created by args and kwargs" - for k, v in dict(*args, **kwargs).items(): - if hasattr(v, "copy"): - # We don't want just a reference (for e.g. numpy arrays) - # KeyDict values are expected to be immutable (Numbers) - # or to have a copy attribute. - v = v.copy() - self[k] = v + if not self and len(args) == 1 and isinstance(args[0], KeyDict): + dict.update(self, args[0]) + self.keymap.update(args[0].keymap) + self._unmapped_keys.update(args[0]._unmapped_keys) # pylint:disable=protected-access + else: + for k, v in dict(*args, **kwargs).items(): + self[k] = v def parse_and_index(self, key): "Returns key if key had one, and veckey/idx for indexed veckeys." @@ -116,7 +124,7 @@ def parse_and_index(self, key): key = key.veckey return key, idx - def __contains__(self, key): + def __contains__(self, key): # pylint:disable=too-many-return-statements "In a winding way, figures out if a key is in the KeyDict" try: key, idx = self.parse_and_index(key) @@ -124,6 +132,8 @@ def __contains__(self, key): return False except ValueError: # multiple keys correspond return True + if not isinstance(key, Hashable): + return False if dict.__contains__(self, key): if idx: try: @@ -167,6 +177,8 @@ def __getitem__(self, key): raise KeyError(key) values = [] for k in keys: + if not idx and k.shape: + self._copyonwrite(k) got = dict.__getitem__(self, k) if idx: got = got[idx] @@ -182,6 +194,8 @@ def __setitem__(self, key, value): # pylint: disable=too-many-boolean-expressions key, idx = self.parse_and_index(key) if key not in self.keymap: + if not hasattr(self, "_unmapped_keys"): + self.__init__() # py3's pickle sets items before init... :( self.keymap[key].add(key) self._unmapped_keys.add(key) if idx: @@ -189,6 +203,7 @@ def __setitem__(self, key, value): kwargs = {} if number_array else {"dtype": "object"} emptyvec = np.full(key.shape, np.nan, **kwargs) dict.__setitem__(self, key, emptyvec) + self.owned.add(key) if isinstance(value, FixedScalar): value = value.value # substitute constant monomials if isinstance(value, Quantity): @@ -197,7 +212,9 @@ def __setitem__(self, key, value): if is_sweepvar(value): dict.__setitem__(self, key, np.array(dict.__getitem__(self, key), object)) + self.owned.add(key) value = SweepValue(value[1]) + self._copyonwrite(key) dict.__getitem__(self, key)[idx] = value else: if (self.collapse_arrays and hasattr(key, "descr") @@ -211,15 +228,19 @@ def __setitem__(self, key, value): value = np.array([clean_value(key, v) for v in value]) if getattr(value, "shape", False) and dict.__contains__(self, key): goodvals = ~isnan(value) - if self[key].dtype != value.dtype: + present_value = dict.__getitem__(self, key) + if present_value.dtype != value.dtype: # e.g., we're replacing a number with a linked function - dict.__setitem__(self, key, np.array(self[key], + dict.__setitem__(self, key, np.array(present_value, dtype=value.dtype)) + self.owned.add(key) + self._copyonwrite(key) self[key][goodvals] = value[goodvals] else: if hasattr(value, "dtype") and value.dtype == INT_DTYPE: value = np.array(value, "f") dict.__setitem__(self, key, value) + self.owned.add(key) def update_keymap(self): "Updates the keymap with the keys in _unmapped_keys" @@ -238,7 +259,8 @@ def __delitem__(self, key): key, idx = self.parse_and_index(key) keys = self.keymap[key] if not keys: - raise KeyError("key %s not found." % key) + raise KeyError(key) + copied = set() # have to copy bc update leaves duplicate sets for k in list(keys): delete = True if idx: @@ -250,11 +272,15 @@ def __delitem__(self, key): mapkeys = set([k]) if self.keymapping and hasattr(k, "keys"): mapkeys.update(k.keys) - for mappedkey in mapkeys: - if mappedkey in self.keymap: - self.keymap[mappedkey].remove(k) - if not self.keymap[mappedkey]: - del self.keymap[mappedkey] + for mapkey in mapkeys: + if mapkey in self.keymap: + if len(self.keymap[mapkey]) == 1: + del self.keymap[mapkey] + continue + if mapkey not in copied: + self.keymap[mapkey] = set(self.keymap[mapkey]) + copied.add(mapkey) + self.keymap[mapkey].remove(k) class KeySet(KeyDict): diff --git a/gpkit/nomials/array.py b/gpkit/nomials/array.py index 93691e164..960ea3f91 100644 --- a/gpkit/nomials/array.py +++ b/gpkit/nomials/array.py @@ -7,7 +7,9 @@ >>> px = gpkit.NomialArray([1, x, x**2]) """ +from __future__ import print_function from operator import eq, le, ge, xor +from functools import reduce # pylint: disable=redefined-builtin import numpy as np from .map import NomialMap from .math import Signomial diff --git a/gpkit/nomials/core.py b/gpkit/nomials/core.py index d08065aa8..307a33e6f 100644 --- a/gpkit/nomials/core.py +++ b/gpkit/nomials/core.py @@ -53,7 +53,17 @@ def latex(self, excluded=()): units_tf = units.replace("frac", "tfrac").replace(r"\cdot", r"\cdot ") return " + ".join(sorted(mstrs)) + units_tf - __hash__ = NomialData.__hash__ # required by Python 3 + def prod(self): + "Return self for compatibility with NomialArray" + return self + + def sum(self): + "Return self for compatibility with NomialArray" + return self + + def to(self, units): + "Create new Signomial converted to new units" + return self.__class__(self.hmap.to(units)) # pylint: disable=no-member @property def value(self): @@ -69,30 +79,19 @@ def value(self): p = self.sub(self.varkeyvalues()) # pylint: disable=not-callable return p.cs[0] if isinstance(p, FixedScalar) else p - def prod(self): - "Return self for compatibility with NomialArray" - return self - - def sum(self): - "Return self for compatibility with NomialArray" - return self - - def to(self, units): - "Create new Signomial converted to new units" - return self.__class__(self.hmap.to(units)) # pylint: disable=no-member - def __eq__(self, other): "True if self and other are algebraically identical." if isinstance(other, Numbers): return isinstance(self, FixedScalar) and self.value == other return super(Nomial, self).__eq__(other) - def __radd__(self, other): - return self + other + # pylint: disable=multiple-statements + def __ne__(self, other): return not Nomial.__eq__(self, other) - def __rmul__(self, other): - return self * other + # required by Python 3 + __hash__ = NomialData.__hash__ + def __truediv__(self, other): return self.__div__(other) # pylint: disable=not-callable - def __truediv__(self, other): - "For the / operator in Python 3.x" - return self.__div__(other) # pylint: disable=not-callable + # for arithmetic consistency + def __radd__(self, other): return self + other + def __rmul__(self, other): return self * other diff --git a/gpkit/nomials/data.py b/gpkit/nomials/data.py index 147bde225..0cc80c9de 100644 --- a/gpkit/nomials/data.py +++ b/gpkit/nomials/data.py @@ -57,15 +57,13 @@ def exps(self): def cs(self): "Create cs or return cached cs" if self._cs is None: - self._cs = np.array(self.hmap.values()) + self._cs = np.array(list(self.hmap.values())) if self.hmap.units: self._cs = self._cs*self.hmap.units return self._cs def __hash__(self): - if self._hashvalue is None: - self._hashvalue = hash(hash(self.hmap) + hash(str(self.hmap.units))) - return self._hashvalue + return hash(self.hmap) @property def varkeys(self): diff --git a/gpkit/nomials/map.py b/gpkit/nomials/map.py index 0316938a0..379519a4b 100644 --- a/gpkit/nomials/map.py +++ b/gpkit/nomials/map.py @@ -3,7 +3,6 @@ import numpy as np from ..exceptions import DimensionalityError from ..small_classes import HashVector, Strings, qty, EMPTY_HV -from ..small_scripts import mag from .substitution import parse_subs DIMLESS_QUANTITY = qty("dimensionless") @@ -22,6 +21,10 @@ class NomialMap(HashVector): expmap = None # used for monomial-mapping postsubstitution; see .mmap() csmap = None # used for monomial-mapping postsubstitution; see .mmap() + def copy(self): + "Return a copy of this" + return self.__class__(self) + def units_of_product(self, thing, thing2=None): "Sets units to those of `thing*thing2`. Ugly optimized code." if thing is None and thing2 is None: @@ -68,12 +71,14 @@ def diff(self, varkey): out = NomialMap() for exp in self: if varkey in exp: - exp = HashVector(exp) + exp = exp.copy() x = exp[varkey] c = self[exp] * x if x is 1: + exp.hashvalue ^= hash((varkey, 1)) del exp[varkey] else: + exp.hashvalue ^= hash((varkey, x)) ^ hash((varkey, x-1)) exp[varkey] = x-1 out[exp] = c out.units_of_product(self.units, @@ -121,19 +126,17 @@ def sub(self, substitutions, varkeys, parsedsubs=False): if vk in fixed: varlocs[vk].add((exp, new_exp)) + squished = set() for vk in varlocs: - expval = [] exps, cval = varlocs[vk], fixed[vk] if hasattr(cval, "hmap"): - expval, = cval.hmap.keys() # NOTE: fails on posynomials - cval = cval.hmap - if hasattr(cval, "to"): - cval = mag(cval.to(vk.units or DIMLESS_QUANTITY)) - if expval or isinstance(cval, NomialMap): - cval, = cval.values() - exps_covered = set() + if any(cval.hmap.keys()): + raise("Monomial substitutions are no longer supported.") + cval, = cval.hmap.to(vk.units or DIMLESS_QUANTITY).values() + elif hasattr(cval, "to"): + cval = cval.to(vk.units or DIMLESS_QUANTITY).magnitude for o_exp, exp in exps: - subinplace(cp, exp, o_exp, vk, cval, expval, exps_covered) + subinplace(cp, exp, o_exp, vk, cval, squished) return cp def mmap(self, orig): @@ -156,7 +159,7 @@ def mmap(self, orig): origexps = list(orig.keys()) selfexps = list(self.keys()) for orig_exp, self_exp in self.expmap.items(): - total_c = self.get(self_exp, None) + total_c = self.get(self_exp, None) # TODO: seems unnecessary? if total_c: fraction = self.csmap.get(orig_exp, orig[orig_exp])/total_c m_from_ms[self_exp][orig_exp] = fraction @@ -166,32 +169,27 @@ def mmap(self, orig): # pylint: disable=invalid-name -def subinplace(cp, exp, o_exp, vk, cval, expval, exps_covered): +def subinplace(cp, exp, o_exp, vk, cval, squished): "Modifies cp by substituing cval/expval for vk in exp" x = exp[vk] - powval = float(cval)**x if cval != 0 or x >= 0 else np.inf + powval = float(cval)**x if cval != 0 or x >= 0 else np.sign(cval)*np.inf cp.csmap[o_exp] *= powval - if exp in cp and exp not in exps_covered: + if exp in cp: c = cp.pop(exp) - exp._hashvalue ^= hash((vk, x)) # remove (key, value) from _hashvalue + exp.hashvalue ^= hash((vk, x)) # remove (key, value) from hashvalue del exp[vk] - for key in expval: - if key in exp: - exp._hashvalue ^= hash((key, exp[key])) # remove from hash - newval = expval[key]*x + exp[key] - else: - newval = expval[key]*x - exp._hashvalue ^= hash((key, newval)) # add to hash - exp[key] = newval value = powval * c if exp in cp: + squished.add(exp.copy()) currentvalue = cp[exp] if value != -currentvalue: - cp[exp] = value + currentvalue + cp[exp] += value else: del cp[exp] # remove zeros created during substitution elif value: cp[exp] = value - exps_covered.add(exp) - if not cp: # make sure it's never an empty hmap - cp[EMPTY_HV] = 0.0 + if not cp: # make sure it's never an empty hmap + cp[EMPTY_HV] = 0.0 + elif exp in squished: + exp.hashvalue ^= hash((vk, x)) # remove (key, value) from hashvalue + del exp[vk] diff --git a/gpkit/nomials/math.py b/gpkit/nomials/math.py index 314961df4..08851a642 100644 --- a/gpkit/nomials/math.py +++ b/gpkit/nomials/math.py @@ -1,4 +1,5 @@ """Signomial, Posynomial, Monomial, Constraint, & MonoEQCOnstraint classes""" +from __future__ import print_function, unicode_literals from collections import defaultdict import numpy as np from .core import Nomial @@ -47,7 +48,8 @@ def __init__(self, hmap=None, cs=1, require_positive=True, **descr): # pylint: elif isinstance(hmap, Strings): hmap = VarKey(hmap, **descr).hmap elif isinstance(hmap, dict): - exp = HashVector({VarKey(k): v for k, v in hmap.items() if v}) + exp = HashVector({VarKey(k): v + for k, v in hmap.items() if v}) hmap = NomialMap({exp: mag(cs)}) hmap.units_of_product(cs) super(Signomial, self).__init__(hmap) @@ -399,7 +401,7 @@ def _simplify_posy_ineq(self, hmap, pmap=None): return hmap coeff = 1 - hmap[EMPTY_HV] if pmap is not None: # note constant term's mmap - const_idx = hmap.keys().index(EMPTY_HV) + const_idx = list(hmap.keys()).index(EMPTY_HV) self.const_mmap = self.pmap.pop(const_idx) # pylint: disable=attribute-defined-outside-init self.const_coeff = coeff # pylint: disable=attribute-defined-outside-init if coeff >= -self.feastol and len(hmap) == 1: @@ -439,7 +441,7 @@ def _gen_unsubbed(self, p_lt, m_gt): except DimensionalityError: raise DimensionalityError(p_lt, m_gt) hmap = p_lt.hmap.copy() - for exp in hmap.keys(): + for exp in list(hmap): hmap[exp-m_exp] = hmap.pop(exp)/m_c hmap = self._simplify_posy_ineq(hmap) if hmap is None: @@ -492,11 +494,9 @@ def sens_from_dual(self, la, nu, result): # pylint: disable=unused-argument for idx, percentage in self.const_mmap.items(): nu_[idx] += percentage * la*scale nu = nu_ - var_senss = {} # Constant sensitivities - for var in self.varkeys: - locs = presub.varlocs[var] - var_senss[var] = sum([presub.exps[i][var]*nu[i] for i in locs]) - return var_senss + return {var: sum([presub.exps[i][var]*nu[i] + for i in presub.varlocs[var]]) + for var in self.varkeys} # Constant sensitivities def as_gpconstr(self, x0): # pylint: disable=unused-argument "The GP version of a Posynomial constraint is itself" @@ -518,7 +518,7 @@ def __init__(self, left, right): self.meq_bounded = {} self.relax_sensitivity = 0 # don't count equality sensitivities if self.unsubbed and len(self.varkeys) > 1: - exp = self.unsubbed[0].hmap.keys()[0] + exp, = list(self.unsubbed[0].hmap.keys()) for key, e in exp.items(): if key in self.substitutions: for bound in ("upper", "lower"): @@ -561,7 +561,7 @@ def sens_from_dual(self, la, nu, result): if not la or not nu: return {} # as_posyslt1 created no inequalities self.relax_sensitivity = la[0] - la[1] - var_senss = HashVector() + var_senss = {} for var in self.varkeys: for i, m in enumerate(self.unsubbed): if var in m.varlocs: @@ -632,8 +632,8 @@ def as_posyslt1(self, substitutions=None): 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: (self._negysig.varkeys.keys() - + sig.varkeys.keys()) + self._sigvars = {exp: (list(self._negysig.varkeys.keys()) + + list(sig.varkeys.keys())) for exp, sig in self._coeffsigs.items()} return p_ineq.as_posyslt1(substitutions) @@ -670,10 +670,11 @@ def subval(posy): "Substitute solution into a posynomial and return the result" hmap = posy.sub(result["variables"], require_positive=False).hmap - assert len(hmap) == 1 and not hmap.keys()[0] # constant - return hmap.values()[0] + (key, value), = hmap.items() + assert not key # constant + return value - var_senss = HashVector() + var_senss = {} invnegy_val = 1/subval(self._negysig) for i, nu_i in enumerate(nu): mon = self._mons[i] diff --git a/gpkit/small_classes.py b/gpkit/small_classes.py index 2eafc962c..3e76bf021 100644 --- a/gpkit/small_classes.py +++ b/gpkit/small_classes.py @@ -1,5 +1,6 @@ """Miscellaneous small classes""" from operator import xor +from six import with_metaclass import numpy as np from ._pint import Quantity, qty # pylint: disable=unused-import from functools import reduce # pylint: disable=redefined-builtin @@ -19,9 +20,8 @@ def __instancecheck__(cls, obj): return hasattr(obj, "hmap") and len(obj.hmap) == 1 and not obj.vks -class FixedScalar(object): +class FixedScalar(with_metaclass(FixedScalarMeta)): # pylint: disable=no-init "Instances of this class are scalar Nomials with no variables" - __metaclass__ = FixedScalarMeta class Count(object): @@ -183,16 +183,19 @@ class HashVector(dict): >>> x = gpkit.nomials.Monomial('x') >>> exp = gpkit.small_classes.HashVector({x: 2}) """ - def copy(self): - "Return a copy of this" - return self.__class__(super(HashVector, self).copy()) + hashvalue = None - # pylint:disable=access-member-before-definition, attribute-defined-outside-init def __hash__(self): "Allows HashVectors to be used as dictionary keys." - if not hasattr(self, "_hashvalue") or self._hashvalue is None: - self._hashvalue = reduce(xor, map(hash, self.items()), 0) - return self._hashvalue + if self.hashvalue is None: + self.hashvalue = reduce(xor, map(hash, self.items()), 0) + return self.hashvalue + + def copy(self): + "Return a copy of this" + hv = self.__class__(self) + hv.hashvalue = self.hashvalue + return hv def __neg__(self): "Return Hashvector with each value negated." @@ -237,6 +240,7 @@ def __add__(self, other): sums[key] = value + svalue else: sums[key] = value + sums.hashvalue = None return sums return NotImplemented @@ -245,6 +249,7 @@ def __sub__(self, other): return self + -other def __rsub__(self, other): return other + -self def __radd__(self, other): return self + other def __div__(self, other): return self * other**-1 + def __truediv__(self, other): return self * other**-1 def __rdiv__(self, other): return other * self**-1 def __rmul__(self, other): return self * other diff --git a/gpkit/small_scripts.py b/gpkit/small_scripts.py index 9cc7bb580..9e70e8dde 100644 --- a/gpkit/small_scripts.py +++ b/gpkit/small_scripts.py @@ -1,4 +1,5 @@ """Assorted helper methods""" +from __future__ import print_function from collections import Iterable import numpy as np diff --git a/gpkit/solution_array.py b/gpkit/solution_array.py index 2246ff747..a3323d038 100644 --- a/gpkit/solution_array.py +++ b/gpkit/solution_array.py @@ -1,7 +1,8 @@ """Defines SolutionArray class""" +from __future__ import print_function import re from collections import Iterable -import cPickle as pickle +import pickle import numpy as np from .nomials import NomialArray from .small_classes import DictOfLists, Strings @@ -314,7 +315,7 @@ def diff(self, sol, showvars=None, min_percent=1.0, str """ if isinstance(sol, Strings): - sol = pickle.load(open(sol)) + sol = pickle.load(open(sol, "rb")) selfvars = set(self["variables"]) solvars = set(sol["variables"]) if showvars: @@ -438,11 +439,11 @@ def save(self, filename="solution.pkl"): (the "message" field is preserved) Solution can then be loaded with e.g.: - >>> import cPickle as pickle + >>> import pickle >>> pickle.load(open("solution.pkl")) """ program, model, cost, warnings = self.pickle_prep() - pickle.dump(self, open(filename, "w")) + pickle.dump(self, open(filename, "wb")) self["cost"], self["warnings"] = cost, warnings self.program, self.model = program, model @@ -759,7 +760,7 @@ def var_table(data, title, printunits=True, latex=False, rawlines=False, if dim_size >= ncols and dim_size <= maxcolumns: horiz_dim, ncols = dim_idx, dim_size # align the array with horiz_dim by making it the last one - dim_order = range(last_dim_index) + dim_order = list(range(last_dim_index)) dim_order.insert(horiz_dim, last_dim_index) flatval = val.transpose(dim_order).flatten() vals = [vecfmt % v for v in flatval[:ncols]] diff --git a/gpkit/tests/from_paths.py b/gpkit/tests/from_paths.py index aaa209d36..4f19fed99 100644 --- a/gpkit/tests/from_paths.py +++ b/gpkit/tests/from_paths.py @@ -1,4 +1,5 @@ "Runs each file listed in pwd/TESTS as a test" +from __future__ import print_function import unittest import os @@ -25,7 +26,7 @@ def clean(string): def add_filetest(testclass, path): "Add test that imports the given path and runs its test() function" path = path.strip() - print "adding test for", repr(path) + print("adding test for", repr(path)) def test_fn(self): top_level = os.getcwd() diff --git a/gpkit/tests/helpers.py b/gpkit/tests/helpers.py index bfaedf654..178e08b49 100644 --- a/gpkit/tests/helpers.py +++ b/gpkit/tests/helpers.py @@ -5,6 +5,9 @@ import importlib from ..repr_conventions import DEFAULT_UNIT_PRINTING +if sys.version_info >= (3, 0): + reload = importlib.reload # pylint: disable=redefined-builtin,invalid-name,no-member + def generate_example_tests(path, testclasses, solvers=None, newtest_fn=None): """ @@ -56,7 +59,10 @@ def test(self): import gpkit with NewDefaultSolver(solver): - testfn(name, import_dict, path)(self) + try: + testfn(name, import_dict, path)(self) + except FutureWarning as fw: + print(fw) # clear modelnums to ensure deterministic script-like output! gpkit.globals.NamedVariables.reset_modelnumbers() @@ -66,7 +72,6 @@ def test(self): ("model numbers", gpkit.globals.NamedVariables.modelnums), ("lineage", gpkit.NamedVariables.lineage), ("signomials enabled", gpkit.SignomialsEnabled), - ("signomials enabled base", gpkit.SignomialsEnabled._true), # pylint: disable=protected-access ("vectorization", gpkit.Vectorize.vectorization), ("namedvars", gpkit.NamedVariables.namedvars)]: if global_thing: diff --git a/gpkit/tests/t_constraints.py b/gpkit/tests/t_constraints.py index 2aa8464fa..589d0bf87 100644 --- a/gpkit/tests/t_constraints.py +++ b/gpkit/tests/t_constraints.py @@ -1,5 +1,6 @@ """Unit tests for Constraint, MonomialEquality and SignomialInequality""" import unittest +import sys from gpkit import Variable, SignomialsEnabled, Posynomial, VectorVariable from gpkit.nomials import SignomialInequality, PosynomialInequality from gpkit.nomials import MonomialEquality @@ -39,9 +40,10 @@ def test_bad_elements(self): v = VectorVariable(2, "v") with self.assertRaises(ValueError): _ = Model(x, [v == "A"]) - with self.assertRaises(ValueError): + err = TypeError if sys.version_info >= (3, 0) else ValueError + with self.assertRaises(err): _ = Model(x, [v <= ["A", "B"]]) - with self.assertRaises(ValueError): + with self.assertRaises(err): _ = Model(x, [v >= ["A", "B"]]) def test_evalfn(self): diff --git a/gpkit/tests/t_examples.py b/gpkit/tests/t_examples.py index b33e9e836..254050394 100644 --- a/gpkit/tests/t_examples.py +++ b/gpkit/tests/t_examples.py @@ -2,7 +2,7 @@ import unittest import os import numpy as np -import cPickle as pickle +import pickle from gpkit import settings from gpkit.tests.helpers import generate_example_tests @@ -112,11 +112,11 @@ def test_vectorize(self, example): def test_primal_infeasible_ex1(self, example): with self.assertRaises(RuntimeWarning) as cm: example.m.solve(verbosity=0) - err = cm.exception - if "mosek" in err.message: - self.assertIn("PRIM_INFEAS_CER", err.message) - elif "cvxopt" in err.message: - self.assertIn("unknown", err.message) + err = str(cm.exception) + if "mosek" in err: + self.assertIn("PRIM_INFEAS_CER", err) + elif "cvxopt" in err: + self.assertIn("unknown", err) def test_primal_infeasible_ex2(self, example): with self.assertRaises(RuntimeWarning): diff --git a/gpkit/tests/t_keydict.py b/gpkit/tests/t_keydict.py index 1b86ef3bc..bac6e13c1 100644 --- a/gpkit/tests/t_keydict.py +++ b/gpkit/tests/t_keydict.py @@ -43,7 +43,7 @@ def test_dictlike(self): kd = KeyDict() kd["a string key"] = "a string value" self.assertTrue(isinstance(kd, dict)) - self.assertEqual(kd.keys(), ["a string key"]) + self.assertEqual(list(kd.keys()), ["a string key"]) def test_vector(self): v = VectorVariable(3, "v") diff --git a/gpkit/tests/t_model.py b/gpkit/tests/t_model.py index 9bf245bda..b259eae81 100644 --- a/gpkit/tests/t_model.py +++ b/gpkit/tests/t_model.py @@ -1,5 +1,6 @@ """Tests for GP and SP classes""" import unittest +import sys import numpy as np from gpkit import (Model, Monomial, settings, VectorVariable, Variable, SignomialsEnabled, ArrayVariable, SignomialEquality) @@ -60,7 +61,7 @@ def test_sigeq(self): SignomialEquality(x**2 + x, y)]) sol = m.localsolve(solver=self.solver, verbosity=0, mutategp=False) self.assertAlmostEqual(sol("x"), 0.1639472, self.ndig) - self.assertAlmostEqual(sol("y"), 0.1908254, self.ndig) + self.assertAlmostEqual(sol("y")[0], 0.1908254, self.ndig) self.assertAlmostEqual(sol("c"), 0.2669448, self.ndig) # test right vector input to sigeq with SignomialsEnabled(): @@ -68,7 +69,7 @@ def test_sigeq(self): SignomialEquality(y, x**2 + x)]) sol = m.localsolve(solver=self.solver, verbosity=0) self.assertAlmostEqual(sol("x"), 0.1639472, self.ndig) - self.assertAlmostEqual(sol("y"), 0.1908254, self.ndig) + self.assertAlmostEqual(sol("y")[0], 0.1908254, self.ndig) self.assertAlmostEqual(sol("c"), 0.2669448, self.ndig) # test scalar input to sigeq y = Variable("y") @@ -372,8 +373,7 @@ def test_sp_substitutions(self): y = Variable('y', 1) z = Variable('z', 4) - import sys - from cStringIO import StringIO + from io import StringIO old_stdout = sys.stdout sys.stdout = stringout = StringIO() @@ -401,8 +401,7 @@ def test_tautological(self): y = Variable('y') z = Variable('z') - import sys - from cStringIO import StringIO + from io import StringIO old_stdout = sys.stdout sys.stdout = stringout = StringIO() @@ -685,7 +684,7 @@ class Box(Model): w, d, h """ def setup(self): - exec parse_variables(Box.__doc__) + exec(parse_variables(Box.__doc__)) return [V == h*w*d] @@ -722,11 +721,15 @@ def test_modelcontainmentprinting(self): def test_no_naming_on_var_access(self): # make sure that analysis models don't add their names to # variables looked up from other models - box = Box() - area_bounds = BoxAreaBounds(box) - M = Model(box["V"], [box, area_bounds]) - for var in ("h", "w", "d"): - self.assertEqual(len(M.variables_byname(var)), 1) + if sys.version_info >= (3, 0): + with self.assertRaises(FutureWarning): + box = Box() + else: + box = Box() + area_bounds = BoxAreaBounds(box) + M = Model(box["V"], [box, area_bounds]) + for var in ("h", "w", "d"): + self.assertEqual(len(M.variables_byname(var)), 1) TESTS = [TestModelSolverSpecific, TestModelNoSolve] diff --git a/gpkit/tests/t_sub.py b/gpkit/tests/t_sub.py index a5e010be6..d104ab71f 100644 --- a/gpkit/tests/t_sub.py +++ b/gpkit/tests/t_sub.py @@ -41,32 +41,6 @@ def test_numeric(self): self.assertEqual(p.sub({x.key: 3}), 9) self.assertEqual(p.sub({"x": 3}), 9) - def test_basic(self): - """Basic substitution, symbolic""" - x = Variable('x') - y = Variable('y') - p = 1 + x**2 - q = p.sub({x: y**2}) - self.assertEqual(q, 1 + y**4) - self.assertEqual(x.sub({x: y}), y) - - def test_scalar_units(self): - x = Variable("x", "m") - xvk = x.key - y = Variable("y", "km") - yvk = y.key - units_exist = bool(x.units) - for x_ in ["x", xvk, x]: - for y_ in [yvk, y]: - if not isinstance(y_, str) and units_exist: - expected = 1000.0 - else: - expected = 1.0 - self.assertAlmostEqual(expected, mag(x.sub({x_: y_}).c)) - if units_exist: - z = Variable("z", "s") - self.assertRaises(ValueError, y.sub, {y: z}) - def test_dimensionless_units(self): x = Variable('x', 3, 'ft') y = Variable('y', 1, 'm') @@ -74,12 +48,6 @@ def test_dimensionless_units(self): # units are enabled self.assertAlmostEqual((x/y).value, 0.9144) - def test_unitless_monomial_sub(self): - "Tests that dimensionless and undimensioned subs can interact." - x = Variable("x", "-") - y = Variable("y") - self.assertEqual(x.sub({x: y}), y) - def test_vector(self): x = Variable("x") y = Variable("y") @@ -100,14 +68,11 @@ def test_variable(self): y = Variable('y') m = x*y**2 self.assertEqual(x.sub(3), 3) - self.assertEqual(x.sub(y), y) - self.assertEqual(x.sub(m), m) # make sure x was not mutated self.assertEqual(x, Variable('x')) self.assertNotEqual(x.sub(3), Variable('x')) # also make sure the old way works self.assertEqual(x.sub({x: 3}), 3) - self.assertEqual(x.sub({x: y}), y) # and for vectors xvec = VectorVariable(3, 'x') self.assertEqual(xvec[1].sub(3), 3) @@ -230,7 +195,6 @@ def test_vector_sweep(self): m.substitutions.update({y: ('sweep', [[2, 3], [5, 7], [9, 11]])}) a = m.solve(verbosity=0)["cost"] b = [6, 15, 27, 14, 35, 63, 22, 55, 99] - # below line fails with changing dictionary keys in py3 self.assertTrue(all(abs(a-b)/(a+b) < 1e-7)) x_min = Variable("x_min", 1) # constant to check array indexing m = Model(x, [x >= y.prod(), x >= x_min]) diff --git a/gpkit/tests/t_tools.py b/gpkit/tests/t_tools.py index 77b8ff704..3e4ae0945 100644 --- a/gpkit/tests/t_tools.py +++ b/gpkit/tests/t_tools.py @@ -1,5 +1,6 @@ """Tests for tools module""" import unittest +import sys import numpy as np from numpy import log from gpkit import Variable, VectorVariable, Model, NomialArray @@ -22,7 +23,7 @@ class OnlyVectorParse(Model): x [-] just another variable """ def setup(self): - exec parse_variables(OnlyVectorParse.__doc__) # pylint: disable=exec-used + exec(parse_variables(OnlyVectorParse.__doc__)) # pylint: disable=exec-used class Fuselage(Model): @@ -51,7 +52,7 @@ class Fuselage(Model): # pylint: disable=undefined-variable, exec-used, invalid-name def setup(self, Wfueltot): - exec parse_variables(self.__doc__) + exec(parse_variables(self.__doc__)) return [ f == l/R/2, k >= 1 + 60/f**3 + f/400, @@ -67,14 +68,21 @@ class TestTools(unittest.TestCase): def test_vector_only_parse(self): # pylint: disable=no-member - m = OnlyVectorParse() - self.assertTrue(hasattr(m, "x")) - self.assertIsInstance(m.x, NomialArray) - self.assertEqual(len(m.x), 3) - + if sys.version_info >= (3, 0): + with self.assertRaises(FutureWarning): + m = OnlyVectorParse() + else: + m = OnlyVectorParse() + self.assertTrue(hasattr(m, "x")) + self.assertIsInstance(m.x, NomialArray) + self.assertEqual(len(m.x), 3) def test_parse_variables(self): - Fuselage(Variable("Wfueltot", 5, "lbf")) + if sys.version_info >= (3, 0): + with self.assertRaises(FutureWarning): + Fuselage(Variable("Wfueltot", 5, "lbf")) + else: + Fuselage(Variable("Wfueltot", 5, "lbf")) def test_binary_sweep_tree(self): bst0 = BinarySweepTree([1, 2], [{"cost": 1}, {"cost": 8}], None, None) diff --git a/gpkit/tests/test_repo.py b/gpkit/tests/test_repo.py index 6ba7d4f8c..9e8d8f4c2 100644 --- a/gpkit/tests/test_repo.py +++ b/gpkit/tests/test_repo.py @@ -1,4 +1,5 @@ "Implements tests for all external repositories." +from __future__ import print_function import os import sys import subprocess @@ -15,10 +16,10 @@ def test_repo(repo=".", xmloutput=False): """ os.chdir(repo) settings = get_settings() - print - print "SETTINGS" - print settings - print + print("") + print("SETTINGS") + print(settings) + print("") if repo == "." and not os.path.isdir("gpkitmodels"): git_clone("gplibrary") @@ -61,7 +62,7 @@ def test_repos(repos=None, xmloutput=False, ingpkitmodels=False): repos_list_filename = "gplibrary"+os.sep+"EXTERNALTESTS" pip_install("gplibrary", local=True) else: - print "USING LOCAL DIRECTORY AS GPKITMODELS DIRECTORY" + print("USING LOCAL DIRECTORY AS GPKITMODELS DIRECTORY") repos_list_filename = "EXTERNALTESTS" pip_install(".", local=True) repos = [line.strip() for line in open(repos_list_filename, "r")] @@ -107,10 +108,10 @@ def call_and_retry(cmd, max_iterations=5, delay=5): "Tries max_iterations times (waiting d each time) to run a command" iterations = 0 return_code = None - print "calling", cmd + print("calling", cmd) while return_code != 0 and iterations < max_iterations: iterations += 1 - print " attempt", iterations + print(" attempt", iterations) return_code = subprocess.call(cmd) sleep(delay) return return_code diff --git a/gpkit/tools/autosweep.py b/gpkit/tools/autosweep.py index 642467ecd..e11a313b6 100644 --- a/gpkit/tools/autosweep.py +++ b/gpkit/tools/autosweep.py @@ -1,4 +1,5 @@ "Tools for optimal fits to GP sweeps" +from __future__ import print_function from time import time import numpy as np from ..small_classes import Count @@ -83,7 +84,7 @@ def posy_at(self, posy, value): bst = self.min_bst(value) lo, hi = bst.bounds loval, hival = [sol(posy) for sol in bst.sols] - lo, hi, loval, hival = np.log(map(mag, [lo, hi, loval, hival])) + lo, hi, loval, hival = np.log(list(map(mag, [lo, hi, loval, hival]))) interp = (hi-np.log(value))/float(hi-lo) return np.exp(interp*loval + (1-interp)*hival) @@ -109,7 +110,7 @@ def cost_at(self, _, value, bound=None): else: lo, hi = bst.bounds loval, hival = [sol["cost"] for sol in bst.sols] - lo, hi, loval, hival = np.log(map(mag, [lo, hi, loval, hival])) + lo, hi, loval, hival = np.log(list(map(mag, [lo, hi, loval, hival]))) interp = (hi-np.log(value))/float(hi-lo) return np.exp(interp*loval + (1-interp)*hival) @@ -157,7 +158,7 @@ def save(self, filename="autosweep.p"): >>> import cPickle as pickle >>> pickle.load(open("autosweep.p")) """ - import cPickle as pickle + import pickle programs = [] costs = [] for sol in self.sollist: @@ -165,7 +166,7 @@ def save(self, filename="autosweep.p"): sol.program = None costs.append(sol["cost"]) sol["cost"] = mag(sol["cost"]) - pickle.dump(self, open(filename, "w")) + pickle.dump(self, open(filename, "wb")) for i, sol in enumerate(self.sollist): sol["cost"] = costs[i] sol.program = programs[i] diff --git a/gpkit/tools/docstring.py b/gpkit/tools/docstring.py index f02fc2d10..c3b429673 100644 --- a/gpkit/tools/docstring.py +++ b/gpkit/tools/docstring.py @@ -1,6 +1,7 @@ "Docstring-parsing methods" import numpy as np import re +import sys def expected_unbounded(instance, doc): @@ -58,6 +59,8 @@ def expected_unbounded(instance, doc): def parse_variables(string, errorcatch=True): "Parses a string to determine what variables to create from it" + if sys.version_info >= (3, 0): + raise FutureWarning("parse_variables is not yet supported in Python 3") out = "from gpkit import Variable, VectorVariable\n" out += check_and_parse_flag(string, "Constants\n", errorcatch, constant_declare) @@ -141,7 +144,7 @@ def variable_declaration(nameval, units, label, line, errorcatch=True): out = """ try: {0} -except Exception, e: +except Exception as e: raise ValueError("`"+e.__class__.__name__+": "+str(e)+"` was raised" " while executing the parsed line `{0}`. {1}") """.format(out, PARSETIP) diff --git a/gpkit/varkey.py b/gpkit/varkey.py index 98cbf04ff..f5d6ca62d 100644 --- a/gpkit/varkey.py +++ b/gpkit/varkey.py @@ -87,7 +87,7 @@ def __getattr__(self, attr): @property def models(self): "Returns a tuple of just the names of models in self.lineage" - return zip(*self.lineage)[0] + return list(zip(*self.lineage))[0] def latex_unitstr(self): "Returns latex unitstr" diff --git a/setup.py b/setup.py index 20eecb39c..19e5531a8 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ LICENSE = """The MIT License (MIT) -Copyright (c) 2018 Edward Burnell +Copyright (c) 2019 Edward Burnell Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -53,8 +53,8 @@ author_email="gpkit@mit.edu", url="https://www.github.com/convexengineering/gpkit", install_requires=["numpy >= 1.12.1", "pint >= 0.8.1", "scipy", "ad", - "ctypesgen", "cvxopt >= 1.1.8"], - version="0.8.0.0", + "cvxopt >= 1.1.8", "six"], + version="0.9.0.1", packages=["gpkit", "gpkit.tools", "gpkit.interactive", "gpkit.constraints", "gpkit.nomials", "gpkit.tests", "gpkit._mosek", "gpkit._pint"], package_data={"gpkit": ["env/settings"],