Diet problem#

Please refer to chapter 2 in the AMPL Book for a detailed description of the problem. We demonstrate how to implement this model with DOcplex, gurobipy, Xpress, and highspy along with the functionality from opti-extensions.

Implementation reference: ampl/colab.ampl.com — Copyright (c) 2022-2022, AMPL Optimization inc. (licensed under the MIT License)

Mathematical formulation#

Index-sets:

  • Nutrients to consider:
    \(i \in NUTR\)
  • Food items to consider:
    \(j \in FOOD\)

Parameters:

  • Cost of food item \(j\):
    \(cost_{j} \in \mathbb{R}^{+} \quad \forall \; j \in FOOD\)
  • Minimum purchase quantity required for food item \(j\):
    \(f\_min_{j} \in \mathbb{R}_{0}^{+} \quad \forall \; j \in FOOD\)
  • Maximum purchase quantity allowed for food item \(j\):
    \(f\_max_{j} \in \mathbb{R}_{0}^{+} \quad \forall \; j \in FOOD\)
  • Minimum amount required of nutrient \(i\):
    \(n\_min_{i} \in \mathbb{R}_{0}^{+} \quad \forall \; i \in NUTR\)
  • Maximum amount required of nutrient \(i\):
    \(n\_max_{i} \in \mathbb{R}_{0}^{+} \quad \forall \; i \in NUTR\)
  • Amount of nutrient \(i\) in food item \(j\):
    \(amt_{i, j} \in \mathbb{R}_{0}^{+} \quad \forall \; i \in NUTR \; \& \; \forall \; j \in FOOD\)

Variables:

  • Quantity of food item \(j\) to be purchased:
    \(buy_{j} \in \mathbb{R}_{0}^{+} (\geq f\_min_{j}, \leq f\_max_{j}) \quad \forall \; j \in FOOD\)

Objective:

  • Minimize the total cost of the diet:
    \(\min \; \sum_{j \in FOOD} cost_{j} \times buy_{j}\)

Constraints:

  • Ensure that the nutritional limits are satisfied by the diet:
    \(n\_min_{i} \leq \sum_{j \in FOOD} amt_{i, j} \times buy_{j} \leq n\_max_{i}, \; \forall \; i \in NUTR\)

Set up problem data#

Index-sets#

from opti_extensions import IndexSet1D

NUTR = IndexSet1D(['A', 'B1', 'B2', 'C'], name='NUTRIENT')

FOOD = IndexSet1D(['BEEF', 'CHK', 'FISH', 'HAM', 'MCH', 'MTL', 'SPG', 'TUR'], name='FOOD')

Parameters#

from opti_extensions import ParamDict1D, ParamDictND

cost = ParamDict1D(
    {
        'BEEF': 3.19,
        'CHK': 2.59,
        'FISH': 2.29,
        'HAM': 2.89,
        'MCH': 1.89,
        'MTL': 1.99,
        'SPG': 1.99,
        'TUR': 2.49,
    },
    key_name='FOOD',
    value_name='COST',
)

f_min = ParamDict1D(
    {
        'BEEF': 0,
        'CHK': 0,
        'FISH': 0,
        'HAM': 0,
        'MCH': 0,
        'MTL': 0,
        'SPG': 0,
        'TUR': 0,
    },
    key_name='FOOD',
    value_name='MIN-QTY',
)

f_max = ParamDict1D(
    {
        'BEEF': 100,
        'CHK': 100,
        'FISH': 100,
        'HAM': 100,
        'MCH': 100,
        'MTL': 100,
        'SPG': 100,
        'TUR': 100,
    },
    key_name='FOOD',
    value_name='MAX-QTY',
)

n_min = ParamDict1D(
    {'A': 700, 'C': 700, 'B1': 700, 'B2': 700},
    key_name='NUTRIENT',
    value_name='MIN-AMT',
)

n_max = ParamDict1D(
    {'A': 10000, 'C': 10000, 'B1': 10000, 'B2': 10000},
    key_name='NUTRIENT',
    value_name='MAX-AMT',
)

amt = ParamDictND(
    {
        ('A', 'BEEF'): 60,
        ('C', 'BEEF'): 20,
        ('B1', 'BEEF'): 10,
        ('B2', 'BEEF'): 15,
        ('A', 'CHK'): 8,
        ('C', 'CHK'): 0,
        ('B1', 'CHK'): 20,
        ('B2', 'CHK'): 20,
        ('A', 'FISH'): 8,
        ('C', 'FISH'): 10,
        ('B1', 'FISH'): 15,
        ('B2', 'FISH'): 10,
        ('A', 'HAM'): 40,
        ('C', 'HAM'): 40,
        ('B1', 'HAM'): 35,
        ('B2', 'HAM'): 10,
        ('A', 'MCH'): 15,
        ('C', 'MCH'): 35,
        ('B1', 'MCH'): 15,
        ('B2', 'MCH'): 15,
        ('A', 'MTL'): 70,
        ('C', 'MTL'): 30,
        ('B1', 'MTL'): 15,
        ('B2', 'MTL'): 15,
        ('A', 'SPG'): 25,
        ('C', 'SPG'): 50,
        ('B1', 'SPG'): 25,
        ('B2', 'SPG'): 15,
        ('A', 'TUR'): 60,
        ('C', 'TUR'): 20,
        ('B1', 'TUR'): 15,
        ('B2', 'TUR'): 10,
    },
    key_names=('FOOD', 'NUTR'),
    value_name='AMT',
)
# fmt: off

Implement with DOcplex#

from docplex.mp.model import Model

from opti_extensions.docplex import add_variables, solve

# Instantiate model
model = Model(name='diet')

# Add variables
buy = add_variables(model, indexset=FOOD, vartype='continuous', lb=f_min, ub=f_max, name='BUY-QTY')
# Instead of:
# buy = model.continuous_var_dict(FOOD, lb=[f_min[j] for j in FOOD], ub=[f_max[j] for j in FOOD], name='BUY-QTY')

# Set objective
model.minimize(
    cost @ buy
    # Instead of:
    # model.sum(cost[j] * buy[j] for j in FOOD)
)

# Add constraints
model.add_constraints_(
    model.sum(amt[i, j] * buy[j] for j in FOOD) >= n_min[i]
    for i in NUTR
)
model.add_constraints_(
    model.sum(amt[i, j] * buy[j] for j in FOOD) <= n_max[i]
    for i in NUTR
)

# Solve with additional logging output for problem and solution statistics
sol = solve(model, log_output=True)
# Instead of:
# sol = model.solve(log_output=True)
------------------------------  LP problem statistics  ------------------------------

Problem name         : diet
Objective sense      : Minimize
Variables            :       8  [Box: 8]
Objective nonzeros   :       8
Linear constraints   :       8  [Less: 4,  Greater: 4]
  Nonzeros           :      62
  RHS nonzeros       :       8

Variables            : Min LB: 0.000000         Max UB: 100.0000
Objective nonzeros   : Min   : 1.890000         Max   : 3.190000
Linear constraints   :
  Nonzeros           : Min   : 8.000000         Max   : 70.00000
  RHS nonzeros       : Min   : 700.0000         Max   : 10000.00

-------------------------------  CPLEX optimizer log  -------------------------------

Version identifier: 22.1.2.0 | 2026-03-02 | af0ce9b93
CPXPARAM_Read_DataCheck                          1
Tried aggregator 1 time.
No LP presolve or aggregator reductions.
Presolve time = 0.00 sec. (0.01 ticks)

Iteration log . . .
Iteration:     1   Dual objective     =            88.200000

---------------------------  Solution quality statistics  ---------------------------

There are no bound infeasibilities.
There are no reduced-cost infeasibilities.
Max. unscaled (scaled) Ax-b resid.          = 6.89226e-13 (1.07692e-14)
Max. unscaled (scaled) c-B'pi resid.        = 2.22045e-16 (2.22045e-16)
Max. unscaled (scaled) |x|                  = 46.6667 (46.6667)
Max. unscaled (scaled) |slack|              = 9300 (581.25)
Max. unscaled (scaled) |pi|                 = 0.126 (2.016)
Max. unscaled (scaled) |red-cost|           = 1.63 (2.06)
Condition number of scaled basis            = 2.3e+00

-------------------------------------------------------------------------------------
sol.display(print_zeros=False)
solution for: diet
objective: 88.200
status: OPTIMAL_SOLUTION(2)
BUY-QTY_MCH = 46.667

Implement with gurobipy#

from gurobipy import GRB, Model, quicksum

from opti_extensions.gurobipy import addVars

# Instantiate model
model = Model(name='diet')

# Add variables
buy = addVars(model, indexset=FOOD, lb=f_min, ub=f_max, vtype=GRB.CONTINUOUS, name='BUY-QTY')
# Instead of:
# buy = model.addVars(FOOD, lb=f_min, ub=f_max, vtype=GRB.CONTINUOUS, name='BUY-QTY')

# Set objective
model.setObjective(
    cost @ buy,
    # Instead of:
    # quicksum(cost[j] * buy[j] for j in FOOD)
    sense=GRB.MINIMIZE,
)

# Add constraints
model.addConstrs(
    quicksum(amt[i, j] * buy[j] for j in FOOD) >= n_min[i] for i in NUTR
)
model.addConstrs(
    quicksum(amt[i, j] * buy[j] for j in FOOD) <= n_max[i] for i in NUTR
)

# Solve
model.optimize()
Restricted license - for non-production use only - expires 2027-11-29
Gurobi Optimizer version 13.0.1 build v13.0.1rc0 (linux64 - "Ubuntu 22.04.3 LTS")

CPU model: AMD EPYC 7R13 Processor, instruction set [SSE2|AVX|AVX2]
Thread count: 1 physical cores, 2 logical processors, using up to 2 threads

Optimize a model with 8 rows, 8 columns and 62 nonzeros (Min)
Model fingerprint: 0x82124c12
Model has 8 linear objective coefficients
Coefficient statistics:
  Matrix range     [8e+00, 7e+01]
  Objective range  [2e+00, 3e+00]
  Bounds range     [1e+02, 1e+02]
  RHS range        [7e+02, 1e+04]

Presolve removed 4 rows and 0 columns
Presolve time: 0.00s
Presolved: 4 rows, 12 columns, 35 nonzeros

Iteration    Objective       Primal Inf.    Dual Inf.      Time
       0    0.0000000e+00   5.468750e+01   0.000000e+00      0s
       1    8.8200000e+01   0.000000e+00   0.000000e+00      0s

Solved in 1 iterations and 0.00 seconds (0.00 work units)
Optimal objective  8.820000000e+01
model.printAttr('X')
    Variable            X
-------------------------
BUY-QTY[MCH]      46.6667

Implement with Xpress#

import xpress as xp

from opti_extensions.xpress import addVariables

# Instantiate problem
prob = xp.problem(name='diet')

# Add variables
buy = addVariables(prob, indexset=FOOD, vartype=xp.continuous, lb=f_min, ub=f_max, name='BUY-QTY')
# Instead of:
# buy = {
#     j: prob.addVariable(name=f'x({j})', ub=f_min.get(j, 0), ub=f_max.get(j, xp.infinity), vartype=xp.continuous)
#     for j in FOOD
# }

# Set objective
prob.setObjective(
    cost @ buy,
    # Instead of:
    # xp.Sum(cost[j] * buy[j] for j in FOOD)
    sense=xp.minimize,
)

# Add constraints
prob.addConstraint(
    xp.Sum(amt[i, j] * buy[j] for j in FOOD) >= n_min[i]
    for i in NUTR
)
prob.addConstraint(
    xp.Sum(amt[i, j] * buy[j] for j in FOOD) <= n_max[i]
    for i in NUTR
)

# Solve
prob.optimize()
/home/docs/checkouts/readthedocs.org/user_builds/opti-extensions/checkouts/latest/examples/model_building/example_build_diet.py:268: LicenseWarning: Using the Community license in this session. If you have a full Xpress license, pass the full path to your license file to xpress.init(). If you want to use the FICO Community license and no longer want to see this message, use the following code before using the xpress module:
  xpress.init('/home/docs/checkouts/readthedocs.org/user_builds/opti-extensions/envs/latest/lib/python3.10/site-packages/xpress/license/community-xpauth.xpr')
  prob = xp.problem(name='diet')
FICO Xpress v9.8.1, Community, solve started 19:57:17, Apr 11, 2026
Heap usage: 430KB (peak 430KB, 73KB system)
Minimizing LP diet using up to 2 threads and up to 7734MB memory, with these control settings:
OUTPUTLOG = 1
NLPPOSTSOLVE = 1
XSLP_DELETIONCONTROL = 0
XSLP_OBJSENSE = 1
Original problem has:
         8 rows            8 cols           62 elements
Presolved problem has:
         4 rows            8 cols           31 elements
Presolve finished in 0 seconds
Heap usage: 430KB (peak 444KB, 73KB system)

Coefficient range                    original                 solved
  Coefficients   [min,max] : [ 8.00e+00,  7.00e+01] / [ 1.25e-01,  1.56e+00]
  RHS and bounds [min,max] : [ 1.00e+02,  1.00e+04] / [ 1.00e+02,  6.25e+02]
  Objective      [min,max] : [ 1.89e+00,  3.19e+00] / [ 1.89e+00,  3.19e+00]
Autoscaling applied standard scaling

Starting concurrent solve with dual (1 thread)

 Concurrent-Solve,   0s
            Dual
    objective   dual inf
 D  88.200000   .0000000

------- optimal --------
Concurrent statistics:
           Dual: 1 simplex iterations, 0.00s
Uncrunching matrix
Optimal solution found

   Its         Obj Value      S   Ninf  Nneg   Sum Dual Inf  Time
     1         88.200000      D      0     0        .000000     0
Dual solved problem
  1 simplex iterations in 0.00 seconds at time 0

Final objective                       : 8.819999999999999e+01
  Max primal violation      (abs/rel) :       0.0 /       0.0
  Max dual violation        (abs/rel) :       0.0 /       0.0
  Max complementarity viol. (abs/rel) :       0.0 /       0.0

(<SolveStatus.COMPLETED: 3>, <SolStatus.OPTIMAL: 1>)
print(f'var: {"value" :>20}')
print('-' * 25)
for k, v in prob.getSolution(buy).items():
    if abs(v) > 1e-6:
        name = f'{buy.value_name}[{k}]:'
        print(f'{name:<15}  {v :>8.4f}')
var:                value
-------------------------
BUY-QTY[MCH]:     46.6667

Implement with highspy#

from highspy import Highs, HighsVarType

from opti_extensions.highspy import addVariables as addVariablesHighs

# Instantiate model
model = Highs()
model.silent()

# Add variables
buy = addVariablesHighs(
    model,
    indexset=FOOD,
    lb=f_min,
    ub=f_max,
    type=HighsVarType.kContinuous,
    name_prefix='BUY-QTY',
)
# Instead of:
# buy = model.addVariables(
#     FOOD, lb=f_min, ub=f_max, type=HighsVarType.kContinuous, name_prefix='BUY-QTY'
# )

# Set objective
model.minimize(
    cost @ buy
    # Instead of:
    # Highs.qsum(cost[j] * buy[j] for j in FOOD)
)

# Add constraints
model.addConstrs(
    Highs.qsum(amt[i, j] * buy[j] for j in FOOD) >= n_min[i]
    for i in NUTR
)
model.addConstrs(
    Highs.qsum(amt[i, j] * buy[j] for j in FOOD) <= n_max[i]
    for i in NUTR
)

# Solve
model.solve()
<HighsStatus.kOk: 0>
print(f'{"var:":<15} {"value":>8}')
print('-' * 25)
for idx, var in buy.items():
    val = model.val(var)
    if abs(val) > 1e-6:
        name = f'{buy.value_name}[{idx}]:'
        print(f'{name:<15}  {val:>8.4f}')
var:               value
-------------------------
BUY-QTY[MCH]:     46.6667

Total running time of the script: (0 minutes 0.025 seconds)

Gallery generated by Sphinx-Gallery