Converting existing code

You may already have a working model — in AMPL, in PuLP, or in an older gurobipy script — and want it in a Python codebase the rest of the team can maintain. Modeler handles that as a conversion of the whole project rather than of the file: it recovers the specification the original was implementing, then generates the implementation and the tests from that specification, so the port enters the loop like any other model.

Gurobot converts the file you hand it: read the source, produce the gurobipy equivalent, run both to check the numbers agree. Modeler treats the same request as a project instead of a translation — the output is not just code, but a specification and a test suite alongside it, so the port is something you can keep passing when the model changes rather than a snapshot that only matched the original on the day it was written.

This is the facility problem again, this time written in AMPL rather than PuLP:

facility.mod
set PLANTS;
set WAREHOUSES;

param demand{WAREHOUSES} >= 0;
param capacity{PLANTS} >= 0;
param fixed_cost{PLANTS} >= 0;
param transport_cost{PLANTS, WAREHOUSES} >= 0;

var Ship{PLANTS, WAREHOUSES} >= 0;
var Open{PLANTS} binary;

minimize TotalCost:
    sum {p in PLANTS, w in WAREHOUSES} transport_cost[p, w] * Ship[p, w]
    + sum {p in PLANTS} fixed_cost[p] * Open[p];

subject to DemandSatisfied {w in WAREHOUSES}:
    sum {p in PLANTS} Ship[p, w] = demand[w];

subject to CapacityLimit {p in PLANTS}:
    sum {w in WAREHOUSES} Ship[p, w] <= capacity[p] * Open[p];
facility.dat
data;

set PLANTS := 1 2 3 4 5;
set WAREHOUSES := 1 2 3 4;

param demand :=
    1 15
    2 18
    3 14
    4 20;

param capacity :=
    1 20
    2 22
    3 17
    4 19
    5 18;

param fixed_cost :=
    1 12000
    2 15000
    3 17000
    4 13000
    5 16000;

param transport_cost:
         1    2    3    4 :=
    1  4000 2500 1200 2200
    2  2000 2600 1800 2600
    3  3000 3400 2600 3100
    4  2500 3000 4100 3700
    5  4500 4000 3000 3200;

facility.mod, facility.dat attached

We have been running this in AMPL against the Gurobi solver. AMPL is not something the rest of the team can maintain, so we want it in our Python codebase — same problem, same data, with tests, not just a script that happens to print the right number today.

Reading the model. Two sets, PLANTS and WAREHOUSES; four parameters; a continuous Ship variable and a binary Open variable; one objective and two constraint families. This is the same fixed-charge facility location structure as the reference example in the knowledge base — cost-minimizing plant closure with a capacity-linking constraint — so the mapping to gurobipy is direct. Because the request is “put this in our codebase,” not “translate this file,” I should write the specification first: that is what makes the port something the team can read and maintain, not just a diff against the AMPL source. Then the implementation, then tests derived from the specification rather than copied from AMPL’s output.

This is a plant-closure model: which plants to keep open, minimizing transportation and fixed cost, subject to meeting every warehouse’s demand and never shipping from a closed plant. I have written it up as a specification first, since that is what the rest of the team will read instead of the AMPL source:

• Objective — transportation cost for everything shipped, plus the fixed cost of every plant kept open.
• C1 — Demand is met exactly. DemandSatisfied in the AMPL model — each warehouse receives exactly its demand.
• C2 — Plants ship within capacity, and only when open. CapacityLimit — an open plant ships at most its capacity; a closed plant ships nothing.
Learn More:
• Facility Examples ★★☆

The converted model

facility_modeler.py — the model-building core
with gp.Env() as env, gp.Model("facility", env=env) as model:
    if time_limit is not None:
        model.Params.TimeLimit = time_limit

    # ship[p, w]: thousands of products sent from plant p to warehouse w.
    ship = model.addVars(plants, warehouses, name="ship")
    # open_plant[p]: 1 if plant p stays open, 0 if it is closed.
    open_plant = model.addVars(plants, vtype=GRB.BINARY, name="open")

    model.setObjective(
        gp.quicksum(
            transport_costs[p, w] * ship[p, w]
            for p in plants
            for w in warehouses
        )
        + gp.quicksum(fixed_costs[p] * open_plant[p] for p in plants),
        GRB.MINIMIZE,
    )

    # C1: every warehouse receives exactly its demand.
    model.addConstrs(
        (ship.sum("*", w) == demands[w] for w in warehouses), name="demand"
    )
    # C2: a plant ships within its capacity, and nothing at all when closed.
    model.addConstrs(
        (ship.sum(p, "*") <= capacities[p] * open_plant[p] for p in plants),
        name="capacity",
    )

AMPL’s PLANTS and WAREHOUSES sets become the plants and warehouses lists; Ship and Open become tupledict variables from addVars; and DemandSatisfied / CapacityLimit become the two addConstrs calls, named the same way so a diagnosis later reads against the specification rather than against R1, R2.

Tests, not just a rerun

Gurobot’s conversion is checked by solving both versions once and comparing the objective. Modeler writes the check into the suite instead, so it keeps holding after this session ends:

def test_t1_baseline_closes_plant_3():
    """T1: same baseline AMPL solved — close plant 3, total cost 210,500."""
    result = run()

    assert result["status"] == "optimal"
    assert result["closed_plants"] == [3]
    assert result["total_cost"] == pytest.approx(210_500)

T1 is the same scenario the AMPL model was solving, and the assertion is the number AMPL reported for it — the test exists specifically to catch the port drifting from that answer, not just to exercise the code.

Where this differs from a Gurobot conversion

Gurobot

Modeler

Translates the file you attach.

Writes the specification the file was implementing, then the code, then tests.

Verifies by solving both once and comparing.

Leaves a test that keeps verifying every time the model changes.

Comes back with converted code.

Comes back with a project: spec, code, and tests, all three consistent with each other.

Reach for Gurobot when the AMPL file is a one-off you need translated today. Reach for Modeler when it is the thing your team will own going forward.