How to create variables

In PuLP 4 variables are created by the problem and belong to it. Create the problem first, then its variables:

from pulp import LpProblem, LpMinimize, LpBinary, LpInteger

prob = LpProblem("example", LpMinimize)
x = prob.add_variable("x", lowBound=0, upBound=10)

A variable cannot be used in another problem. Adding an expression that uses a variable from another problem raises PulpError.

One variable: add_variable

add_variable() takes a name, the bounds and the category:

x = prob.add_variable("x", lowBound=0)                 # x >= 0
y = prob.add_variable("y", upBound=5, cat=LpInteger)   # integer, y <= 5
z = prob.add_variable("z", cat=LpBinary)               # 0 or 1
  • lowBound and upBound default to None, which means no bound. A bound must be finite; pass None instead of float("inf").

  • cat is "Continuous" (default), "Integer" or "Binary". A binary variable is stored as an integer variable with bounds 0 and 1, so z.cat returns "Integer". Use z.isBinary() to check for binaries.

  • The variable is part of the problem as soon as it is created, even if no constraint uses it yet.

Many variables: add_variable_dicts

Most models need one variable per item, or per combination of items. add_variable_dicts() creates them all in one call and returns them in a dictionary keyed by the index values. Use it in place of the 3.x LpVariable.dicts. The arguments are:

  • name: the prefix of the variable names.

  • indices: one iterable of index values, or a tuple of iterables for several indices.

  • lowBound, upBound, cat: the same as for add_variable. They apply to every variable.

With one index you get a flat dictionary. Each name is the prefix and the index value joined by _:

warehouses = ["A", "B"]
bars = [1, 2, 3]

buy = prob.add_variable_dicts("buy", warehouses, lowBound=0)
# {'A': buy_A, 'B': buy_B}
buy["A"]          # the variable called buy_A

With a tuple of indices you get nested dictionaries, one level per index:

flow = prob.add_variable_dicts("flow", (warehouses, bars), lowBound=0, cat=LpInteger)
flow["A"][2]      # the variable called flow_A_2
flow["A"]         # {1: flow_A_1, 2: flow_A_2, 3: flow_A_3}

The nested dictionaries work well with lpSum:

from pulp import lpSum

supply = {"A": 1000, "B": 4000}
demand = {1: 500, 2: 900, 3: 1800}
for w in warehouses:
    prob += lpSum(flow[w][b] for b in bars) <= supply[w], f"supply_{w}"
for b in bars:
    prob += lpSum(flow[w][b] for w in warehouses) >= demand[b], f"demand_{b}"

Choosing the names

If name contains %s, PuLP fills in one %s per index instead of adding _%s for each:

route = prob.add_variable_dicts("route_%s_to_%s", (warehouses, bars))
route["B"][3].name   # 'route_B_to_3'

Index values are converted with str(), and characters that LP and MPS files cannot hold, such as spaces, are replaced by _. For example, the index "new york" gives the name ship_new_york. Give each family of variables its own prefix. Two variables with the same name are accepted, but writeLP, writeMPS and the solvers that go through those files raise PulpError: Repeated variable names.

Binary variables

cat=LpBinary creates integer variables with bounds 0 and 1 in every level of the dictionary:

is_open = prob.add_variable_dicts("open", warehouses, cat=LpBinary)
is_open["A"].isBinary()   # True

Other shapes: add_variable_dict and add_variable_matrix

Both take the same arguments as add_variable_dicts:

  • add_variable_dict() returns one flat dictionary keyed by tuples, one key per combination of the indices:

    assign = prob.add_variable_dict("assign", (warehouses, bars), cat=LpBinary)
    assign["A", 1]    # the variable called assign_A_1
    
  • add_variable_matrix() returns nested lists, indexed by position instead of by value:

    grid = prob.add_variable_matrix("grid", (warehouses, bars))
    grid[0][1]        # the variable called grid_A_2
    

Use whichever shape makes your constraints easier to write. add_variable_dicts is the usual choice.

Changing a variable after it is created

The bounds and the category of a variable can be changed later. This is useful, for example, to solve the linear relaxation of a problem and then make it integer again:

x.lowBound = 2
x.upBound = None        # no upper bound
x.cat = LpInteger
x.cat = "Continuous"    # relax it
x.cat = LpBinary        # integer with bounds 0 and 1

To add a variable to constraints that already exist, see How to model column-wise.