Ex. I1: Disjunctions - Theory, Examples

This is an illustrative notebook for disjunctive constraints in pyomo. We cover


Explanation

Disjunctions selectively enforce differing sets of constraints, for

  • sequencing: \(x\) ends before \(y,\) or \(y\) ends before \(x\)
  • switching: a unit is built/used or not
  • alternative selection: select from a set of pricing policies (or cost vs. performance)

Disjunction Formulation

Following [Grossmann, 2021], the generalized disjunctive program (GDP) has the following formulation.

\[\begin{equation} \lor_{i \in D_{k}} \begin{bmatrix} Y_{ik}\\ n_{ik}(x)\leq0\\ c_{k} = \mu_{ik} \end{bmatrix} \ \ \ , \ \ \ \Omega(Y)= \text{True} \end{equation}\] where,

Notation Definition
\(\lor\) The OR operator that connects a finite collection of disjunctive clauses
\(D_{k}\) The set of disjunctive terms
\(Y_{ik}\) Boolean “indicator variable”
\(h_{ik}(x)\leq 0\) Constraint enforced when \(Y_{ik}\) is true
\(c_k = \mu_{ik}\) Parameter values set when indicator variable is true
\(\Omega(Y)\) Additional logical constraints on indicator variables (ensures XOR)

Disjunctive Problem Formulation

\[\begin{equation*} \tag {01} \begin{array}{llr} \textrm{min} & f(x) & \textrm{Objective Function} \\ \textrm{s.t.} & g(x) \, \leq \, 0 & \textrm{Algebraic Constraints} \\ & \bigvee_{i \in D_k} \begin{bmatrix} Y_{ik} \\ r_{ik}(x) \leq 0 \end{bmatrix}, \, k \in K & \textrm{Disjunctions} \\ & \Omega (Y) = \textrm{True} & \textrm{Logic Propositions} \\ & L \leq x \leq U, \, x \in \mathbb R^n, & \textrm{Continuous Variables} \\ & Y_{ik} \in \{\textrm{True, False} \} & \textrm{Boolean Variables} \end{array} \end{equation*}\]

where,

  • \(f : \mathbb R^n \rightarrow \mathbb R\) is a function, \(x\) is a vector of continuous variables with bounds \(L\) and \(U\)
  • \(g : \mathbb R^n \rightarrow \mathbb R^l\) represents the set of global constraints.
  • Each disjunction \(k \in K\) is composed of a number of terms \(i \in D_k\) that are connected by the Boolean operator OR (\(\vee\) ).
  • Each term \(i \in D_k\) consists of a Boolean variable \(Y_{ik}\) and a set of inequalities \(r_{ik}(x) \leq 0,\) where \(r_{ik} : \mathbb R^n \rightarrow \mathbb R^j.\) If \(Y_{ik}\) is true, then \(r_{ik}(x) \leq 0\) is enforced, otherwise these constraints are ignored.
  • \(\Omega (Y) = \textrm{True}\) are logic propositions for the Boolean variables \(Y_{ik},\) where for each clause \(t \in {1, \dots, T},\) the set \(R_t\) is the subset of Boolean variables that are non-negated and \(Q_t\) is the subset of Boolean variables that are negated.

N.B.: we assume that each disjunction is an exclusive-or, so that for each \(k\) exactly one variable \(Y_{ik}\) is true. Put another way, we assume the logic constraints \(\underline{\vee}_{i \in D_k} Y_{ik}\) are contained in \(\Omega (Y) = \textrm{True}.\)

Example 1: Facility choice

This (trivial) disjunction example involves selecting between two facilities to minimize the power \(P:\)

  • If facility 1 is selected, then power \(P\) is between \(5\) and \(10\) units.
  • If facility 2 is selected, then power \(P\) is between \(20\) and \(30\) units.

Linear Disjunction Form: \[ \begin{equation} \lor_{i \in D} \begin{bmatrix} A_{i}x \leq b_{i}(x) \end{bmatrix} \end{equation}\]

Applied to the Facility-choice Problem: \[ \begin{equation} \begin{bmatrix} y_1\\ P \leq 10\\ -P \leq -5 \end{bmatrix} \lor \begin{bmatrix} y_2\\ P \leq 30\\ -P \leq -20 \end{bmatrix} \end{equation}\]

where the Boolean (binary) variable \(y_{1}\) represents facility 1 and \(y_{2}\) represents facility 2.

Define Model in pyomo with GDP

Use the disjunctionconstuct of pyomo to define the facility-choice problem.

import pyomo.environ as pyo

def create_model():
    '''
    Build the facility problem model.
    
    Return:
    model: Pyomo model
    '''
    ## Model
    model = pyo.ConcreteModel(name="Selecting a facility")

    ## Sets
    # Initialized for facility 1 (1) and facility 2 (2)
    model.facilities = pyo.Set(initialize=[1,2])

    ## Parameters
    # Initialized with a dictionary where the keys are 1 and 2 (the facilities)
    # for the minimum and maximum power values
    model.min_power = pyo.Param(model.facilities,initialize={1:5,2:20}) 
    model.max_power = pyo.Param(model.facilities,initialize={1:10,2:30})
    
    ## Variables
    # Facility power bounded between the lower bound (5) and upper bound (30)
    model.P = pyo.Var(bounds=(5,30),doc='Facilty power')

    ## Adding an objective for the example
    @model.Objective()
    def objective(model):
      return model.P 

    ## Disjunctions
    
    # Objective is bounded by the maximum and minimum power values
    @model.Disjunction(#xor=True,
        model.facilities,
        doc="Power bounds for different facility selections")
    def power_bounds(m, r):
        return [
            m.P <= m.max_power[r],
            m.P >= m.min_power[r],
        ]

    return model

Big-M Relaxation Approach

We use “Big-M” constraints to convert the linear disjunctions into mixed-integer constraints to represent logic with continuous variables. The Big-M reformulation results in a smaller transformed model, avoiding the need to add extra variables; however, it yields a looser continuous relaxation. By default, the Big-M transformation will estimate reasonably tight M values if variables are bounded.

The basic idea in the big-M constraints is that for \(y_j = 1,\) the inequality holds true, i.e., \(A_j x \le b_j.\) In contrast, when \(y_j = 0,\) the inequality becomes redundant for a sufficiently large parameter because then \(A_j x \le b_j + M_j.\) Clearly the value of \(M_j\) has to be carefully chosen. A large value will help to render the linear inequality to be redundant. On the other hand, a large value will also cause the LP relaxation to be weak.

General Notation:

\[\begin{equation} A_{i}(x) \leq b_{i} + M_{i}(1-y_{i}) \ , \ \forall i \in D \end{equation}\]

\[\begin{equation} \sum_{i \in D} y_{i} = 1 \end{equation}\]

\[\begin{equation} y_{i} \in \{0,1\} \ , \ \forall i \in D \end{equation}\]

Applied to the Facility Problem: \[ \begin{align} P &\leq 10 + M_{1}(1-y_{1})\\ -P &\leq -5 + M_{1}(1-y_{1})\\ P &\leq 30 + M_{2}(1-y_{2})\\ -P &\leq -20 + M_{2}(1-y_{2})\\ &y_{1} + y_{2} = 1 \end{align}\]

When the \(y\)’s are considered continuous variables, weak bounds for the objective function are formed for large values such as: \[ M_{1} = 100 \ \ \text{and} \ \ M_{2} = 100\]

Main Idea:
Considering the special case where \(h_{i}(x) = A_{i}x - b_{i} \leq 0,\) \(M_{i}\) is sufficiently large to relax \(h_{i}(x) \leq 0\) when \(y_{i}=0\)

Key Takeaways:

  • If \(M\) is too large, we can get a “weak relaxation” because integer programming algorithms need more iterations.
  • If \(M\) is too small, we can get unintended bounds.

Big-M is best to use if the problem size is small.

Big-M Implementation in Pyomo

First create and print the model.

# Creating the model
model = create_model()

# Printing the model
model.pprint()
1 Set Declarations
    facilities : Size=1, Index=None, Ordered=Insertion
        Key  : Dimen : Domain : Size : Members
        None :     1 :    Any :    2 : {1, 2}

2 Param Declarations
    max_power : Size=2, Index=facilities, Domain=Any, Default=None, Mutable=False
        Key : Value
          1 :    10
          2 :    30
    min_power : Size=2, Index=facilities, Domain=Any, Default=None, Mutable=False
        Key : Value
          1 :     5
          2 :    20

1 Var Declarations
    P : Facilty power
        Size=1, Index=None
        Key  : Lower : Value : Upper : Fixed : Stale : Domain
        None :     5 :  None :    30 : False :  True :  Reals

1 Objective Declarations
    objective : Size=1, Index=None, Active=True
        Key  : Active : Sense    : Expression
        None :   True : minimize :          P

1 Disjunct Declarations
    power_bounds_disjuncts : Size=4, Index=Any, Active=True
        power_bounds_disjuncts[0] : Active=True
            1 Var Declarations
                binary_indicator_var : Size=1, Index=None
                    Key  : Lower : Value : Upper : Fixed : Stale : Domain
                    None :     0 :  None :     1 : False :  True : Binary

            1 Constraint Declarations
                constraint : Size=1, Index={1}, Active=True
                    Key : Lower : Body : Upper : Active
                      1 :  -Inf :    P :  10.0 :   True

            1 BooleanVar Declarations
                indicator_var : Size=1, Index=None
                    Key  : Value : Fixed : Stale
                    None :  None : False :  True

            1 LogicalConstraint Declarations
                propositions : Size=0, Index={}, Active=True
                    Key : Body : Active

            4 Declarations: indicator_var binary_indicator_var constraint propositions
        power_bounds_disjuncts[1] : Active=True
            1 Var Declarations
                binary_indicator_var : Size=1, Index=None
                    Key  : Lower : Value : Upper : Fixed : Stale : Domain
                    None :     0 :  None :     1 : False :  True : Binary

            1 Constraint Declarations
                constraint : Size=1, Index={1}, Active=True
                    Key : Lower : Body : Upper : Active
                      1 :   5.0 :    P :  +Inf :   True

            1 BooleanVar Declarations
                indicator_var : Size=1, Index=None
                    Key  : Value : Fixed : Stale
                    None :  None : False :  True

            1 LogicalConstraint Declarations
                propositions : Size=0, Index={}, Active=True
                    Key : Body : Active

            4 Declarations: indicator_var binary_indicator_var constraint propositions
        power_bounds_disjuncts[2] : Active=True
            1 Var Declarations
                binary_indicator_var : Size=1, Index=None
                    Key  : Lower : Value : Upper : Fixed : Stale : Domain
                    None :     0 :  None :     1 : False :  True : Binary

            1 Constraint Declarations
                constraint : Size=1, Index={1}, Active=True
                    Key : Lower : Body : Upper : Active
                      1 :  -Inf :    P :  30.0 :   True

            1 BooleanVar Declarations
                indicator_var : Size=1, Index=None
                    Key  : Value : Fixed : Stale
                    None :  None : False :  True

            1 LogicalConstraint Declarations
                propositions : Size=0, Index={}, Active=True
                    Key : Body : Active

            4 Declarations: indicator_var binary_indicator_var constraint propositions
        power_bounds_disjuncts[3] : Active=True
            1 Var Declarations
                binary_indicator_var : Size=1, Index=None
                    Key  : Lower : Value : Upper : Fixed : Stale : Domain
                    None :     0 :  None :     1 : False :  True : Binary

            1 Constraint Declarations
                constraint : Size=1, Index={1}, Active=True
                    Key : Lower : Body : Upper : Active
                      1 :  20.0 :    P :  +Inf :   True

            1 BooleanVar Declarations
                indicator_var : Size=1, Index=None
                    Key  : Value : Fixed : Stale
                    None :  None : False :  True

            1 LogicalConstraint Declarations
                propositions : Size=0, Index={}, Active=True
                    Key : Body : Active

            4 Declarations: indicator_var binary_indicator_var constraint propositions

1 Disjunction Declarations
    power_bounds : Power bounds for different facility selections
        Size=2, Index=facilities, Active=True
        Key : Disjuncts                                                  : Active : XOR
          1 : ['power_bounds_disjuncts[0]', 'power_bounds_disjuncts[1]'] :   True : True
          2 : ['power_bounds_disjuncts[2]', 'power_bounds_disjuncts[3]'] :   True : True

7 Declarations: facilities min_power max_power P objective power_bounds power_bounds_disjuncts
# Applying Big-M relaxation to the model
pyo.TransformationFactory('gdp.bigm').apply_to(model)

# Printing
model.pprint()
1 Set Declarations
    facilities : Size=1, Index=None, Ordered=Insertion
        Key  : Dimen : Domain : Size : Members
        None :     1 :    Any :    2 : {1, 2}

2 Param Declarations
    max_power : Size=2, Index=facilities, Domain=Any, Default=None, Mutable=False
        Key : Value
          1 :    10
          2 :    30
    min_power : Size=2, Index=facilities, Domain=Any, Default=None, Mutable=False
        Key : Value
          1 :     5
          2 :    20

1 Var Declarations
    P : Facilty power
        Size=1, Index=None
        Key  : Lower : Value : Upper : Fixed : Stale : Domain
        None :     5 :  None :    30 : False :  True :  Reals

1 Objective Declarations
    objective : Size=1, Index=None, Active=True
        Key  : Active : Sense    : Expression
        None :   True : minimize :          P

1 Block Declarations
    _pyomo_gdp_bigm_reformulation : Size=1, Index=None, Active=True
        1 Constraint Declarations
            power_bounds_xor : Size=2, Index=Any, Active=True
                Key : Lower : Body                                                                                            : Upper : Active
                  1 :   1.0 : power_bounds_disjuncts[0].binary_indicator_var + power_bounds_disjuncts[1].binary_indicator_var :   1.0 :   True
                  2 :   1.0 : power_bounds_disjuncts[2].binary_indicator_var + power_bounds_disjuncts[3].binary_indicator_var :   1.0 :   True

        1 Block Declarations
            relaxedDisjuncts : Size=4, Index=NonNegativeIntegers, Active=True
                _pyomo_gdp_bigm_reformulation.relaxedDisjuncts[0] : Active=True
                    1 Constraint Declarations
                        transformedConstraints : Size=1, Index=Any, Active=True
                            Key                          : Lower : Body                                                          : Upper : Active
                            ('constraint[1]_0', 1, 'ub') :  -Inf : P - 20.0*(1 - power_bounds_disjuncts[0].binary_indicator_var) :  10.0 :   True

                    1 Block Declarations
                        localVarReferences : Size=1, Index=None, Active=True
                            1 Var Declarations
                                binary_indicator_var : Size=1, Index=UnindexedComponent_ReferenceSet, ReferenceTo=power_bounds_disjuncts[0].binary_indicator_var
                                    Key  : Lower : Value : Upper : Fixed : Stale : Domain
                                    None :     0 :  None :     1 : False :  True : Binary

                            1 Declarations: binary_indicator_var

                    2 Declarations: transformedConstraints localVarReferences
                _pyomo_gdp_bigm_reformulation.relaxedDisjuncts[1] : Active=True
                    1 Constraint Declarations
                        transformedConstraints : Size=1, Index=Any, Active=True
                            Key                          : Lower : Body                                                         : Upper : Active
                            ('constraint[1]_0', 1, 'lb') :   5.0 : P - 0.0*(1 - power_bounds_disjuncts[1].binary_indicator_var) :  +Inf :   True

                    1 Block Declarations
                        localVarReferences : Size=1, Index=None, Active=True
                            1 Var Declarations
                                binary_indicator_var : Size=1, Index=UnindexedComponent_ReferenceSet, ReferenceTo=power_bounds_disjuncts[1].binary_indicator_var
                                    Key  : Lower : Value : Upper : Fixed : Stale : Domain
                                    None :     0 :  None :     1 : False :  True : Binary

                            1 Declarations: binary_indicator_var

                    2 Declarations: transformedConstraints localVarReferences
                _pyomo_gdp_bigm_reformulation.relaxedDisjuncts[2] : Active=True
                    1 Constraint Declarations
                        transformedConstraints : Size=1, Index=Any, Active=True
                            Key                          : Lower : Body                                                         : Upper : Active
                            ('constraint[1]_0', 1, 'ub') :  -Inf : P - 0.0*(1 - power_bounds_disjuncts[2].binary_indicator_var) :  30.0 :   True

                    1 Block Declarations
                        localVarReferences : Size=1, Index=None, Active=True
                            1 Var Declarations
                                binary_indicator_var : Size=1, Index=UnindexedComponent_ReferenceSet, ReferenceTo=power_bounds_disjuncts[2].binary_indicator_var
                                    Key  : Lower : Value : Upper : Fixed : Stale : Domain
                                    None :     0 :  None :     1 : False :  True : Binary

                            1 Declarations: binary_indicator_var

                    2 Declarations: transformedConstraints localVarReferences
                _pyomo_gdp_bigm_reformulation.relaxedDisjuncts[3] : Active=True
                    1 Constraint Declarations
                        transformedConstraints : Size=1, Index=Any, Active=True
                            Key                          : Lower : Body                                                          : Upper : Active
                            ('constraint[1]_0', 1, 'lb') :  20.0 : P + 15.0*(1 - power_bounds_disjuncts[3].binary_indicator_var) :  +Inf :   True

                    1 Block Declarations
                        localVarReferences : Size=1, Index=None, Active=True
                            1 Var Declarations
                                binary_indicator_var : Size=1, Index=UnindexedComponent_ReferenceSet, ReferenceTo=power_bounds_disjuncts[3].binary_indicator_var
                                    Key  : Lower : Value : Upper : Fixed : Stale : Domain
                                    None :     0 :  None :     1 : False :  True : Binary

                            1 Declarations: binary_indicator_var

                    2 Declarations: transformedConstraints localVarReferences

        2 Declarations: relaxedDisjuncts power_bounds_xor

1 Disjunct Declarations
    power_bounds_disjuncts : Size=4, Index=Any, Active=False
        power_bounds_disjuncts[0] : Active=False
            1 Var Declarations
                binary_indicator_var : Size=1, Index=None
                    Key  : Lower : Value : Upper : Fixed : Stale : Domain
                    None :     0 :  None :     1 : False :  True : Binary

            1 Constraint Declarations
                constraint : Size=1, Index={1}, Active=True
                    Key : Lower : Body : Upper : Active
                      1 :  -Inf :    P :  10.0 :  False

            1 BooleanVar Declarations
                indicator_var : Size=1, Index=None
                    Key  : Value : Fixed : Stale
                    None :  None : False :  True

            1 LogicalConstraint Declarations
                propositions : Size=0, Index={}, Active=False
                    Key : Body : Active

            4 Declarations: indicator_var binary_indicator_var constraint propositions
        power_bounds_disjuncts[1] : Active=False
            1 Var Declarations
                binary_indicator_var : Size=1, Index=None
                    Key  : Lower : Value : Upper : Fixed : Stale : Domain
                    None :     0 :  None :     1 : False :  True : Binary

            1 Constraint Declarations
                constraint : Size=1, Index={1}, Active=True
                    Key : Lower : Body : Upper : Active
                      1 :   5.0 :    P :  +Inf :  False

            1 BooleanVar Declarations
                indicator_var : Size=1, Index=None
                    Key  : Value : Fixed : Stale
                    None :  None : False :  True

            1 LogicalConstraint Declarations
                propositions : Size=0, Index={}, Active=False
                    Key : Body : Active

            4 Declarations: indicator_var binary_indicator_var constraint propositions
        power_bounds_disjuncts[2] : Active=False
            1 Var Declarations
                binary_indicator_var : Size=1, Index=None
                    Key  : Lower : Value : Upper : Fixed : Stale : Domain
                    None :     0 :  None :     1 : False :  True : Binary

            1 Constraint Declarations
                constraint : Size=1, Index={1}, Active=True
                    Key : Lower : Body : Upper : Active
                      1 :  -Inf :    P :  30.0 :  False

            1 BooleanVar Declarations
                indicator_var : Size=1, Index=None
                    Key  : Value : Fixed : Stale
                    None :  None : False :  True

            1 LogicalConstraint Declarations
                propositions : Size=0, Index={}, Active=False
                    Key : Body : Active

            4 Declarations: indicator_var binary_indicator_var constraint propositions
        power_bounds_disjuncts[3] : Active=False
            1 Var Declarations
                binary_indicator_var : Size=1, Index=None
                    Key  : Lower : Value : Upper : Fixed : Stale : Domain
                    None :     0 :  None :     1 : False :  True : Binary

            1 Constraint Declarations
                constraint : Size=1, Index={1}, Active=True
                    Key : Lower : Body : Upper : Active
                      1 :  20.0 :    P :  +Inf :  False

            1 BooleanVar Declarations
                indicator_var : Size=1, Index=None
                    Key  : Value : Fixed : Stale
                    None :  None : False :  True

            1 LogicalConstraint Declarations
                propositions : Size=0, Index={}, Active=False
                    Key : Body : Active

            4 Declarations: indicator_var binary_indicator_var constraint propositions

1 Disjunction Declarations
    power_bounds : Power bounds for different facility selections
        Size=2, Index=facilities, Active=False
        Key : Disjuncts                                                  : Active : XOR
          1 : ['power_bounds_disjuncts[0]', 'power_bounds_disjuncts[1]'] :  False : True
          2 : ['power_bounds_disjuncts[2]', 'power_bounds_disjuncts[3]'] :  False : True

8 Declarations: facilities min_power max_power P objective power_bounds power_bounds_disjuncts _pyomo_gdp_bigm_reformulation
# Solve and print the solution
pyo.SolverFactory('cbc').solve(model, tee=True)

model.P.display()
Welcome to the CBC MILP Solver 
Version: 2.10.12 
Build Date: Mar  5 2025 

command line - /opt/miniconda3/envs/exa-atow/bin/cbc -printingOptions all -import /var/folders/kx/_1g1vzv51nq1yv81c377flsr0000gn/T/tmpu2hntyxq.pyomo.lp -stat=1 -solve -solu /var/folders/kx/_1g1vzv51nq1yv81c377flsr0000gn/T/tmpu2hntyxq.pyomo.soln (default strategy 1)
Option for printingOptions changed from normal to all
Presolve 0 (-6) rows, 0 (-5) columns and 0 (-10) elements
Statistics for presolved model
Original problem has 4 integers (4 of which binary)


Problem has 0 rows, 0 columns (0 with objective) and 0 elements
Column breakdown:
0 of type 0.0->inf, 0 of type 0.0->up, 0 of type lo->inf, 
0 of type lo->up, 0 of type free, 0 of type fixed, 
0 of type -inf->0.0, 0 of type -inf->up, 0 of type 0.0->1.0 
Row breakdown:
0 of type E 0.0, 0 of type E 1.0, 0 of type E -1.0, 
0 of type E other, 0 of type G 0.0, 0 of type G 1.0, 
0 of type G other, 0 of type L 0.0, 0 of type L 1.0, 
0 of type L other, 0 of type Range 0.0->1.0, 0 of type Range other, 
0 of type Free 
Continuous objective value is 5 - 0.00 seconds
Cgl0004I processed model has 0 rows, 0 columns (0 integer (0 of which binary)) and 0 elements
Cbc3007W No integer variables - nothing to do
Cuts at root node changed objective from 5 to -1.79769e+308
Probing was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Gomory was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Knapsack was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Clique was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
MixedIntegerRounding2 was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
FlowCover was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
TwoMirCuts was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
ZeroHalf was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)

Result - Optimal solution found

Objective value:                5.00000000
Enumerated nodes:               0
Total iterations:               0
Time (CPU seconds):             0.00
Time (Wallclock seconds):       0.00

Total time (CPU seconds):       0.00   (Wallclock seconds):       0.01

P : Facilty power
    Size=1, Index=None
    Key  : Lower : Value : Upper : Fixed : Stale : Domain
    None :     5 :   5.0 :    30 : False : False :  Reals

Convex hull approach

Convex hull can be used if we do not want to implement Big-M parameters. This approach requires separating the continuous variables into its components.

General Notation:

\[ \begin{equation} x = \sum_{i \in D} z_{i} \end{equation} \]

\[ \begin{equation} A_{i} z_{i} \leq b_{i}y_{i} \ , \ \forall i \in D \end{equation} \]

\[ \begin{equation} \sum_{i \in D} y_{i} = 1 \end{equation} \]

\[ \begin{equation} 0 \leq z_{i} \leq Uy_{i} \ , \ \forall i \in D \end{equation} \]

\[ \begin{equation} y_{i} = \{0,1\} \ , \ \forall i \in D \end{equation} \]

where \(z_{i}\) are continuous variables separated into as many new variables as there are terms for the disjunctions.

Applied to the Facility Problem:

\[ \begin{align} P &= P_{1} + P_{2} \\ P_{1} &\leq 10y_{1}\\ P_{2} &\leq 30y_{2} \\ -P_{1} &\leq -5y_{1}\\ -P_{2} &\leq -20y_{2}\\ &y_{1} + y_{2} = 1 \end{align}\]

Key Takeaways:

(+) Constraints do not require Big-M parameters which produce a tight linear programming relaxation.
(–) A larger number of variables and constraints is required.

Convex hull is better to use than Big-M if the problem is large.

# Creating the model
model = create_model()

# Apply convex hull relaxation to the model
pyo.TransformationFactory('gdp.hull').apply_to(model)

# Solve and print the solution
pyo.SolverFactory('cbc').solve(model, tee=True)

model.P.display()
Welcome to the CBC MILP Solver 
Version: 2.10.12 
Build Date: Mar  5 2025 

command line - /opt/miniconda3/envs/exa-atow/bin/cbc -printingOptions all -import /var/folders/kx/_1g1vzv51nq1yv81c377flsr0000gn/T/tmp1bjx2j8r.pyomo.lp -stat=1 -solve -solu /var/folders/kx/_1g1vzv51nq1yv81c377flsr0000gn/T/tmp1bjx2j8r.pyomo.soln (default strategy 1)
Option for printingOptions changed from normal to all
Presolve 10 (-6) rows, 5 (-4) columns and 24 (-10) elements
Statistics for presolved model
Original problem has 4 integers (4 of which binary)
Presolved problem has 2 integers (2 of which binary)
==== 4 zero objective 2 different
4 variables have objective of 0
1 variables have objective of 1
==== absolute objective values 2 different
4 variables have objective of 0
1 variables have objective of 1
==== for integers 2 zero objective 1 different
2 variables have objective of 0
==== for integers absolute objective values 1 different
2 variables have objective of 0
===== end objective counts


Problem has 10 rows, 5 columns (1 with objective) and 24 elements
Column breakdown:
0 of type 0.0->inf, 2 of type 0.0->up, 0 of type lo->inf, 
1 of type lo->up, 0 of type free, 0 of type fixed, 
0 of type -inf->0.0, 0 of type -inf->up, 2 of type 0.0->1.0 
Row breakdown:
0 of type E 0.0, 0 of type E 1.0, 0 of type E -1.0, 
0 of type E other, 0 of type G 0.0, 0 of type G 1.0, 
0 of type G other, 5 of type L 0.0, 0 of type L 1.0, 
5 of type L other, 0 of type Range 0.0->1.0, 0 of type Range other, 
0 of type Free 
Continuous objective value is 5 - 0.00 seconds
Cgl0003I 0 fixed, 0 tightened bounds, 1 strengthened rows, 0 substitutions
Cgl0003I 0 fixed, 0 tightened bounds, 1 strengthened rows, 0 substitutions
Cgl0004I processed model has 8 rows, 5 columns (2 integer (2 of which binary)) and 21 elements
Cbc0038I Initial state - 0 integers unsatisfied sum - 0
Cbc0038I Solution found of 5
Cbc0038I Relaxing continuous gives 5
Cbc0038I Before mini branch and bound, 2 integers at bound fixed and 3 continuous
Cbc0038I Mini branch and bound did not improve solution (0.00 seconds)
Cbc0038I After 0.00 seconds - Feasibility pump exiting with objective of 5 - took 0.00 seconds
Cbc0012I Integer solution of 5 found by feasibility pump after 0 iterations and 0 nodes (0.00 seconds)
Cbc0001I Search completed - best objective 5, took 0 iterations and 0 nodes (0.00 seconds)
Cbc0035I Maximum depth 0, 0 variables fixed on reduced cost
Cuts at root node changed objective from 5 to 5
Probing was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Gomory was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Knapsack was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Clique was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
MixedIntegerRounding2 was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
FlowCover was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
TwoMirCuts was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
ZeroHalf was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)

Result - Optimal solution found

Objective value:                5.00000000
Enumerated nodes:               0
Total iterations:               0
Time (CPU seconds):             0.00
Time (Wallclock seconds):       0.00

Total time (CPU seconds):       0.00   (Wallclock seconds):       0.01

P : Facilty power
    Size=1, Index=None
    Key  : Lower : Value : Upper : Fixed : Stale : Domain
    None :     5 :   5.0 :    30 : False : False :  Reals

Example 2: Production Technology Choice

Suppose we have a choice between 2 technologies for production:

  • Technology 1: more time, less expensive.
  • Technology 2: less time, more expensive.

Multi-product factory optimization

A small production facility produces two products, \(X\) and \(Y\). With current technology \(\alpha\), the facility is subject to the following conditions and constraints:

Product labor A labor B material sales price profit
X 1h 2h 100€ 270€ 40€
Y 1h 1h 90€ 210€ 30€
  • Product \(X\) requires 1 hour of labor A, 2 hours of labor B, and 100€ of raw material. Product \(X\) sells for 270€ per unit. The daily demand is limited to 40 units.

  • Product \(Y\) requires 1 hour of labor A, 1 hour of labor B, and 90€ of raw material. Product \(Y\) sells for 210€ per unit with unlimited demand.

  • There are 80 hours per day of labor A available at a cost of 50 €/hour, and there are 100 hours per day of labor B available at a cost of 40 €/hour.

Using the given data we see that the net profit for each unit of \(X\) and \(Y\) is \[ P_X = 270 - (1 \cdot 50 + 2 \cdot 40) - 100 = 40\] and \[ P_Y = 210 - (1 \cdot 50 + 1 \cdot 40 )- 90 = 30\] respectively. The optimal product strategy is the solution of a linear optimization problem,

\[ \begin{align*} \max \quad & 40 x + 30 y\\ \text{s.t.} \quad & x \leq 40 & \text{(demand)}\\ & x + y \leq 80 & \text{(labor A)} \\ & 2 x + y \leq 100 & \text{(labor B)}\\ & x, y \geq 0. \end{align*} \]

import pyomo.environ as pyo

solver = 'appsi_highs'
SOLVER = pyo.SolverFactory(solver)

m = pyo.ConcreteModel("Multi-Product Factory")

m.production_x = pyo.Var(domain=pyo.NonNegativeReals)
m.production_y = pyo.Var(domain=pyo.NonNegativeReals)

@m.Objective(sense=pyo.maximize)
def maximize_profit(m):
    return 40 * m.production_x + 30 * m.production_y

@m.Constraint()
def demand(m):
    return m.production_x <= 40

@m.Constraint()
def laborA(m):
    return m.production_x + m.production_y <= 80

@m.Constraint()
def laborB(m):
    return 2 * m.production_x + m.production_y <= 100

SOLVER.solve(m)

print(f"Profit = ${pyo.value(m.maximize_profit):.2f}")
print(f"Production X = {pyo.value(m.production_x)}")
print(f"Production Y = {pyo.value(m.production_y)}")
Profit = $2600.00
Production X = 20.0
Production Y = 60.0

Disjunction: technology choice

Suppose a new technology \(\beta\) has been developed for labor B, with the potential to cut costs by reducing the time required to finish product \(X\) from \(2.0\) to \(1.5\) hours, but requires more highly skilled labor with a higher unit cost of \(50\)€ per hour.

The net profit for unit of product \(X\)

  • with technology \(\alpha\) is \(270 - 100 - 50 - 2 \cdot 40 = 40\)€,
  • while with technology \(\beta\) is equal to \(270 - 100 - 50 - 1.5 \cdot 50 = 45\)€.

We need to assess whether the new technology is beneficial, that is, whether adopting it would lead to higher profits. The decision here is whether to use technology \(\alpha\) or \(\beta\).

In this situation we have an `either-or’ structure for both the objective and for Labor B constraint:

\[ \underbrace{p = 40x + 30y, \ 2 x + y \leq 100}_{\text{$\alpha$ technology}} \quad \text{ or } \quad \underbrace{p = 50x + 30y, \ 1.5 x + y \leq 100}_{\text{$\beta$ technology}}. \]

We use two techniques for embedding disjunctions into mixed-integer linear optimization problems:

  1. Big-M
  2. Convex hull

Big-M implementation

The first approach is using the “big-M” technique introduces a single binary decision variable \(z\) associated with choosing technology \(\alpha\) (\(z=0\)) or technology \(\beta\) (\(z=1\)). Using MILP, we can formulate this problem as follows:

\[ \begin{align*} \max \quad & \text{profit}\\ \text{s.t.} \quad & x \leq 40 & \text{(demand)}\\ & x + y \leq 80 & \text{(labor A)} \\ & \text{profit} \leq 40x + 30y + M z & \text{(profit with technology $\alpha$)} \\ & \text{profit} \leq 45x + 30y + M (1 - z) & \text{(profit with technology $\beta$)}\\ & 2 x + y \leq 100 + M z & \text{(labor B with technology $\alpha$)} \\ & 1.5 x + y \leq 100 + M (1 - z) & \text{(labor B with technology $\beta$)} \\ & z \in \mathbb{B} \\ & x, y \geq 0. \end{align*} \]

where the variable \(z \in \{ 0, 1\}\) “activates” the constraints related to the old or new technology, respectively, and \(M\) is a large enough constant. It can be implemented in Pyomo as follows.

m = pyo.ConcreteModel("Multi-Product Factory - MILP formulation")

m.profit = pyo.Var(domain=pyo.NonNegativeReals)
m.production_x = pyo.Var(domain=pyo.NonNegativeReals)
m.production_y = pyo.Var(domain=pyo.NonNegativeReals)

m.z = pyo.Var(domain=pyo.Binary)
M = 10000

@m.Objective(sense=pyo.maximize)
def maximize_profit(m):
    return m.profit

@m.Constraint()
def profit_constr_1(m):
    return m.profit <= 40 * m.production_x + 30 * m.production_y + M * m.z

@m.Constraint()
def profit_constr_2(m):
    return m.profit <= 45 * m.production_x + 30 * m.production_y + M * (1 - m.z)
    #return m.profit <= 30 * m.production_x + 30 * m.production_y + M * (1 - m.z)

@m.Constraint()
def demand(m):
    return m.production_x <= 40

@m.Constraint()
def laborA(m):
    return m.production_x + m.production_y <= 80

@m.Constraint()
def laborB_1(m):
    return 2 * m.production_x + m.production_y <= 100 + M * m.z

@m.Constraint()
def laborB_2(m):
    return 1.5 * m.production_x + m.production_y <= 100 + M * (1 - m.z)


SOLVER.solve(m, tee=True)

print(f"Profit = ${pyo.value(m.maximize_profit):.2f}")
print(f"Production X = {pyo.value(m.production_x)}")
print(f"Production Y = {pyo.value(m.production_y)}")
Running HiGHS 1.10.0 (git hash: fd86653): Copyright (c) 2025 HiGHS under MIT licence terms
MIP  has 6 rows; 4 cols; 17 nonzeros; 1 integer variables (1 binary)
Coefficient ranges:
  Matrix [1e+00, 1e+04]
  Cost   [1e+00, 1e+00]
  Bound  [1e+00, 1e+00]
  RHS    [4e+01, 1e+04]
Presolving model
5 rows, 4 cols, 16 nonzeros  0s
5 rows, 4 cols, 16 nonzeros  0s

Solving MIP model with:
   5 rows
   4 cols (1 binary, 0 integer, 0 implied int., 3 continuous)
   16 nonzeros

Src: B => Branching; C => Central rounding; F => Feasibility pump; H => Heuristic; L => Sub-MIP;
     P => Empty MIP; R => Randomized rounding; S => Solve LP; T => Evaluate node; U => Unbounded;
     z => Trivial zero; l => Trivial lower; u => Trivial upper; p => Trivial point; X => User solution

        Nodes      |    B&B Tree     |            Objective Bounds              |  Dynamic Constraints |       Work      
Src  Proc. InQueue |  Leaves   Expl. | BestBound       BestSol              Gap |   Cuts   InLp Confl. | LpIters     Time

         0       0         0   0.00%   4200            -inf                 inf        0      0      0         0     0.0s
 R       0       0         0   0.00%   4200            2600              61.54%        0      0      0         2     0.0s
 C       0       0         0   0.00%   3000            3000               0.00%        6      2      0         8     0.0s
 T       0       0         0   0.00%   3000            3000               0.00%        6      2      0         8     0.0s
         1       0         1 100.00%   3000            3000               0.00%        6      2      0         8     0.0s

Solving report
  Status            Optimal
  Primal bound      3000
  Dual bound        3000
  Gap               0% (tolerance: 0.01%)
  P-D integral      0.000290254989245
  Solution status   feasible
                    3000 (objective)
                    0 (bound viol.)
                    8.881784197e-16 (int. viol.)
                    0 (row viol.)
  Timing            0.00 (total)
                    0.00 (presolve)
                    0.00 (solve)
                    0.00 (postsolve)
  Max sub-MIP depth 0
  Nodes             1
  Repair LPs        0 (0 feasible; 0 iterations)
  LP iterations     8 (total)
                    0 (strong br.)
                    6 (separation)
                    0 (heuristics)
Profit = $3000.00
Production X = 40.0
Production Y = 40.0

Disjunctive programming implementation

Alternatively, we can formulate our problem using a disjunction, preserving the logical structure, as follows:

\[ \begin{align*} \max \quad & \text{profit}\\ \text{s.t.} \quad & x \leq 40 & \text{(demand)}\\ & x + y \leq 80 & \text{(labor A)} \\ & \begin{bmatrix} \text{profit} = 40x + 30y\\ 2 x + y \leq 100 \end{bmatrix} \veebar \begin{bmatrix} \text{profit} = 45x + 30y\\ 1.5 x + y \leq 100 \end{bmatrix}\\ & x, y \geq 0. \end{align*} \]

This formulation has the benefit that the solver can intelligently partition the problem’s solution into various sub-cases, based on the given disjunction. Pyomo natively supports disjunctions, as illustrated in the following implementation.

m = pyo.ConcreteModel("Multi-Product Factory - Disjunctive Programming")

m.profit = pyo.Var(bounds=(-1000, 10000))
m.x = pyo.Var(domain=pyo.NonNegativeReals, bounds=(0, 1000))
m.y = pyo.Var(domain=pyo.NonNegativeReals, bounds=(0, 1000))

@m.Objective(sense=pyo.maximize)
def maximize_profit(m):
    return m.profit

@m.Constraint()
def demand(m):
    return m.x <= 40

@m.Constraint()
def laborA(m):
    return m.x + m.y <= 80

# Define a disjunction using Pyomo's Disjunction component
# The 'xor=True' indicates that only one of the disjuncts must be true
@m.Disjunction(xor=True)
def technologies(m):
    # The function returns a list of two disjuncts
    # each containing a profit and a constraint
    return [
        [m.profit == 40 * m.x + 30 * m.y, 2 * m.x + m.y <= 100],
        [m.profit == 45 * m.x + 30 * m.y, 1.5 * m.x + m.y <= 100],
    ]

# Transform the Generalized Disjunctive Programming (GDP) model using
# the big-M method into a MILP problem and solve it
pyo.TransformationFactory("gdp.bigm").apply_to(m)
SOLVER.solve(m, tee=True)

print(f"Profit = ${pyo.value(m.maximize_profit):.2f}")
print(f"Production X = {pyo.value(m.x)}")
print(f"Production Y = {pyo.value(m.y)}")
Running HiGHS 1.10.0 (git hash: fd86653): Copyright (c) 2025 HiGHS under MIT licence terms
MIP  has 9 rows; 5 cols; 27 nonzeros; 2 integer variables (2 binary)
Coefficient ranges:
  Matrix [1e+00, 8e+04]
  Cost   [1e+00, 1e+00]
  Bound  [1e+00, 1e+04]
  RHS    [1e+00, 8e+04]
Presolving model
7 rows, 4 cols, 24 nonzeros  0s
7 rows, 4 cols, 24 nonzeros  0s

Solving MIP model with:
   7 rows
   4 cols (1 binary, 0 integer, 0 implied int., 3 continuous)
   24 nonzeros

Src: B => Branching; C => Central rounding; F => Feasibility pump; H => Heuristic; L => Sub-MIP;
     P => Empty MIP; R => Randomized rounding; S => Solve LP; T => Evaluate node; U => Unbounded;
     z => Trivial zero; l => Trivial lower; u => Trivial upper; p => Trivial point; X => User solution

        Nodes      |    B&B Tree     |            Objective Bounds              |  Dynamic Constraints |       Work      
Src  Proc. InQueue |  Leaves   Expl. | BestBound       BestSol              Gap |   Cuts   InLp Confl. | LpIters     Time

         0       0         0   0.00%   4200            -inf                 inf        0      0      0         0     0.0s
 R       0       0         0   0.00%   4200            2600              61.54%        0      0      0         2     0.0s
 C       0       0         0   0.00%   3000            3000               0.00%        6      2      0         8     0.0s
         1       0         1 100.00%   3000            3000               0.00%        6      2      0         8     0.0s

Solving report
  Status            Optimal
  Primal bound      3000
  Dual bound        3000
  Gap               0% (tolerance: 0.01%)
  P-D integral      0.00028999664704
  Solution status   feasible
                    3000 (objective)
                    0 (bound viol.)
                    0 (int. viol.)
                    0 (row viol.)
  Timing            0.00 (total)
                    0.00 (presolve)
                    0.00 (solve)
                    0.00 (postsolve)
  Max sub-MIP depth 0
  Nodes             1
  Repair LPs        0 (0 feasible; 0 iterations)
  LP iterations     8 (total)
                    0 (strong br.)
                    6 (separation)
                    0 (heuristics)
Profit = $3000.00
Production X = 40.0
Production Y = 40.0

Alternative solver

Try with the cbc solver…

solver = 'cbc'
SOLVER = pyo.SolverFactory(solver)

SOLVER.solve(m, tee=True)

print(f"Profit = ${pyo.value(m.maximize_profit):.2f}")
print(f"Production X = {pyo.value(m.x)}")
print(f"Production Y = {pyo.value(m.y)}")
Welcome to the CBC MILP Solver 
Version: 2.10.12 
Build Date: Mar  5 2025 

command line - /opt/miniconda3/envs/exa-atow/bin/cbc -printingOptions all -import /var/folders/kx/_1g1vzv51nq1yv81c377flsr0000gn/T/tmpt98ljm9h.pyomo.lp -stat=1 -solve -solu /var/folders/kx/_1g1vzv51nq1yv81c377flsr0000gn/T/tmpt98ljm9h.pyomo.soln (default strategy 1)
Option for printingOptions changed from normal to all
 CoinLpIO::readLp(): Maximization problem reformulated as minimization
Coin0009I Switching back to maximization to get correct duals etc
Presolve 7 (-2) rows, 4 (-1) columns and 24 (-3) elements
Statistics for presolved model
Original problem has 2 integers (2 of which binary)
Presolved problem has 1 integers (1 of which binary)
==== 3 zero objective 2 different
3 variables have objective of -0
1 variables have objective of 1
==== absolute objective values 2 different
3 variables have objective of 0
1 variables have objective of 1
==== for integers 1 zero objective 1 different
1 variables have objective of -0
==== for integers absolute objective values 1 different
1 variables have objective of 0
===== end objective counts


Problem has 7 rows, 4 columns (1 with objective) and 24 elements
Column breakdown:
0 of type 0.0->inf, 2 of type 0.0->up, 0 of type lo->inf, 
1 of type lo->up, 0 of type free, 0 of type fixed, 
0 of type -inf->0.0, 0 of type -inf->up, 1 of type 0.0->1.0 
Row breakdown:
0 of type E 0.0, 0 of type E 1.0, 0 of type E -1.0, 
0 of type E other, 1 of type G 0.0, 0 of type G 1.0, 
1 of type G other, 1 of type L 0.0, 0 of type L 1.0, 
4 of type L other, 0 of type Range 0.0->1.0, 0 of type Range other, 
0 of type Free 
Continuous objective value is 7900 - 0.00 seconds
Cgl0003I 0 fixed, 0 tightened bounds, 1 strengthened rows, 0 substitutions
Cgl0004I processed model has 7 rows, 4 columns (1 integer (1 of which binary)) and 24 elements
Cbc0038I Initial state - 1 integers unsatisfied sum - 0.49
Cbc0038I Pass   1: suminf.    0.00000 (0) obj. 3000 iterations 1
Cbc0038I Solution found of 3000
Cbc0038I Relaxing continuous gives 3000
Cbc0038I Before mini branch and bound, 0 integers at bound fixed and 1 continuous
Cbc0038I Full problem 7 rows 4 columns, reduced to 5 rows 3 columns
Cbc0038I Mini branch and bound improved solution from 3000 to 3000 (0.00 seconds)
Cbc0038I Round again with cutoff of 3490
Cbc0038I Pass   2: suminf.    0.04900 (1) obj. 3490 iterations 1
Cbc0038I Pass   3: suminf.    0.06900 (1) obj. 3490 iterations 6
Cbc0038I Pass   4: suminf.    0.06900 (1) obj. 3490 iterations 0
Cbc0038I Pass   5: suminf.    0.06900 (1) obj. 3490 iterations 0
Cbc0038I Pass   6: suminf.    0.06900 (1) obj. 3490 iterations 0
Cbc0038I Pass   7: suminf.    0.06900 (1) obj. 3490 iterations 0
Cbc0038I Pass   8: suminf.    0.06900 (1) obj. 3490 iterations 0
Cbc0038I Pass   9: suminf.    0.06900 (1) obj. 3490 iterations 0
Cbc0038I Pass  10: suminf.    0.06900 (1) obj. 3490 iterations 0
Cbc0038I Pass  11: suminf.    0.06900 (1) obj. 3490 iterations 0
Cbc0038I Pass  12: suminf.    0.06900 (1) obj. 3490 iterations 0
Cbc0038I Pass  13: suminf.    0.06900 (1) obj. 3490 iterations 0
Cbc0038I Pass  14: suminf.    0.06900 (1) obj. 3490 iterations 0
Cbc0038I Pass  15: suminf.    0.06900 (1) obj. 3490 iterations 0
Cbc0038I Pass  16: suminf.    0.06900 (1) obj. 3490 iterations 0
Cbc0038I Pass  17: suminf.    0.06900 (1) obj. 3490 iterations 0
Cbc0038I Pass  18: suminf.    0.06900 (1) obj. 3490 iterations 0
Cbc0038I Pass  19: suminf.    0.06900 (1) obj. 3490 iterations 0
Cbc0038I Pass  20: suminf.    0.06900 (1) obj. 3490 iterations 0
Cbc0038I Pass  21: suminf.    0.06900 (1) obj. 3490 iterations 0
Cbc0038I Pass  22: suminf.    0.06900 (1) obj. 3490 iterations 0
Cbc0038I Pass  23: suminf.    0.06900 (1) obj. 3490 iterations 0
Cbc0038I Pass  24: suminf.    0.06900 (1) obj. 3490 iterations 0
Cbc0038I Pass  25: suminf.    0.06900 (1) obj. 3490 iterations 0
Cbc0038I Pass  26: suminf.    0.06900 (1) obj. 3490 iterations 0
Cbc0038I Pass  27: suminf.    0.06900 (1) obj. 3490 iterations 0
Cbc0038I Pass  28: suminf.    0.06900 (1) obj. 3490 iterations 0
Cbc0038I Pass  29: suminf.    0.06900 (1) obj. 3490 iterations 0
Cbc0038I Pass  30: suminf.    0.06900 (1) obj. 3490 iterations 0
Cbc0038I Pass  31: suminf.    0.06900 (1) obj. 3490 iterations 0
Cbc0038I No solution found this major pass
Cbc0038I Before mini branch and bound, 0 integers at bound fixed and 1 continuous
Cbc0038I Full problem 7 rows 4 columns, reduced to 5 rows 3 columns
Cbc0038I Mini branch and bound did not improve solution (0.00 seconds)
Cbc0038I After 0.00 seconds - Feasibility pump exiting with objective of 3000 - took 0.00 seconds
Cbc0012I Integer solution of 3000 found by feasibility pump after 0 iterations and 0 nodes (0.00 seconds)
Cbc0031I 2 added rows had average density of 3
Cbc0013I At root node, 10 cuts changed objective from 7900 to 3000 in 1 passes
Cbc0014I Cut generator 0 (Probing) - 5 row cuts average 2.8 elements, 0 column cuts (0 active)  in 0.000 seconds - new frequency is 1
Cbc0014I Cut generator 1 (Gomory) - 1 row cuts average 4.0 elements, 0 column cuts (0 active)  in 0.000 seconds - new frequency is 1
Cbc0014I Cut generator 2 (Knapsack) - 0 row cuts average 0.0 elements, 0 column cuts (0 active)  in 0.000 seconds - new frequency is -100
Cbc0014I Cut generator 3 (Clique) - 0 row cuts average 0.0 elements, 0 column cuts (0 active)  in 0.000 seconds - new frequency is -100
Cbc0014I Cut generator 4 (MixedIntegerRounding2) - 2 row cuts average 2.0 elements, 0 column cuts (0 active)  in 0.000 seconds - new frequency is 1
Cbc0014I Cut generator 5 (FlowCover) - 0 row cuts average 0.0 elements, 0 column cuts (0 active)  in 0.000 seconds - new frequency is -100
Cbc0014I Cut generator 6 (TwoMirCuts) - 2 row cuts average 2.0 elements, 0 column cuts (0 active)  in 0.000 seconds - new frequency is -100
Cbc0001I Search completed - best objective 3000, took 0 iterations and 0 nodes (0.00 seconds)
Cbc0035I Maximum depth 0, 0 variables fixed on reduced cost
Cuts at root node changed objective from 7900 to 3000
Probing was tried 1 times and created 5 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Gomory was tried 1 times and created 1 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Knapsack was tried 1 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Clique was tried 1 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
MixedIntegerRounding2 was tried 1 times and created 2 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
FlowCover was tried 1 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
TwoMirCuts was tried 1 times and created 2 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
ZeroHalf was tried 1 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)

Result - Optimal solution found

Objective value:                3000.00000000
Enumerated nodes:               0
Total iterations:               0
Time (CPU seconds):             0.00
Time (Wallclock seconds):       0.01

Total time (CPU seconds):       0.00   (Wallclock seconds):       0.01

Profit = $3000.00
Production X = 40.0
Production Y = 40.0