2025, Dec 09 01:00

A Working AMPL + HiGHS Sequencing Model to Minimize Mold Changeovers: Common Pitfalls and the Fixes

Build a feasible AMPL + HiGHS sequencing model to minimize mold changeovers: fix syntax issues, use non-strict integer bounds, and apply an alldiff constraint.

Minimizing mold changeovers looks simple at first glance: you have a set of jobs, each tied to a mold, and a matrix with the cost to switch from one mold to another. The goal is to find a job order that minimizes the total shift cost. In practice, one tiny modeling mistake can make the solver report infeasibility or fail to start. Below is a compact walkthrough of a working AMPL+HiGHS setup and the exact pitfalls that blocked a feasible solution.

Problem setup

Each job requires exactly one mold. Switching molds incurs a cost defined by a mold-to-mold matrix. We want a permutation of jobs that minimizes the sum of mold shift costs between consecutive jobs.

Code that triggers the issue

The following snippets reproduce the setup and the modeling pattern that led to an infeasible run. The core problem sits in the model file.

%pip install -q amplpy

ENGINE = "highs"

from amplpy import AMPL, ampl_notebook

am = ampl_notebook(
    modules=["highs"],
    license_uuid="default",
)
import pandas as pd

job_to_mold = pd.DataFrame(
    {
        "Job1": {"mold": 1},
        "Job2": {"mold": 2},
        "Job3": {"mold": 1},
        "Job4": {"mold": 3},
        "Job5": {"mold": 2},
        "Job6": {"mold": 3},
        "Job7": {"mold": 2},
    }
).T

mold_move_cost = pd.DataFrame(
    {
        "mold1": {"mold1": 0, "mold2": 2, "mold3": 4},
        "mold2": {"mold1": 2, "mold2": 0, "mold3": 4},
        "mold3": {"mold1": 7, "mold2": 1, "mold3": 0},
    }
).T
%%writefile MfgSequence.mod
set JOBSET;
set TOOLSET;
set jj within {JOBSET,JOBSET};
set mm within {TOOLSET,TOOLSET};
set TURNIDX;

param cost_job{jj};
param cost_mold{mm};
param nj;
param nm;
param nturns;

var pos{JOBSET} integer >=0,<nj

minimize obj:
    sum{k in TURNIDX, a in JOBSET, b in JOBSET}
        (if pos[k]==a and pos[k+1]==b then 1 else 0) * cost_job[a,b];

subject to no_ties{a in JOBSET, b in JOBSET}:
    (pos[a] > pos[b] or pos[a] < pos[b]);
def build_sequence_model(job_to_mold, mold_move_cost):
    mdl = AMPL()
    mdl.read("MfgSequence.mod")

    nj = len(job_to_mold)
    JOBSET = range(nj)
    TURNIDX = range(nj - 1)
    nturns = len(TURNIDX)
    jj_pairs = {(i, j) for i in JOBSET for j in JOBSET}

    mdl.param["nj"] = nj
    mdl.set["JOBSET"] = JOBSET
    mdl.set["TURNIDX"] = TURNIDX
    mdl.set["jj"] = jj_pairs
    mdl.param["nturns"] = nturns

    nm = len(mold_move_cost)
    TOOLSET = range(nm)
    mm_pairs = {(i, j) for i in TOOLSET for j in TOOLSET}

    mdl.param["nm"] = nm
    mdl.set["TOOLSET"] = TOOLSET
    mdl.set["mm"] = mm_pairs

    mcost = mold_move_cost.values
    mdl.param["cost_mold"] = mcost

    j2m = job_to_mold.values
    job_cost = {
        (i, j): mcost[(k, l)].tolist()
        for (i, j) in jj_pairs
        for (k, l) in mm_pairs
        if (j2m[i] - 1) == k and (j2m[j] - 1) == l
    }
    mdl.param["cost_job"] = job_cost

    return mdl
opt = build_sequence_model(job_to_mold, mold_move_cost)
opt.solve(solver=ENGINE)
print(opt.solve_result)
opt.display("pos")

Why this fails

The infeasibility is not coming from the math of the sequencing objective. The model declaration itself blocks the solve. The first blocker is a syntax error: the variable declaration line misses the semicolon, so AMPL never gets a proper model to solve.

Once the syntax is fixed, there is a second modeling issue. Using a strict upper bound for an integer variable makes HiGHS unable to infer usable bounds. The variable domain was specified with a strict inequality, which prevents the solver from getting valid bounds. Since the variable is integer, the upper bound must be non-strict and set to one less than the count. In other words, replace the strict upper bound with a non-strict one that is exactly one unit lower.

There is also a cleaner way to enforce that every job appears exactly once in the sequence. Instead of modeling pairwise inequality via a logical disjunction, use an alldiff constraint over the position variables. This directly expresses that all positions are distinct and captures the idea of a permutation.

Fix and a compact model

The objective is determined purely by transitions between consecutive jobs and the associated mold shift costs. The formulation below addresses the syntax issue, uses an alldiff constraint for the permutation, and adopts the correct non-strict bound.

set JOBSET;
set TOOLSET;
set jj within {JOBSET,JOBSET};
set mm within {TOOLSET,TOOLSET};
set TURNIDX;

param cost_job{jj};
param cost_mold{mm};
param nj;
param nm;
param nturns;

var pos{JOBSET} integer >= 0, <= nj - 1;

minimize total_cost:
    sum{k in TURNIDX, a in JOBSET, b in JOBSET}
        if pos[k]==a and pos[k+1]==b then cost_job[a,b];

subject to visit_every_job:
    alldiff{k in JOBSET} pos[k];

With this formulation in place, the rest of the Python integration remains the same pattern. You read the model file, load JOBSET, TURNIDX, and the job-to-mold-derived cost matrix, and solve with HiGHS.

model = build_sequence_model(job_to_mold, mold_move_cost)
model.solve(solver=ENGINE, verbose=True)
assert model.solve_result == "solved", model.solve_result
sol = model.var['pos'].to_dict()
print(sol)

For the provided data, the solution is:

{0: 3, 1: 5, 2: 1, 3: 4, 4: 6, 5: 2, 6: 0}

Why it matters

Sequence models are sensitive to small formulation details. A missing semicolon prevents AMPL from even parsing the model. A strict inequality on an integer variable propagates as a missing bound for the solver. And expressing permutations with an alldiff is both clearer and less error-prone than manually encoding pairwise inequalities with logical disjunctions.

Conclusion

If a sequencing model unexpectedly has no feasible solution, start by checking the model’s syntax and the bounds of integer variables. Replace strict bounds with non-strict ones where appropriate, and use an alldiff constraint to assert that every position is unique. Keeping the objective written directly in terms of consecutive transitions makes the formulation compact and aligns naturally with the cost matrix of mold changes.