0

I want to implement boolean logic and dependent variables into a Mixed-Integer Linear Program with scipy.optimize.milp using a highs solver.

How do I set the actual matrices and vectors c, A_ub, b_ub, A_eq, b_eq to fit these exemplary Boolean operations of the exemplary MILP:

Boolean variables: a, b, c, d, e, f, g, h, i, j, k, l

Minimize 1a+1b+...+1l

such that:

a OR b

c AND d

e XOR f

g NAND h

i != j

k == l

a,b,...,l are set to integers via the integrality parameter:

integrality=np.repeat(3, 12+amount_of_helper_variables)

And the lower and upper bounds are set to match boolean values 1 or 0 only:

Forall x in {a,b,...,l}: 0 <= x <= 1

I figured this CS post might help a lot as a general building guide, especially for solvers taking arbitrary formula input formats, but didn't get far myself with the conversion to standard matrix form until now.

I'm asking for a generalizable approach for conversion that basically can be used as a helper method for array creation and doesn't just apply to the stated problem but all boolean formula conversions for standard matrix form MILP using np.arrays to juggle the variables and helpers around.

ABC
  • 189
  • 9

2 Answers2

1

Disclaimer

Generalization is fine, but sometimes we lose exploitable substructures in mathematical-optimization. Sometimes this is bad!

Recommendation

That being said, i recommend the following.

Intermediate language: Conjunctive normal form

  • It's well known, that we can express any boolean function with it
  • It's the form a SAT-solver would expect: DIMACS CNF -> some empirical proof that it's a good pick
  • There is lots of well-understood tooling
  • There is a natural MILP-formulation

Transformation: CNF -> MILP

Helper-function

  • Input: CNF defined on boolean variables (integral and bounded by [0, 1])
  • Output:
    • Set of constraints aka rows in constraint matrix A_ub
    • Set of constants aka scalars in b_ub

No matter what kind of input you have:

You might go through one joint CNF or decompose into many CNFs. And by definition you can concatenate them and their "conjunction." Meaning: A_ub and b_ub are stacking those outputs.

The transformation is simple:

for each c in cnf:
    for each disjunction in c:
        add constraint:
        ---------------
        sum of positive literals - sum of negative literals >= 1 - |negative literals|

Wiki: Literal:

A positive literal is just an atom (e.g. x).
A negative literal is the negation of an atom (e.g. not x).

Example for a given clause = disjunction in some cnf:

x1 or x2 or !x3
->
x1 + x2 + (1-x3) >= 1      easier to understand
<->
x1 + x2 - x3 >= 1 - 1      as proposed above
<->
x1 + x2 - x3 >= 0

(i left one step open -> we need to multiply our constraints with -1 to follow scipys standard-form; but well... you get the idea)

Tooling

CNF

  • SymPy has a boolean algebra module which could help (e.g. transform to cnf)
  • pyeda can achieve similar things (and is actually more targeting use-cases like that)

Remarks

There is tons of other potentiall relevant stuff, especially around CNF-creation.

These things are often important in the real-world, e.g. Tseitin-transformation (for cases where a native cnf-creation would result in exponential-size). pyeda also knows about tseitin if i remember correctly.

But well... it's just a Stack-Overflow answer ;-)

References

If you need some reading material, i recommend:

Hooker, John N. Integrated methods for optimization. Vol. 170. New York: Springer, 2012.

sascha
  • 32,238
  • 6
  • 68
  • 110
1

I would approach this in two steps:

  1. Write things down equation based
  2. Convert (painfully) into matrix format

So we have:

  1. x OR y. I.e. x=1 OR y=1. That is x+y>=1.
  2. x AND y. I.e. x=1 AND y=1. That means just fixing both variables to 1.
  3. x XOR y. I.e. x=1 XOR y=1. That is x+y=1.
  4. x NAND y. I.e. not (x=1 AND y=1). So x+y<=1.
  5. x <> y. This different notation for x XOR y. We handled that already.
  6. x=y.This equation is ready as is. Maybe write as x-y=0.

Step 2, can usually be done in block format using a (large) piece of paper. Each column is a variable (or block of variables) and each row is a constraint. Here all matrix entries (coefficients) are 0, -1 or 1. E.g. x-y=0 means: create a row with a coefficient of 1 in the x column and a -1 in the y column. See: How to implement Linear Programming problem in scipy with complex objective for an example. It is often better to automate this and let a program do this for you. Python tools that do this for you are e.g. PuLP and Pyomo.

Erwin Kalvelagen
  • 15,677
  • 2
  • 14
  • 39