Inspecting AMPL Models: expand and show with amplpy#
Description: Demonstrates the ampl.show(), ampl.expand(), and entity-level expand() methods introduced in amplpy 0.17.0, using the classic diet problem as a running example.
Tags: amplpy, api, expand, show, diet, highlights
Notebook author: Jürgen Lentz <lentz@ampl.com>
When building an optimization model it is often useful to inspect what AMPL has actually understood from your model and data — especially after substituting concrete parameter values.
amplpy 0.17.0 introduced two new methods on the AMPL object and a companion method on every model entity:
Method |
What it returns |
|---|---|
|
A compact catalogue of every declared entity in the model |
|
Every constraint and objective written out with parameter values substituted |
|
The same expansion restricted to a single entity (constraint, variable, or objective) |
|
Expansion of a single indexed instance, e.g. |
This notebook walks through each method using the well-known diet problem.
# Install dependencies
%pip install -q amplpy
# Google Colab & Kaggle integration
from amplpy import AMPL, ampl_notebook
ampl = ampl_notebook(
modules=["highs"], # modules to install
license_uuid="default", # license to use
) # instantiate AMPL object and register magics
The diet problem#
The classic diet problem asks: what is the cheapest combination of food packages that still meets weekly nutritional requirements?
Sets
FOOD— available food itemsNUTR— nutrients to track
Parameters
cost[j]— cost per package of foodjf_min[j],f_max[j]— lower/upper bound on packages of foodjn_min[i],n_max[i]— minimum/maximum amount of nutrientiamt[i,j]— percentage of daily requirement of nutrientiper package of foodj
Variables
Buy[j]— number of packages of foodjto purchase
Objective
Minimise total cost: \(\sum_{j \in \text{FOOD}} \text{cost}_j \cdot \text{Buy}_j\)
Constraint
For every nutrient \(i\): \(n\_min_i \le \sum_{j} amt_{ij} \cdot Buy_j \le n\_max_i\)
Define the model#
from amplpy import AMPL
ampl = AMPL()
ampl.eval("""
set FOOD;
set NUTR;
param cost {FOOD} > 0;
param f_min {FOOD} >= 0;
param f_max {j in FOOD} >= f_min[j];
param n_min {NUTR} >= 0;
param n_max {i in NUTR} >= n_min[i];
param amt {NUTR, FOOD} >= 0;
var Buy {j in FOOD} >= f_min[j], <= f_max[j];
minimize Total_Cost: sum {j in FOOD} cost[j] * Buy[j];
subject to Diet {i in NUTR}:
n_min[i] <= sum {j in FOOD} amt[i,j] * Buy[j] <= n_max[i];
""")
Load the data#
import pandas as pd
import numpy as np
foods = ["BEEF", "CHK", "FISH", "HAM", "MCH", "MTL", "SPG", "TUR"]
nutrs = ["A", "B1", "B2", "C"]
food_df = pd.DataFrame(
{
"cost": [3.19, 2.59, 2.29, 2.89, 1.89, 1.99, 1.99, 2.49],
"f_min": [0] * 8,
"f_max": [100] * 8,
},
index=pd.Index(foods, name="FOOD"),
)
nutr_df = pd.DataFrame(
{
"n_min": [700] * 4,
"n_max": [10000] * 4,
},
index=pd.Index(nutrs, name="NUTR"),
)
amt_df = pd.DataFrame(
np.array(
[
[60, 8, 8, 40, 15, 70, 25, 60], # A
[10, 20, 15, 35, 15, 15, 25, 15], # B1
[15, 20, 10, 10, 15, 15, 15, 10], # B2
[20, 0, 10, 40, 35, 30, 50, 20], # C
]
),
index=pd.Index(nutrs, name="NUTR"),
columns=pd.Index(foods, name="FOOD"),
)
ampl.set["FOOD"] = foods
ampl.set["NUTR"] = nutrs
ampl.set_data(food_df, "FOOD")
ampl.set_data(nutr_df, "NUTR")
ampl.param["amt"] = amt_df
ampl.show() — catalogue of declared entities#
ampl.show() returns a brief listing of every entity declared in the model, grouped by kind (sets, parameters, variables, constraints, objectives).
It is the quickest way to confirm that the model was read correctly and to get an overview of its structure.
print(ampl.show())
parameters: amt cost f_max f_min n_max n_min
sets: FOOD NUTR
variable: Buy
constraint: Diet
objective: Total_Cost
ampl.expand() — full model with data substituted#
ampl.expand() returns every constraint instance and the objective written out with the concrete parameter values from the current data.
This is the easiest way to verify that parameter values have been loaded as expected and that the model structure is correct before solving.
print(ampl.expand())
minimize Total_Cost:
3.19*Buy['BEEF'] + 2.59*Buy['CHK'] + 2.29*Buy['FISH'] + 2.89*Buy['HAM']
+ 1.89*Buy['MCH'] + 1.99*Buy['MTL'] + 1.99*Buy['SPG'] +
2.49*Buy['TUR'];
subject to Diet['A']:
700 <= 60*Buy['BEEF'] + 8*Buy['CHK'] + 8*Buy['FISH'] + 40*Buy['HAM'] +
15*Buy['MCH'] + 70*Buy['MTL'] + 25*Buy['SPG'] + 60*Buy['TUR'] <= 10000;
subject to Diet['B1']:
700 <= 10*Buy['BEEF'] + 20*Buy['CHK'] + 15*Buy['FISH'] + 35*Buy['HAM']
+ 15*Buy['MCH'] + 15*Buy['MTL'] + 25*Buy['SPG'] + 15*Buy['TUR'] <=
10000;
subject to Diet['B2']:
700 <= 15*Buy['BEEF'] + 20*Buy['CHK'] + 10*Buy['FISH'] + 10*Buy['HAM']
+ 15*Buy['MCH'] + 15*Buy['MTL'] + 15*Buy['SPG'] + 10*Buy['TUR'] <=
10000;
subject to Diet['C']:
700 <= 20*Buy['BEEF'] + 10*Buy['FISH'] + 40*Buy['HAM'] + 35*Buy['MCH']
+ 30*Buy['MTL'] + 50*Buy['SPG'] + 20*Buy['TUR'] <= 10000;
Entity-level expand() — inspecting a single entity#
Each model entity (constraint, variable, objective) exposes its own expand() method.
The output depends on the type of entity:
Constraint — the expanded algebraic form of every instance in that constraint family.
Variable — the coefficient of that variable in every constraint and objective where it appears.
Objective — the expanded algebraic form of the objective.
Expanding a constraint#
ampl.con['Diet'].expand() prints each Diet[i] instance with its bounds and the substituted expression.
print(ampl.con["Diet"].expand())
subject to Diet['A']:
700 <= 60*Buy['BEEF'] + 8*Buy['CHK'] + 8*Buy['FISH'] + 40*Buy['HAM'] +
15*Buy['MCH'] + 70*Buy['MTL'] + 25*Buy['SPG'] + 60*Buy['TUR'] <= 10000;
subject to Diet['B1']:
700 <= 10*Buy['BEEF'] + 20*Buy['CHK'] + 15*Buy['FISH'] + 35*Buy['HAM']
+ 15*Buy['MCH'] + 15*Buy['MTL'] + 25*Buy['SPG'] + 15*Buy['TUR'] <=
10000;
subject to Diet['B2']:
700 <= 15*Buy['BEEF'] + 20*Buy['CHK'] + 10*Buy['FISH'] + 10*Buy['HAM']
+ 15*Buy['MCH'] + 15*Buy['MTL'] + 15*Buy['SPG'] + 10*Buy['TUR'] <=
10000;
subject to Diet['C']:
700 <= 20*Buy['BEEF'] + 10*Buy['FISH'] + 40*Buy['HAM'] + 35*Buy['MCH']
+ 30*Buy['MTL'] + 50*Buy['SPG'] + 20*Buy['TUR'] <= 10000;
Expanding a variable#
ampl.var['Buy'].expand() lists every constraint and objective in which each Buy[j] instance appears, together with its coefficient.
print(ampl.var["Buy"].expand())
Coefficients of Buy['BEEF']:
Diet['A'] 60
Diet['B1'] 10
Diet['B2'] 15
Diet['C'] 20
Total_Cost 3.19
Coefficients of Buy['CHK']:
Diet['A'] 8
Diet['B1'] 20
Diet['B2'] 20
Total_Cost 2.59
Coefficients of Buy['FISH']:
Diet['A'] 8
Diet['B1'] 15
Diet['B2'] 10
Diet['C'] 10
Total_Cost 2.29
Coefficients of Buy['HAM']:
Diet['A'] 40
Diet['B1'] 35
Diet['B2'] 10
Diet['C'] 40
Total_Cost 2.89
Coefficients of Buy['MCH']:
Diet['A'] 15
Diet['B1'] 15
Diet['B2'] 15
Diet['C'] 35
Total_Cost 1.89
Coefficients of Buy['MTL']:
Diet['A'] 70
Diet['B1'] 15
Diet['B2'] 15
Diet['C'] 30
Total_Cost 1.99
Coefficients of Buy['SPG']:
Diet['A'] 25
Diet['B1'] 25
Diet['B2'] 15
Diet['C'] 50
Total_Cost 1.99
Coefficients of Buy['TUR']:
Diet['A'] 60
Diet['B1'] 15
Diet['B2'] 10
Diet['C'] 20
Total_Cost 2.49
Expanding an objective#
ampl.obj['Total_Cost'].expand() shows the objective expression with parameter values substituted.
print(ampl.obj["Total_Cost"].expand())
minimize Total_Cost:
3.19*Buy['BEEF'] + 2.59*Buy['CHK'] + 2.29*Buy['FISH'] + 2.89*Buy['HAM']
+ 1.89*Buy['MCH'] + 1.99*Buy['MTL'] + 1.99*Buy['SPG'] +
2.49*Buy['TUR'];
Instance-level expand() — a single indexed instance#
Indexing into an entity returns a specific instance, which also has expand().
This is useful when you only need to inspect one particular row of a constraint family or one variable.
A single constraint instance#
print(ampl.con["Diet"]["A"].expand())
subject to Diet['A']:
700 <= 60*Buy['BEEF'] + 8*Buy['CHK'] + 8*Buy['FISH'] + 40*Buy['HAM'] +
15*Buy['MCH'] + 70*Buy['MTL'] + 25*Buy['SPG'] + 60*Buy['TUR'] <= 10000;
A single variable instance#
print(ampl.var["Buy"]["BEEF"].expand())
Coefficients of Buy['BEEF']:
Diet['A'] 60
Diet['B1'] 10
Diet['B2'] 15
Diet['C'] 20
Total_Cost 3.19
Solving and verifying#
After inspection we can solve the model as usual.
ampl.solve(solver="highs")
assert ampl.solve_result == "solved", ampl.solve_result
print(f"Optimal cost: ${ampl.obj['Total_Cost'].value():.2f}")
print()
print(ampl.var["Buy"].to_pandas().to_string())
HiGHS 1.7.1:HiGHS 1.7.1: optimal solution; objective 88.2
1 simplex iterations
0 barrier iterations
Optimal cost: $88.20
Buy.val
BEEF 0.000000
CHK 0.000000
FISH 0.000000
HAM 0.000000
MCH 46.666667
MTL 0.000000
SPG 0.000000
TUR 0.000000
Summary#
amplpy method |
Scope |
Typical use |
|---|---|---|
|
Entire model |
Quick structural overview |
|
Entire model |
Verify all data is loaded correctly |
|
One constraint family |
Check a constraint’s expansion |
|
One variable |
See where a variable appears and its coefficients |
|
One objective |
Inspect the objective expression |
|
One instance |
Focus on a single indexed constraint |
|
One instance |
Focus on a single indexed variable |
All of these methods require amplpy ≥ 0.17.0.