Skip to content

Commit

Permalink
Merge pull request #208 from jonls/solver-float
Browse files Browse the repository at this point in the history
Fix issues with non-float numbers provided to solver
  • Loading branch information
jonls committed Feb 27, 2017
2 parents 16e1888 + 84634d5 commit 2e75025
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 21 deletions.
6 changes: 3 additions & 3 deletions psamm/lpsolver/cplex.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ def set_objective(self, expression):
for var, value in expression.values():
if not isinstance(var, Product):
self._non_zero_objective.add(var)
linear.append((self._variables[var], value))
linear.append((self._variables[var], float(value)))
else:
if len(var) > 2:
raise ValueError('Invalid objective: {}'.format(var))
Expand All @@ -254,7 +254,7 @@ def set_objective(self, expression):
var2 = self._variables[var[1]]
if var1 == var2:
value *= 2
quad.append((var1, var2, value))
quad.append((var1, var2, float(value)))

# We have to build the set of variables to
# update so that we can avoid calling set_linear if the set is empty.
Expand All @@ -266,7 +266,7 @@ def set_objective(self, expression):
self._cp.objective.set_quadratic_coefficients(quad)

if hasattr(self._cp.objective, 'set_offset'):
self._cp.objective.set_offset(expression.offset)
self._cp.objective.set_offset(float(expression.offset))

set_linear_objective = set_objective
"""Set objective of the problem.
Expand Down
26 changes: 11 additions & 15 deletions psamm/lpsolver/glpk.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,9 +253,9 @@ def set_objective(self, expression):

for variable, value in expression.values():
var_index = self._variables[variable]
swiglpk.glp_set_obj_coef(self._p, var_index, value)
swiglpk.glp_set_obj_coef(self._p, var_index, float(value))

swiglpk.glp_set_obj_coef(self._p, 0, expression.offset)
swiglpk.glp_set_obj_coef(self._p, 0, float(expression.offset))

set_linear_objective = set_objective
"""Set objective of the problem.
Expand Down Expand Up @@ -408,21 +408,19 @@ def status(self):
return 'No dual feasible solution'
return str(swiglpk.glp_get_status(self._problem._p))

def _get_var_value(self, variable):
self._check_valid()
if variable not in self._problem._variables:
raise ValueError('Unknown variable: {}'.format(variable))
def _has_variable(self, variable):
"""Whether variable exists in the solution."""
return self._problem.has_variable(variable)

def _get_value(self, variable):
"""Return value of variable in solution."""
return swiglpk.glp_get_col_prim(
self._problem._p, self._problem._variables[variable])

def get_value(self, expression):
"""Return value of expression."""

self._check_valid()
if isinstance(expression, Expression):
return sum(self._get_var_value(var) * value
for var, value in expression.values())
return self._get_var_value(expression)
return super(Result, self).get_value(expression)


class MIPResult(Result):
Expand All @@ -438,9 +436,7 @@ def status(self):
self._check_valid()
return str(swiglpk.glp_mip_status(self._problem._p))

def _get_var_value(self, variable):
self._check_valid()
if variable not in self._problem._variables:
raise ValueError('Unknown variable: {}'.format(variable))
def _get_value(self, variable):
"""Return value of variable in solution."""
return swiglpk.glp_mip_col_val(
self._problem._p, self._problem._variables[variable])
15 changes: 14 additions & 1 deletion psamm/lpsolver/lp.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
from collections import Counter, defaultdict
import abc
import enum
from fractions import Fraction
from decimal import Decimal

import six
from six import add_metaclass, iteritems, viewkeys, viewitems, text_type
Expand Down Expand Up @@ -755,8 +757,19 @@ def _has_variable(self, var):

def _evaluate_expression(self, expr):
"""Evaluate an :class:`.Expression` using :meth:`_get_value`."""
total = expr.offset
def cast_value(v):
# Convert Decimal to Fraction to allow successful multiplication
# by either float (most solvers) or Fraction (exact solver).
# Multiplying Fraction and float results in a float, and
# multiplying Fraction and Fraction result in Fraction, which are
# exactly the types of results we want.
if isinstance(v, Decimal):
return Fraction(v)
return v

total = cast_value(expr.offset)
for var, value in expr.values():
value = cast_value(value)
if not isinstance(var, Product):
total += self._get_value(var) * value
else:
Expand Down
40 changes: 38 additions & 2 deletions psamm/tests/test_lpsolver_generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@
# You should have received a copy of the GNU General Public License
# along with PSAMM. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright 2015 Jon Lund Steffensen <jon_steffensen@uri.edu>
# Copyright 2015-2017 Jon Lund Steffensen <jon_steffensen@uri.edu>

import os
import unittest
from decimal import Decimal
from fractions import Fraction

from psamm.lpsolver import generic
from psamm.lpsolver import generic, lp


class TestSolverProblem(unittest.TestCase):
Expand All @@ -34,6 +36,40 @@ def setUp(self):
except generic.RequirementsError:
self.skipTest('Unable to find an LP solver for tests')

def test_set_decimal_variable_bounds(self):
"""Test that Decimal can be used for variable bounds."""
prob = self.solver.create_problem()
prob.define('x', lower=Decimal('3.5'), upper=Decimal('500.3'))

def test_set_decimal_objective(self):
"""Test that Decimal can be used in objective."""
prob = self.solver.create_problem()
prob.define('x', lower=3, upper=100)
prob.set_objective(Decimal('3.4') * prob.var('x'))

def test_set_fraction_objective(self):
"""Test that Fraction can be used in objective."""
prob = self.solver.create_problem()
prob.define('x', lower=-5, upper=5)
prob.set_objective(Fraction(1, 2) * prob.var('x'))

def test_set_decimal_constraint(self):
"""Test that Decimal can be used in constraints."""
prob = self.solver.create_problem()
prob.define('x', lower=0, upper=5)
prob.add_linear_constraints(
Decimal('5.6') * prob.var('x') >= Decimal('8.9'))

def test_result_evaluate_decimal_expression(self):
"""Test that Decimal in expression can be used to evaluate result."""
prob = self.solver.create_problem()
prob.define('x', lower=0, upper=Fraction(5, 2))
prob.add_linear_constraints(prob.var('x') >= 2)
prob.set_objective(prob.var('x'))
result = prob.solve(lp.ObjectiveSense.Maximize)
value = result.get_value(Decimal('3.4') * prob.var('x'))
self.assertAlmostEqual(value, Fraction(17, 2))

def test_redefine_variable(self):
"""Test that redefining a variable fails."""
prob = self.solver.create_problem()
Expand Down

0 comments on commit 2e75025

Please sign in to comment.