def test_to_lp_terms(self, x, y): # 2x + 3y + 7 lhs = LpExpression('lhs', {x: 2, y: 3}, 7) # x + 5y + 2 rhs = LpExpression('rhs', {x: 1, y: 5}, 2) constraint = LpConstraint(lhs, 'leq', rhs, 'test_constraint') assert constraint.to_lp_terms() == ['x', '- 2 y', '<=', '-5']
def test_write_no_objective(self, problem, x): rhs = LpExpression('rhs', {x: 1}) lhs = LpExpression('lhs', {}, -2) constraint = LpConstraint(rhs, 'geq', lhs, 'constraint') problem.add_constraint(constraint) buffer = StringIO() with pytest.raises(Exception) as e: problem.write_lp(buffer) assert e.value.args == ('No objective', )
def test_write(self, problem, x): objective = LpObjective(name='minimize_cpm', expression={x: 998}, constant=8) rhs = LpExpression('rhs', {x: 1}) lhs = LpExpression('lhs', {}, -2) constraint = LpConstraint(rhs, 'geq', lhs, 'constraint') problem.add_constraint(constraint) problem.set_objective(objective) buffer = StringIO() problem.write_lp(buffer) flipy_string = buffer.getvalue() assert flipy_string == '\\* test_problem *\\\nMinimize\nminimize_cpm: 998 x + 8\nSubject To\nconstraint: x >= -2\nBounds\nx <= 10\nEnd'
def test_write_with_empty_constraint(self, problem, x): objective = LpObjective(name='minimize_cpm', expression={x: 998}, constant=8, sense=Maximize) constraint = LpConstraint(LpExpression('lhs', {x: 0}), 'leq', LpExpression('rhs', {}), 'constraint') problem.add_constraint(constraint) problem.set_objective(objective) buffer = StringIO() problem.write_lp(buffer) flipy_string = buffer.getvalue() assert flipy_string == '\\* test_problem *\\\nMaximize\nminimize_cpm: 998 x + 8\nSubject To\nBounds\nx <= 10\nEnd'
def test_write_slack(self, problem, x): objective = LpObjective(name='minimize_cpm', expression={x: 998}, constant=8, sense=Maximize) rhs = LpExpression('rhs', {x: 1}) lhs = LpExpression('lhs', {}, -2) constraint = LpConstraint(rhs, 'leq', lhs, 'constraint', True, 100) problem.add_constraint(constraint) problem.set_objective(objective) buffer = StringIO() problem.write_lp(buffer) flipy_string = buffer.getvalue() assert flipy_string == '\\* test_problem *\\\nMaximize\nminimize_cpm: 998 x - 100 constraint_slack_variable + 8\nSubject To\nconstraint: - constraint_slack_variable + x <= -2\nBounds\nx <= 10\nEnd'
def test__eq__(self, expression, x, y): assert expression == LpExpression(name='text_expr_3', expression={ x: 998, y: 0 }, constant=8.0)
def expression_2(x, y, z): return LpExpression(name='text_expr_2', expression={ x: -5, y: -2, z: 0 }, constant=0)
def breakdown(self) -> List['LpConstraint']: """ Breaks down a equality constraint into an upper bound and a lower bound constraint """ if self.sense != 'eq': return [self] upper_bound_constraint = LpConstraint( name=self.name + '_ub', lhs=LpExpression(expression=copy.copy(self.lhs.expr)), rhs=LpExpression(expression=copy.copy(self.rhs.expr)), sense='leq', slack=self.slack, slack_penalty=self.slack_penalty, ) lower_bound_constraint = self lower_bound_constraint.sense = 'geq' lower_bound_constraint.name += '_lb' return [lower_bound_constraint, upper_bound_constraint]
def test_add_constraint(self, problem, x): rhs = LpExpression('rhs', {x: 1}) lhs = LpExpression('lhs', {x: 1}, 2) constraint = LpConstraint(rhs, 'geq', lhs, 'constraint') problem.add_constraint(constraint) assert problem.lp_constraints[constraint.name] == constraint assert problem.lp_variables[x.name] == x constraint = LpConstraint(lhs, 'geq', rhs, 'constraint') with pytest.raises(Exception) as e: problem.add_constraint(constraint) assert e.value.args == ( 'LP constraint name %s conflicts with an existing LP constraint' % constraint.name, ) with pytest.raises(Exception) as e: problem.add_constraint(10) assert e.value.args == ('%s is not an LpConstraint' % 10, )
def __init__(self, lhs: LpExpression, sense: str, rhs: Optional[LpExpression] = None, name: Optional[str] = None, slack: bool = False, slack_penalty: Numeric = 0, copy_expr: bool = False) -> None: """ Initialize the constraint Parameters ---------- lhs: The left-hand-side expression sense: The type of constraint (leq, geq, or eq) rhs: The right-hand-side expression name: The name of the constraint slack: Whether the constraint has slack slack_penalty: The penalty for the slack in the constraint copy_expr: Whether to copy the lhs and rhs expressions """ self.lhs = lhs if not copy_expr else copy.copy(lhs) if rhs: self.rhs = rhs else: self.rhs = LpExpression() self.sense = sense self.name = name or '' self.slack = slack self.slack_penalty = slack_penalty self._slack_variable = None
def test_write_long(self, problem, x): a = LpVariable('a', low_bound=0, up_bound=10, var_type=VarType.Integer) b = LpVariable('b', low_bound=0, up_bound=10, var_type=VarType.Integer) c = LpVariable('c', low_bound=0, up_bound=10, var_type=VarType.Integer) d = LpVariable('d', low_bound=0, up_bound=10, var_type=VarType.Integer) e = LpVariable('e', var_type=VarType.Binary) f = LpVariable('f', var_type=VarType.Binary) g = LpVariable('g', var_type=VarType.Binary) h = LpVariable('h', var_type=VarType.Binary) vars = [a, b, c, d, e, f, g, h] # make sure objective is long enough to test the line break objective = LpObjective(name='minimize_cpm', expression={v: 3.1415926535 for v in vars}, constant=8) rhs = LpExpression('rhs', {a: 1000, b: 1000, c: 1000, d: 1000}) lhs = LpExpression('lhs', {}, -2) constraint = LpConstraint(rhs, 'geq', lhs, 'constraint') problem.add_constraint(constraint) problem.set_objective(objective) buffer = StringIO() problem.write_lp(buffer) lp_str = buffer.getvalue() assert lp_str.split('\n') == [ '\\* test_problem *\\', 'Minimize', 'minimize_cpm: 3.1415926535 a + 3.1415926535 b + 3.1415926535 c + 3.1415926535 d', '+ 3.1415926535 e + 3.1415926535 f + 3.1415926535 g + 3.1415926535 h + 8', 'Subject To', 'constraint: 1000 a + 1000 b + 1000 c + 1000 d >= -2', 'Bounds', '0 <= a <= 10', '0 <= b <= 10', '0 <= c <= 10', '0 <= d <= 10', '0 <= e <= 1', '0 <= f <= 1', '0 <= g <= 1', '0 <= h <= 1', 'Generals', 'a', 'b', 'c', 'd', 'Binaries', 'e', 'f', 'g', 'h', 'End' ]
def _shift_variables(self): """ Shifts all variables to the left hand side of the constraint, and shifts the constant to the right hand side of the constraints Returns ------- flipy.LpExpression The shfited lhs expression float The shifted rhs constant """ new_expr = LpExpression(expression=copy.copy(self.lhs.expr)) for var, coeff in self.rhs.expr.items(): new_expr.expr[var] -= coeff if self.slack: new_expr.expr[ self.slack_variable] = -1 if self.sense == 'leq' else 1 const = self.rhs.const - self.lhs.const return new_expr, const
def expression(x): return LpExpression(name='test_expr', expression={x: 998}, constant=8)
def lhs(x, y): # x + 3y + 7 return LpExpression('lhs', {x: 1, y: 3}, 7)
def test_init_no_rhs(self, lhs): lp_constraint = LpConstraint(lhs, 'leq') assert lp_constraint.rhs == LpExpression()
def test_init(self): expression = LpExpression('', None, 5) assert expression.name == '' assert type(expression.expr) == defaultdict assert expression.const == 5
class LpConstraint: """ A class representing a linear constraint """ def __init__(self, lhs: LpExpression, sense: str, rhs: Optional[LpExpression] = None, name: Optional[str] = None, slack: bool = False, slack_penalty: Numeric = 0, copy_expr: bool = False) -> None: """ Initialize the constraint Parameters ---------- lhs: The left-hand-side expression sense: The type of constraint (leq, geq, or eq) rhs: The right-hand-side expression name: The name of the constraint slack: Whether the constraint has slack slack_penalty: The penalty for the slack in the constraint copy_expr: Whether to copy the lhs and rhs expressions """ self.lhs = lhs if not copy_expr else copy.copy(lhs) if rhs: self.rhs = rhs else: self.rhs = LpExpression() self.sense = sense self.name = name or '' self.slack = slack self.slack_penalty = slack_penalty self._slack_variable = None def _shift_variables(self): """ Shifts all variables to the left hand side of the constraint, and shifts the constant to the right hand side of the constraints Returns ------- flipy.LpExpression The shfited lhs expression float The shifted rhs constant """ new_expr = LpExpression(expression=copy.copy(self.lhs.expr)) for var, coeff in self.rhs.expr.items(): new_expr.expr[var] -= coeff if self.slack: new_expr.expr[ self.slack_variable] = -1 if self.sense == 'leq' else 1 const = self.rhs.const - self.lhs.const return new_expr, const def _shift_constant_right(self) -> None: """ Moves the constant on the lhs to the rhs """ if not self.lhs.const: return self.rhs.const -= self.lhs.const self.lhs.const = 0 @property def lhs(self) -> LpExpression: """ Getter for lhs expression """ return self._lhs @lhs.setter def lhs(self, lhs_exp: LpExpression) -> None: """ Setter for lhs expression Raises ------ ValueError If `lhs_exp` is not an LpExpression objective Parameters ---------- lhs_exp: The lhs expression to set """ if not isinstance(lhs_exp, LpExpression): raise ValueError('LHS of LpConstraint must be LpExpression') self._lhs = lhs_exp # pylint: disable=W0201 @property def rhs(self) -> LpExpression: """ Getter for rhs expression """ return self._rhs @rhs.setter def rhs(self, rhs_exp: LpExpression) -> None: """ Setter for rhs expression Raises ------ ValueError If `rhs_exp` is not an LpExpression objective Parameters ------- rhs_exp: The rhs expression to set """ if not isinstance(rhs_exp, LpExpression): raise ValueError('RHS of LpConstraint must be LpExpression') self._rhs = rhs_exp # pylint: disable=W0201 @property def sense(self) -> str: """ Getter for the sense of the constraint """ return self._sense @sense.setter def sense(self, snse: str) -> None: """ Setter for the sense of the constraint. Raises error if not one of 'leq', 'eq', 'geq' Raises ------ ValueError If `snse` is not one of `leq`, `eq` or `geq` Parameters ---------- snse: The sense to set """ try: assert snse.lower() in ('leq', 'eq', 'geq') self._sense = snse.lower() # pylint: disable=W0201 except (AttributeError, AssertionError): raise ValueError("Sense must be one of ('leq', 'eq', 'geq')") @property def lower_bound(self) -> Optional[Numeric]: """ Returns the lower bound on the shifted expression """ if self.sense == 'leq': return None return self.rhs.const - self.lhs.const @property def upper_bound(self) -> Optional[Numeric]: """ Returns the upper bound on the shifted expression """ if self.sense == 'geq': return None return self.rhs.const - self.lhs.const def breakdown(self) -> List['LpConstraint']: """ Breaks down a equality constraint into an upper bound and a lower bound constraint """ if self.sense != 'eq': return [self] upper_bound_constraint = LpConstraint( name=self.name + '_ub', lhs=LpExpression(expression=copy.copy(self.lhs.expr)), rhs=LpExpression(expression=copy.copy(self.rhs.expr)), sense='leq', slack=self.slack, slack_penalty=self.slack_penalty, ) lower_bound_constraint = self lower_bound_constraint.sense = 'geq' lower_bound_constraint.name += '_lb' return [lower_bound_constraint, upper_bound_constraint] @property def slack(self) -> bool: """ Getter for slack indicator """ return self._slack @slack.setter def slack(self, slck: bool) -> None: """ Setter for slack indicator Raises ------ ValueError If `slck` is not a bool type variable Parameters ---------- slck: Whether the constraint has slack """ if slck not in (True, False): raise ValueError('Slack indicator must be True or False') self._slack = slck # pylint: disable=W0201 @property def slack_variable(self) -> LpVariable: """ Getter for the slack variable of the problem. Sets if does not exist. """ self._slack_variable = (self._slack_variable or LpVariable( name=self.name + '_slack_variable', var_type=VarType.Continuous, low_bound=0)) if self.slack else None return self._slack_variable @property def slack_penalty(self) -> Numeric: """ Getter for slack penalty of the constraint """ return self._slack_penalty @slack_penalty.setter def slack_penalty(self, penalty: Numeric) -> None: """ Setter for slack penalty of the constraint. Raises error if negative. Raises ------ ValueError If `penalty` has positive value Parameters ---------- penalty: The slack penalty to set Raises ------ ValueError """ if penalty < 0: raise ValueError('Slack penalty must be non-negative') if penalty == 0 and self.slack: warnings.warn( 'Slack penalty is zero. No incentive to meet this constraint') self._slack_penalty = penalty # pylint: disable=W0201 def check(self) -> bool: """ Checks if the constraint is satisfied given variable assignments """ return { 'leq': operator.le, 'eq': operator.eq, 'geq': operator.ge }[self.sense]( self.lhs.evaluate() + (0 if not self.slack else self.slack_variable.evaluate() * (-1 if self.sense == 'leq' else 1)), self.rhs.evaluate()) def to_lp_terms(self): """ Returns the constraint as a list of terms like ['5', 'a', '<=', '10'] Returns ------- list(str) List of terms in string """ new_expr, const = self._shift_variables() if not any(new_expr.expr.values()): return [] if self.sense.lower() == 'leq': sense = '<=' elif self.sense.lower() == 'geq': sense = '>=' else: sense = '=' terms = [] terms += new_expr.to_lp_terms() terms.append(sense) terms.append(str(const)) return terms
def test_to_lp_lp_expr_constant_zero(self, expression): expr = LpExpression(expression={}, constant=0) assert expr.to_lp_terms() == ['0']
def test_to_lp_lp_expr(self, x, y): expr = LpExpression(expression={x: 1, y: -1}, constant=100) assert expr.to_lp_terms() == ['x', '- y', '+ 100']
def read(cls, obj: Union[str, IO, TextIO, io.StringIO]) -> LpProblem: """ Reads in an LP file and parse it into a flipy.LpProblem object Raises ------ ValueError If `obj` is unreadable and is not a LP string Parameters ---------- obj: str or buffer Returns ------- LpProblem Parsed LpProblem based on the LP file """ if hasattr(obj, 'read'): content = obj.read() elif isinstance(obj, (str, bytes)): content = obj try: if os.path.isfile(content): with open(content, "rb") as f: content = f.read() except (TypeError, ValueError): pass else: raise ValueError("Cannot read object of type %r" % type(obj).__name__) content = content.strip() problem_name = cls._find_problem_name(content) is_maximize, sections = cls._split_content_by_sections( cls._remove_comments(content)) obj_name, obj_expr, obj_coeff = cls._parse_named_expression( sections['objective']) constraints = cls._parse_constraints( sections['constraints']) if 'constraints' in sections else [] bounds = cls._parse_bounds( sections['bounds']) if 'bounds' in sections else [] generals = cls._parse_generals( sections['generals']) if 'generals' in sections else [] binaries = cls._parse_binaries( sections['binaries']) if 'binaries' in sections else [] lp_variables = {} lp_objective = LpObjective(obj_name, constant=obj_coeff, sense=Maximize if is_maximize else Minimize) for obj_var_name, obj_coeff in obj_expr.items(): obj_var = cls._find_variable(lp_variables, obj_var_name) lp_objective.expr[obj_var] = obj_coeff lp_constraints = [] for constraint in constraints: lhs_expr = LpExpression() for var_name, cons_var_coeff in constraint['lhs'].items(): var = cls._find_variable(lp_variables, var_name) lhs_expr.expr[var] = cons_var_coeff lhs_expr.const = constraint['lhs_const'] rhs_expr = LpExpression() for var_name, cons_var_coeff in constraint['rhs'].items(): var = cls._find_variable(lp_variables, var_name) rhs_expr.expr[var] = cons_var_coeff rhs_expr.const = constraint['rhs_const'] lp_constraints.append( LpConstraint(lhs_expr, constraint['sense'], rhs_expr, name=constraint['name'])) cls._parse_variables(lp_variables, bounds, generals, binaries) return LpProblem(problem_name, lp_objective=lp_objective, lp_constraints=lp_constraints)
def rhs(x, y): # x + 5y + 2 return LpExpression('rhs', {x: 1, y: 5}, 2)