Inspecting AMPL Models: expand and show with amplpy#

expand_show.ipynb Open In Colab Open In Deepnote Open In Kaggle Open In Gradient Open In SageMaker Studio Lab Powered by AMPL

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

ampl.show()

A compact catalogue of every declared entity in the model

ampl.expand()

Every constraint and objective written out with parameter values substituted

entity.expand()

The same expansion restricted to a single entity (constraint, variable, or objective)

instance.expand()

Expansion of a single indexed instance, e.g. Diet['A'] or Buy['BEEF']

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 items

  • NUTR — nutrients to track

Parameters

  • cost[j] — cost per package of food j

  • f_min[j], f_max[j] — lower/upper bound on packages of food j

  • n_min[i], n_max[i] — minimum/maximum amount of nutrient i

  • amt[i,j] — percentage of daily requirement of nutrient i per package of food j

Variables

  • Buy[j] — number of packages of food j to 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

ampl.show()

Entire model

Quick structural overview

ampl.expand()

Entire model

Verify all data is loaded correctly

ampl.con['X'].expand()

One constraint family

Check a constraint’s expansion

ampl.var['X'].expand()

One variable

See where a variable appears and its coefficients

ampl.obj['X'].expand()

One objective

Inspect the objective expression

ampl.con['X']['i'].expand()

One instance

Focus on a single indexed constraint

ampl.var['X']['j'].expand()

One instance

Focus on a single indexed variable

All of these methods require amplpy ≥ 0.17.0.