2  Blending with Logical Restrictions

NoteSource and adaptation

Based on Food manufacture 2, Section 12.2 of Williams (2013). Retains the data and added logical restrictions in Sections 12.1–12.2. The holding-cost convention is the same as Chapter 1. The results below solve the stated instance and are not presented as the numerical answer to an unmodified textbook problem.

2.1 Problem Statement

Use the five oils, prices, hardness indices, stocks, and refining capacities of Chapter 1. In each month, use at most three oils. If an oil is used, at least 20 t must be processed. Using either VEG1 or VEG2 requires using OIL3 in the same month. Maximize operating profit with the same final stocks. All quantities are tonnes. This chapter depends on the complete input tables in Chapter 1.

2.2 Model Formulation

Retain \(b_{it},u_{it},s_{it},q_t\) and all equations of Chapter 1. Add binary \(y_{it}\), equal to one if oil \(i\) is used in month \(t\). Let \(M_i=200\) for vegetable oils and \(250\) otherwise, using physical processing bounds rather than an arbitrary large constant.

\[20y_{it}\le u_{it}\le M_i y_{it},\qquad \sum_i y_{it}\le3.\]

\[y_{\mathrm{VEG1},t}\le y_{\mathrm{OIL3},t},\qquad y_{\mathrm{VEG2},t}\le y_{\mathrm{OIL3},t}.\]

The lower bound applies only when an oil is selected; setting \(y=0\) forces zero consumption. The original profit objective is unchanged. The formulation is a mixed-integer linear program (MILP). The implication is one-way: OIL3 may be used without either vegetable oil.

All decision variables and units refer to the problem statement above. Continuous variables are nonnegative unless explicitly stated otherwise; binary and integer domains are specified in the equations and code.

2.3 Pyomo Implementation

The following Python implementation uses Pyomo and HiGHS. Run from the workbook root so that the models package and shared helpers are importable. The shared solver and audit functions check optimal termination before loading values, then verify every active constraint, variable bound, and integer domain. Any imported earlier-chapter model supplies the data and balances already explained there.

The full source is problem02.py. To solve and print this problem independently:

~/.venvs/optim/bin/python -m models.common 2
import pyomo.environ as pyo
from models.food_manufacture import build_model, OILS, MONTHS, validate
from models.common import frame


def build():
    m = build_model()
    m.selected = pyo.Var(m.I, m.T, domain=pyo.Binary)
    m.minimum_lot = pyo.Constraint(
        m.I, m.T, rule=lambda m, i, t: m.use[i, t] >= 20 * m.selected[i, t]
    )
    m.link = pyo.Constraint(
        m.I,
        m.T,
        rule=lambda m, i, t: (
            m.use[i, t] <= (200 if i in m.V else 250) * m.selected[i, t]
        ),
    )
    m.count = pyo.Constraint(
        m.T, rule=lambda m, t: sum(m.selected[i, t] for i in m.I) <= 3
    )
    m.logic = pyo.Constraint(
        m.V, m.T, rule=lambda m, i, t: m.selected[i, t] <= m.selected["OIL3", t]
    )
    return m


def check(m):
    validate(m)
    for t in m.T:
        used = [i for i in OILS if pyo.value(m.use[i, t]) > 1e-6]
        assert len(used) <= 3
        assert all(pyo.value(m.use[i, t]) >= 20 - 1e-6 for i in used)
        assert not any(i.startswith("VEG") for i in used) or "OIL3" in used


def tables(m):
    return {
        name: frame(
            [
                [MONTHS[t - 1], *[pyo.value(getattr(m, name)[i, t]) for i in OILS]]
                for t in m.T
            ],
            ["Month", *OILS],
        )
        for name in ["use", "buy", "stock"]
    }


def plot(m):
    return (
        MONTHS,
        [sum(pyo.value(m.selected[i, t]) for i in OILS) for t in m.T],
        "Number of oils used",
    )

# Solve, audit constraints, and run domain-specific checks.
from models.common import solve, audit
model = solve(build())
audit(model)
check(model)
for name, result_table in tables(model).items():
    print(name)
    print(result_table.to_string(index=False))

2.4 Optimal Solution

The solver reports optimal termination. The objective is 100,278.703704 GBP over six months (maximize). The largest violation across active constraints, variable bounds, and integer domains is 1.40e-11 in the corresponding model units. The problem-specific checks also pass. These checks establish numerical consistency with the stated model, not the validity of its assumptions for a real facility.

2.4.1 Use

Month VEG1 VEG2 OIL1 OIL2 OIL3
Jan 0 200 0 230 20
Feb 85.1852 114.8148 0 0 250
Mar 85.1852 114.8148 0 0 250
Apr 155 0 0 230 20
May 155 0 0 230 20
Jun 0 200 0 230 20

2.4.2 Buy

Month VEG1 VEG2 OIL1 OIL2 OIL3
Jan 0 0 0 0 0
Feb 0 0 0 190 0
Mar 0 0 0 0 40
Apr 0 0 0 0 0
May 0 0 0 0 540
Jun 480.3704 629.6296 0 730 0

2.4.3 Stock

Month VEG1 VEG2 OIL1 OIL2 OIL3
Jan 500 300 500 270 480
Feb 414.8148 185.1852 500 460 230
Mar 329.6296 70.3704 500 460 20
Apr 174.6296 70.3704 500 230 0
May 19.6296 70.3704 500 0 520
Jun 500 500 500 500 500
Figure 2.1: Selected quantities from the verified optimal solution. Units are stated on the axis.

Tables round numerical values for reading; feasibility checks use the original solver values. Multiple optimal decisions may exist. Machine-readable result records the solver status and package versions. Figure-generation code is in figures/workbook.py.

2.5 Brief Discussion

The added logical restrictions reduce six-month profit by £7,563.89 relative to Chapter 1 under the same cost convention.

The minimum lot size can exclude blends that were attractive in the continuous model. The additional constraints can only reduce or preserve the Chapter 1 optimum. A binary variable represents use during the month, not purchase or storage. Experiment: change the maximum number of oils from three to two and compare the objective and active ingredients.