Repository: hoburg/gpkit (python)

Issue:
Variable and Varkey __eq__ disagree
These should be symmetric:

```python
In [1]: from gpkit import Variable

In [2]: W = Variable("W", 5, "lbf", "weight of 1 bag of sugar")

In [3]: W.key == W
Out[3]: True

In [4]: W == W.key
Out[4]: False
```

Pull request description:
Symmetric Variable / VarKey ==
Closes #1138 

@whoburg ready for review

Patch:
diff --git a/gpkit/nomials/variables.py b/gpkit/nomials/variables.py
index 496f5a7..26d2fc8 100644
--- a/gpkit/nomials/variables.py
+++ b/gpkit/nomials/variables.py
@@ -67,11 +67,6 @@ class Variable(Monomial):
         """Get the VarKey associated with this Variable"""
         return list(self.exp)[0]
 
-    @property
-    def descr(self):
-        "a Variable's descr is derived from its VarKey."
-        return self.key.descr
-
     def to(self, arg):
         "Create new Signomial converted to new units"
          # pylint: disable=no-member
diff --git a/gpkit/tools/fmincon.py b/gpkit/tools/fmincon.py
index e8aa3c5..185c0d8 100644
--- a/gpkit/tools/fmincon.py
+++ b/gpkit/tools/fmincon.py
@@ -1,21 +1,25 @@
 "A module to facilitate testing GPkit against fmincon"
-from math import log10, floor
+from math import log10, floor, log
 from .. import SignomialsEnabled
+from ..keydict import KeyDict
 from ..small_scripts import mag
-# pylint: disable=too-many-statements,too-many-locals
+# pylint: disable=too-many-statements,too-many-locals,too-many-branches
 
-def generate_mfiles(model, algorithm='interior-point', guesstype='ones',
-                    gradobj='on', gradconstr='on', writefiles=True):
+def generate_mfiles(model, logspace=False, algorithm='interior-point',
+                    guess='ones', gradobj='on', gradconstr='on',
+                    writefiles=True):
     """A method for preparing fmincon input files to run a GPkit program
 
     INPUTS:
         model       [GPkit model] The model to replicate in fmincon
 
+        logspace    [Boolean] Whether to re-produce the model in logspace
+
         algorithm:  [string] Algorithm used by fmincon
                     'interior-point': uses the interior point solver
                     'SQP': uses the sequential quadratic programming solver
 
-        guesstype:  [string] The type of initial guess used
+        guess:      [string] The type of initial guess used
                     'ones': One for each variable
                     'order-of-magnitude-floor': The "log-floor" order of
                                                 magnitude of the GP/SP optimal
@@ -25,6 +29,8 @@ def generate_mfiles(model, algorithm='interior-point', guesstype='ones',
                                                 solution (i.e. O(42)=100)
                     'almost-exact-solution': The GP/SP optimal solution rounded
                                              to 1 significant figure
+                    OR
+                    [list] The actual values of initial guess to use
 
         gradconstr: [string] Include analytical constraint gradients?
                     'on': Yes
@@ -36,6 +42,9 @@ def generate_mfiles(model, algorithm='interior-point', guesstype='ones',
 
         writefiles: [Boolean] whether or not to actually write the m files
     """
+    if logspace: # Supplying derivatives not supported for logspace
+        gradobj = 'off'
+        gradconstr = 'off'
 
     # Create a new dictionary mapping variables to x(i)'s for use w/ fmincon
     i = 1
@@ -46,44 +55,68 @@ def generate_mfiles(model, algorithm='interior-point', guesstype='ones',
     for key in model.varkeys:
         if key not in model.substitutions:
             newdict[key] = 'x({0})'.format(i)
+            if logspace:
+                newdict[key].replace('x', 'y')
             newlist += [key.str_without(["units"])]
             lookup += ['x_{0}: '.format(i) + key.str_without(["units"])]
             i += 1
-    x0string = make_initial_guess(model, newlist, guesstype)
+    x0string = make_initial_guess(model, newlist, guess, logspace)
 
     cost = model.cost # needs to be before subinplace()
     constraints = model
-    constraints.subinplace(constraints.substitutions)
+    substitutions = constraints.substitutions
+    constraints.substitutions = KeyDict()
+    constraints.subinplace(substitutions)
     constraints.subinplace(newdict)
+    constraints.substitutions = substitutions
 
     # Make all constraints less than zero, return list of clean strings
     c = [] # inequality constraints
     ceq = [] # equality constraints
     dc = [] # gradients of inequality constraints
     dceq = [] # gradients of equality constraints
-    with SignomialsEnabled():
+
+    if logspace:
         for constraint in constraints:
-            if constraint.oper == '<=':
-                cc = constraint.left - constraint.right
-                c += [cc.str_without(["units", "models"])]
-            elif constraint.oper == '>=':
-                cc = constraint.right - constraint.left
-                c += [cc.str_without(["units", "models"])]
-            elif constraint.oper == '=':
-                cc = constraint.right - constraint.left
-                ceq += [cc.str_without(["units", "models"])]
-
-            # Differentiate each constraint w.r.t each variable
-            cdm = []
-            for key in original_varkeys:
-                if key not in model.substitutions:
-                    cd = cc.diff(newdict[key])
-                    cdm += [cd.str_without("units").replace('**', '.^')]
-
-            if constraint.oper != '=':
-                dc += [",...\n          ".join(cdm)]
+            expdicttuple = constraint.as_posyslt1()[0].exps
+            clist = mag(constraint.as_posyslt1()[0].cs)
+
+            constraintstring = ['log(']
+            for expdict, C in zip(expdicttuple, clist):
+                constraintstring += ['+ {0}*exp('.format(C)]
+                for k, v in expdict.iteritems():
+                    constraintstring += ['+{0} * {1}'.format(v, k)]
+                constraintstring += [')']
+            constraintstring += [')']
+
+            if constraint.oper == '=':
+                ceq += [' '.join(constraintstring)]
             else:
-                dceq += [",...\n            ".join(cdm)]
+                c += [' '.join(constraintstring)]
+    else:
+        with SignomialsEnabled():
+            for constraint in constraints:
+                if constraint.oper == '<=':
+                    cc = constraint.left - constraint.right
+                    c += [cc.str_without(["units", "models"])]
+                elif constraint.oper == '>=':
+                    cc = constraint.right - constraint.left
+                    c += [cc.str_without(["units", "models"])]
+                elif constraint.oper == '=':
+                    cc = constraint.right - constraint.left
+                    ceq += [cc.str_without(["units", "models"])]
+
+                # Differentiate each constraint w.r.t each variable
+                cdm = []
+                for key in original_varkeys:
+                    if key not in model.substitutions:
+                        cd = cc.diff(newdict[key])
+                        cdm += [cd.str_without("units").replace('**', '.^')]
+
+                if constraint.oper != '=':
+                    dc += [",...\n          ".join(cdm)]
+                else:
+                    dceq += [",...\n            ".join(cdm)]
 
     # String for the constraint function .m file
     confunstr = ("function [c, ceq, DC, DCeq] = confun(x)\n" +
@@ -103,18 +136,31 @@ def generate_mfiles(model, algorithm='interior-point', guesstype='ones',
                  "\n           ]';\n" +
                  "end")
 
-    # Differentiate the objective function w.r.t each variable
+    # Objective function (and derivatives if applicable)
+    cost.subinplace(newdict)
     objdiff = []
-    for key in original_varkeys:
-        if key not in model.substitutions:
-            costdiff = cost.diff(key)
-            costdiff.subinplace(newdict)
-            objdiff += [costdiff.str_without(["units", "models"]).replace('**',
-                                                                          '.^')]
+    if logspace:
+        objstring = ['log(']
+        expdicttuple = cost.exps
+        clist = mag(cost.cs)
+        for expdict, cc in zip(expdicttuple, clist):
+            objstring += ['+ {0}*exp('.format(cc)]
+            for k, v in expdict.iteritems():
+                objstring += ['+{0} * {1}'.format(v, k)]
+            objstring += [')']
+        objstring += [')']
+        obj = ' '.join(objstring)
+    else:
+        # Differentiate the objective function w.r.t each variable
+        for key in original_varkeys:
+            if key not in model.substitutions:
+                costdiff = cost.diff(key)
+                costdiff.subinplace(newdict)
+                objdiff += [costdiff.str_without(["units", "models"]).replace(
+                    '**', '.^')]
 
-    # Replace variables with x(i), make clean string using matlab power syntax
-    cost.subinplace(newdict)
-    obj = cost.str_without(["units", "models"]).replace('**', '.^')
+        # Replace variables with x(i), make clean string using matlab power syn.
+        obj = cost.str_without(["units", "models"]).replace('**', '.^')
 
     # String for the objective function .m file
     objfunstr = ("function [f, gradf] = objfun(x)\n" +
@@ -126,6 +172,7 @@ def generate_mfiles(model, algorithm='interior-point', guesstype='ones',
                  "end")
 
     # String for main.m
+    fval = "exp(fval)" if logspace else "fval"
     mainfunstr = (x0string +
                   "options = optimset('fmincon');\n" +
                   "options.Algorithm = '{0}';\n".format(algorithm) +
@@ -134,14 +181,18 @@ def generate_mfiles(model, algorithm='interior-point', guesstype='ones',
                   "options.GradObj = '{0}';\n".format(gradobj) +
                   "options.GradConstr = '{0}';\n".format(gradconstr) +
                   "tic;\n" +
-                  "[x,fval] = ...\n" +
+                  "[x,fval, exitflag, output] = ...\n" +
                   "fmincon(@objfun,x0,[],[],[],[],[],[],@confun,options);\n" +
                   "elapsed = toc;\n" +
                   "fid = fopen('elapsed.txt', 'w');\n" +
                   "fprintf(fid, '%.1f', elapsed);\n" +
                   "fclose(fid);\n" +
+                  "fid = fopen('iterations.txt', 'w');\n" +
+                  "fprintf(fid, '%d', output.iterations);\n" +
+                  "fclose(fid);\n" +
                   "fid = fopen('cost.txt', 'w');\n" +
-                  "fprintf(fid, '%.5g', fval);\n" +
+                  "fprintf(fid, '%.5g', {0});\n".format(fval) +
+                  "if exitflag == -2\n\tfprintf(fid, '(i)');\nend\n" +
                   "fclose(fid);")
 
     if writefiles:
@@ -164,27 +215,37 @@ def generate_mfiles(model, algorithm='interior-point', guesstype='ones',
     return obj, c, ceq, dc, dceq
 
 
-def make_initial_guess(model, newlist, guesstype='ones'):
+def make_initial_guess(model, newlist, guess='ones', logspace=False):
     """Returns initial guess"""
     try:
         sol = model.solve(verbosity=0)
     except TypeError:
         sol = model.localsolve(verbosity=0)
-    if guesstype == "ones":
-        x0string = ["x0 = ones({0},1);\n".format(len(sol['freevariables']))]
+
+    if guess == "ones":
+        nvars = len(sol['freevariables'])
+        if logspace:
+            x0string = ["x0 = zeros({0},1);\n".format(nvars)]
+        else:
+            x0string = ["x0 = ones({0},1);\n".format(nvars)]
     else:
         x0string = ["x0 = ["]
         i = 1
         for vk in newlist:
             xf = mag(sol['freevariables'][vk])
-            if guesstype == "almost-exact-solution":
+            if guess == "almost-exact-solution":
                 x0 = round(xf, -int(floor(log10(abs(xf))))) # rounds to 1sf
-            elif guesstype == "order-of-magnitude-floor":
+            elif guess == "order-of-magnitude-floor":
                 x0 = 10**floor(log10(xf))
-            elif guesstype == "order-of-magnitude-round":
+            elif guess == "order-of-magnitude-round":
                 x0 = 10**round(log10(xf))
+            elif isinstance(guess, list):
+                x0 = guess[i-1]
             else:
                 raise Exception("Unexpected guess type")
+
+            if logspace:
+                x0 = log(x0)
             x0string += [str(x0) + ", "]
             i += 1
         x0string += ["];\n"]
diff --git a/gpkit/tools/tools.py b/gpkit/tools/tools.py
index 5da9481..a3131ee 100644
--- a/gpkit/tools/tools.py
+++ b/gpkit/tools/tools.py
@@ -138,7 +138,7 @@ def composite_objective(*objectives, **kwargs):
     ws = VectorVariable(n-1, "w_{CO}", sweepvals, "-")
     w_s = []
     for w in ws:
-        descr = dict(w.descr)
+        descr = dict(w.key.descr)
         del descr["value"]
         descr["name"] = "v_{CO}"
         w_s.append(Variable(value=lambda const: 1-const[w.key], **descr))  # pylint: disable=cell-var-from-loop


Test patch:
diff --git a/gpkit/tests/t_model.py b/gpkit/tests/t_model.py
index 8eff3dc..8171f6f 100644
--- a/gpkit/tests/t_model.py
+++ b/gpkit/tests/t_model.py
@@ -275,10 +275,11 @@ class TestSP(unittest.TestCase):
         with SignomialsEnabled():
             m = Model(x, Bounded([x + y >= 1], verbosity=0))
         sol = m.localsolve(verbosity=0)
-        if "value near lower bound" in sol["boundedness"]:
-            self.assertIn(x, sol["boundedness"]["value near lower bound"])
-        if "value near upper bound" in sol["boundedness"]:
-            self.assertIn(y, sol["boundedness"]["value near upper bound"])
+        boundedness = sol["boundedness"]
+        if "value near lower bound" in boundedness:
+            self.assertEqual(x.key, boundedness["value near lower bound"][0])
+        if "value near upper bound" in boundedness:
+            self.assertEqual(y.key, boundedness["value near upper bound"][0])
 
     def test_values_vs_subs(self):
         # Substitutions update method
diff --git a/gpkit/tests/t_tools.py b/gpkit/tests/t_tools.py
index dfe3538..a2a2834 100644
--- a/gpkit/tests/t_tools.py
+++ b/gpkit/tests/t_tools.py
@@ -101,6 +101,21 @@ class TestTools(unittest.TestCase):
                               '-3.2*x(2).^2.2', '0,...\n          -1'])
         self.assertEqual(DCeq, ['-1,...\n            0'])
 
+    def test_fmincon_generator_logspace(self):
+        "Test fmincon comparison tool (logspace)"
+        x = Variable('x')
+        y = Variable('y')
+        m = Model(x, [x**3.2 >= 17*y + y**-0.2,
+                      x >= 2,
+                      y == 4])
+        obj, c, ceq, _, _ = generate_mfiles(m, writefiles=False, logspace=True)
+        self.assertEqual(c, [('log( + 1.0*exp( +-3.2 * x(2) +-0.2 * x(1) ) + ' +
+                              '17.0*exp( +-3.2 * x(2) +1 * x(1) ) )'),
+                             'log( + 2.0*exp( +-1 * x(2) ) )'])
+        self.assertEqual(obj, 'log( + 1.0*exp( +1 * x(2) ) )')
+        self.assertEqual(ceq, ['log( + 0.25*exp( +1 * x(1) ) )'])
+
+
 TESTS = [TestTools]
 
 
diff --git a/gpkit/tests/t_vars.py b/gpkit/tests/t_vars.py
index 5ad2eee..8dbe18e 100644
--- a/gpkit/tests/t_vars.py
+++ b/gpkit/tests/t_vars.py
@@ -124,6 +124,14 @@ class TestVariable(unittest.TestCase):
             x = Variable("x", "ft")
             self.assertEqual(x.to("inch").c.magnitude, 12)
 
+    def test_eq_ne(self):
+        # test for #1138
+        W = Variable("W", 5, "lbf", "weight of 1 bag of sugar")
+        self.assertTrue(W != W.key)
+        self.assertTrue(W.key != W)
+        self.assertFalse(W == W.key)
+        self.assertFalse(W.key == W)
+
 
 class TestVectorVariable(unittest.TestCase):
     """TestCase for the VectorVariable class.