Search code examples
python-3.xoptimizationpyomo

Multiplication of an Expression object with a boolean in pyomo


I'm currently trying to upgrade pyomo from 6.5.0 to 6.6.2

However I encounter an issue with the * operator when trying to multiply a boolean with an expression object. It works fine with binary variables but fails with native Booleans returning these errors:

- TypeError: unsupported operand type(s) for *: '_GeneralExpressionData' and 'bool'
- TypeError: unsupported operand type(s) for *: 'bool' and 'SumExpression'
- TypeError: unsupported operand type(s) for *: 'ScalarExpression' and 'bool'
- TypeError: unsupported operand type(s) for *: 'bool' and 'NPV_SumExpression'

All of these operations were working fine with 6.5.0 and I can always work around it by converting the Booleans to Ints before the multiplication but that would imply a lot of changes to the project.
Why is this operation not supported any more and how could I solve this efficiently?
thanks

Configuration:

  • Windows 11
  • Python 3.11
  • Pyomo 6.6.2

Working example:

import pyomo.environ as pyo
from pyomo.repn.standard_repn import generate_standard_repn
from pyomo.opt import SolverFactory

model = pyo.ConcreteModel()

model.x = pyo.Var([1,2], domain=pyo.NonNegativeReals)
model.exp = pyo.Expression(rule= lambda m: 3*m.x[1] + 4*m.x[2])

model.OBJ = pyo.Objective(expr = 2*model.x[1] + 3*model.x[2])

model.Constraint1 = pyo.Constraint(expr = model.exp >= 1)


def update_expression(attribute, update, add_expression: bool = False):
     original_index = attribute.index_set()

     update_values = (update[index] for index in original_index)

     for index, update_value in zip(original_index, update_values, strict=True):
         attribute[index] = generate_standard_repn(update_value + attribute[index] * add_expression).to_expression()

 model.new_expression = pyo.Expression(rule= lambda _ :-1)

 update_expression(model.exp, model.new_expression, add_expression=True)

 opt = SolverFactory('cbc')
 result = opt.solve(model, tee=True)
 model.display()

An example of a function used to update expressions dynamically


Solution

  • That behavior is intentional and the exception you are seeing was added to catch modeling errors. The crux of the problem is that logical operations and algebraic operations conceptually belong to separate expression systems and should not be mixed. For example, should "5 * True" be the algebraic operation (multiply) and return 5 (assuming that the logical state True should be mapped to the numeric state 1), or should it be the logical operation (and) and return True (after mapping the 5 to True)? Python uses the former, and that may seem correct. Unfortunately, this gets more confusing when building expression systems. Consider:

    (m.p >= 5) * 10

    If m.p is an immutable Param with a value of 6, then by the Python rules, this expression would to 10. Unfortunately, if m.p is a Var, you would get an expression, and that expression can not be written out to any solver [*1].

    This distinction between algebraic and logical expression systems became more apparent as we developed and expanded the support for logical expressions in Pyomo (e.g., through work in GDP, as well as the new draft constraint programming interface). As of Pyomo 6.6, we drew a strong distinction between algebraic expressions (which, for historical reasons are in the NumericExpression hierarchy), logical expressions (in the BooleanExpression hierarchy) and relational expressions (derived from RelationalExpression) that bridge the two (a relational expression is a logical expression that has algebraic expression arguments).

    As to your use case, instead of:

    for index, update_value in zip(original_index, update_values, strict=True):
        attribute[index] = generate_standard_repn(update_value + attribute[index] * add_expression).to_expression()
    

    I would recommend something like:

    if add_expression:
        for index, update_value in zip(original_index, update_values, strict=True):
            attribute[index] += update_value
    else:
        for index, update_value in zip(original_index, update_values, strict=True):
            attribute[index] = update_value
    

    [*1] You can technically write that expression in an NL file, but it would need to be converted to Expr_if(IF_=m.p >= 5, THEN_=10, ELSE_=0). While you can emit that expression, the discontinuity at m.p == 5 can cause problems for many solvers.