Elements of the production model
Describes the details of this linear programming model.
The volsay model shown in A simple production problem (volsay.mod) is a linear programming model. Linear programming is the class of problems that can be expressed as the optimization of a linear objective function subject to a set of linear constraints (i.e., linear equations and inequalities) over real numbers. Linear programming models can be solved for large numbers of variables and constraints and are, from a computational standpoint, the simplest applications considered in this manual.
This section examines:
Arrays
Data declarations
Aggregate operators and quantifiers
Isolating the data
Data initialization
Tuples
Displaying results
Setting CPLEX parameters
Integer programming: the knapsack problem
Mixed integer-linear programming: a blending problem
For more information
Applications of linear and integer programming studies the application of OPL to linear programming, integer programming, mixed-integer linear programming, and piecewise-linear programming.
Arrays
The previous statement is very specific to the application at hand. In general, it is desirable to write generic models that can be extended, modified easily, and applied in different contexts. The next sections describe a number of OPL concepts to simplify the process of creating such models. A first step towards more genericity is the use of arrays, which makes it easier, for instance, to accommodate new products in the future.
The Volsay production planning model can be rewritten using arrays as:
The volsay production model with arrays
{string} Products = {"gas","chloride"};
dvar float production[Products];
maximize
40 * production["gas"] + 50 * production["chloride"];
subject to {
production["gas"] + production["chloride"] <= 50;
3 * production["gas"] + 4 * production["chloride"] <= 180;
production["chloride"] <= 40;
}
This new statement illustrates several features of the language. First, the instruction
{string} Products = {"gas","chloride"};
declares a set of strings Products that represents the set of products of the company. The declaration
dvar float production[Products];
declares an array of two decision variables, production["gas"] and production["chloride"], to represent the optimal production of ammoniac gas and ammonium chloride. These decision variables are used in the rest of the statement, which remains essentially the same as in A simple production problem (volsay.mod). As will become clear subsequently, one of the novel features of OPL is the generality of its arrays: OPL arrays can have an arbitrary number of dimensions and their index sets can be arbitrary finite sets, possibly involving complex data structures.
Data declarations
A second fundamental step towards more genericity in the model amounts to representing the
problem data explicitly. In addition to the products, the problem data obviously consists of the
components (nitrogen, hydrogen, and chloride), the demand of each product for each component, the
profit of each product, and the stock available for each component. The following example,
gas.dat, declares and initializes the data.
Declaring and initializing data (gas.dat)
Products = { "gas" "chloride" };
Components = { "nitrogen" "hydrogen" "chlorine" };
Demand = [ [1 3 0] [1 4 1] ];
Profit = [30 40];
Stock = [50 180 40];
The data element Components is a set of strings that defines the chemical components necessary for the products, demand is a two-dimensional array whose element demand[p][c] represents the demand of product p for component c, and profit and stock are two arrays representing the profit of each product and the stock available for each component. The rest of the statement can be obtained easily by replacing the numbers by the relevant data items. For instance, the objective function is simply written as
maximize
sum( p in Products )
Profit[p] * Production[p];
Aggregate operators and quantifiers
It should be clear, however, that the previous statement contains much redundancy. All constraints, and all arithmetic terms in these constraints and in the objective function, are similar: they differ only in their indices.
OPL has two features to factorize these commonalities, aggregate operators and quantifiers, as
shown in the following model, gas1.mod.
A simple production model (gas1.mod).
{string} Products = { "gas", "chloride" };
{string} Components = { "nitrogen", "hydrogen", "chlorine" };
float Demand[Products][Components] = [ [1, 3, 0], [1, 4, 1] ];
float Profit[Products] = [30, 40];
float Stock[Components] = [50, 180, 40];
dvar float+ Production[Products];
maximize
sum( p in Products )
Profit[p] * Production[p];
subject to {
forall( c in Components )
ct:
sum( p in Products )
Demand[p][c] * Production[p] <= Stock[c];
}
The objective function
maximize
sum( p in Products )
Profit[p] * Production[p];
illustrates the use of the aggregate operator sum to take the summation of the individual profits. A variety of aggregate operators are available in OPL, including sum, prod, min, and max.
The instruction
subject to {
forall( c in Components )
ct:
sum( p in Products )
Demand[p][c] * Production[p] <= Stock[c];
}
shows how the universal quantifier forall can be used to state closely related constraints. It generates one constraint for each chemical component, each constraint stating that the total demand for the component cannot exceed its available stock. OPL supports rich parameter specifications in aggregate operators and quantifiers (see Expressions in the Language Reference Manual).
Isolating the data
Another fundamental step in making models reusable is to separate the model and the instance data. OPL supports this clean separation through the notion of projects.
A project is the association of a model file, one or more data files (optional), and one or more settings files (optional), associated in run configurations. A minimal project has one run configuration containing only one model. Model files use the file name extension.mod while data files use the file name extension.dat. The model declares the data but does not initialize it. The data files contain the initialization instructions for each declared data item. See Understanding OPL projects in Quick Start.
Here we do not describe the details of IBM ILOG OPL, but generally describe applications by giving the model and the instance data separately.
For instance, the following model, gas.mod, and the instance data,
gas.dat, together make up a project for the Volsay production-planning problem. The
model part is essentially the same as the one presented earlier, except that it declares the data
but does not initialize it.
The production model (gas.mod)
{string} Products = ...;
{string} Components = ...;
float Demand[Products][Components] = ...;
float Profit[Products] = ...;
float Stock[Components] = ...;
dvar float+ Production[Products];
maximize
sum( p in Products )
Profit[p] * Production[p];
subject to {
forall( c in Components )
ct:
sum( p in Products )
Demand[p][c] * Production[p] <= Stock[c];
}
A declaration of the form
float profit[Products] = ...;
declares the array profit and specifies that its initialization is given in a data file. The data file simply associates an initialization with each non-initialized piece of data.
Instance data for the production model (gas.dat)
Products = { "gas" "chloride" };
Components = { "nitrogen" "hydrogen" "chlorine" };
Demand = [ [1 3 0] [1 4 1] ];
Profit = [30 40];
Stock = [50 180 40];
Data initialization
OPL offers a variety of ways of initializing data. One particularly useful feature is the
possibility of associating indices with values to avoid various kinds of errors. The following
instance data, gasn.dat, illustrates this feature on the instance data for the
Volsay production model.
Instance data with indices for the production model (gasn.dat)
Products = { "gas", "chloride" };
Components = { "nitrogen", "hydrogen", "chlorine" };
Profit = #["gas":30, "chloride":40]#;
Stock = #["nitrogen":50, "hydrogen":180, "chlorine":40]#;
Demand = #[
"gas": #[ "nitrogen":1 "hydrogen":3 "chlorine":0 ]#,
"chloride": #[ "nitrogen":1 "hydrogen":4 "chlorine":1 ]#
]#;
The initialization
profit = #["gas":30 "chloride":40]#;
describes the initialization of array profit by associating the value 30 with index gas and the value 40 with index chloride. (Of course, the order of the pairs has no importance in these initializations.) When using index:value pairs, the delimiters #[ and ]# must be used instead of [ and ]. Note also that, in data files, the items can be initialized in any order and the commas can be omitted freely.
Tuples
OPL offers a variety of data structures in addition to arrays and sets of strings. Tuples, a fundamental tool for structuring the application data, offer an alternative to the traditional approach of representing data in parallel arrays. To see the use of tuples in OPL, consider the following production-planning model. To meet the demands of its customers, a company manufactures its products in its own factories (inside production) or buys them from other companies (outside production).
Inside production is subject to some resource constraints: each product consumes a certain amount
of each resource. In contrast, outside production is theoretically unlimited. The problem is to
determine how much of each product should be produced inside and outside the company while
minimizing the overall production cost, meeting the demand, and satisfying the resource constraints.
The following example, production.mod, depicts an OPL model for this problem that
uses only the concepts introduced so far, and production.dat presents the data for
a specific instance.
A production-planning problem (production.mod)
{string} Products = ...;
{string} Resources = ...;
float Consumption[Products][Resources] = ...;
float Capacity[Resources] = ...;
float Demand[Products] = ...;
float InsideCost[Products] = ...;
float OutsideCost[Products] = ...;
dvar float+ Inside[Products];
dvar float+ Outside[Products];
minimize
sum( p in Products )
( InsideCost[p] * Inside[p] + OutsideCost[p] * Outside[p] );
subject to {
forall( r in Resources )
ctCapacity:
sum( p in Products )
Consumption[p][r] * Inside[p] <= Capacity[r];
forall(p in Products)
ctDemand:
Inside[p] + Outside[p] >= Demand[p];
}
An instance of the problem must specify the products, the resources, the capacity of the resources, the demand for each product, the consumption of resources by the different products, and the inside and outside costs of each product. These various data items are specified in the standard way in production.dat below. The model contains two arrays of variables: element Inside[p] (respectively Outside[p]) represents the inside (respectively outside) production of product p. The objective function specifies that the production cost must be minimized.
Data for the production-planning problem (production.dat)
Products = { "kluski", "capellini", "fettuccine" };
Resources = { "flour", "eggs" };
Consumption = [ [0.5, 0.2], [0.4, 0.4], [0.3, 0.6] ];
Capacity = [ 20, 40 ];
Demand = [ 100, 200, 300 ];
InsideCost = [ 0.6, 0.8, 0.3 ];
OutsideCost = [ 0.8, 0.9, 0.4 ];
The production cost is simply the sum of the individual production costs, which are obtained by multiplying the inside and outside productions of the given product by their respective costs. Finally, the model has two types of constraints. The first set of constraints expresses the capacity constraints, the second set states the demand constraints. The model is once again a linear programming problem.
A solution to production.mod
For the instance data given in Data for the production-planning problem (production.dat), OPL outputs the following solution:
Final Solution with objective 372.0000:
inside = [40.0000 0.0000 0.0000];
outside = [60.0000 200.0000 300.0000];
Although the model is simple, it is inconvenient in separating the data associated with each product in different arrays: for instance, array demand stores the demand for the products, while array insideCost stores their inside costs. This technique, sometimes called parallel arrays, may be error-prone and less readable for more complicated models. Tuples provide a simple way to cluster related data and impose more structure on a model. This is illustrated in the revisited example below, product.mod, and the revised data product.dat, which exhibit an alternative model for the production-planning problem.
The production-planning problem revisited (product.mod)
{string} Products = ...;
{string} Resources = ...;
tuple productData {
float demand;
float insideCost;
float outsideCost;
float consumption[Resources];
}
productData Product[Products] = ...;
float Capacity[Resources] = ...;
dvar float+ Inside[Products];
dvar float+ Outside[Products];
execute CPX_PARAM {
cplex.preind = 0;
cplex.simdisplay = 2;
}
minimize
sum( p in Products )
(Product[p].insideCost * Inside[p] +
Product[p].outsideCost * Outside[p] );
subject to {
forall( r in Resources )
ctInside:
sum( p in Products )
Product[p].consumption[r] * Inside[p] <= Capacity[r];
forall( p in Products )
ctDemand:
Inside[p] + Outside[p] >= Product[p].demand;
}
Data for the revised production-planning problem (product.dat)
Products = { "kluski", "capellini", "fettuccine" };
Resources = { "flour", "eggs" };
Product = #[
kluski : < 100, 0.6, 0.8, [ 0.5, 0.2 ] >,
capellini : < 200, 0.8, 0.9, [ 0.4, 0.4 ] >,
fettuccine : < 300, 0.3, 0.4, [ 0.3, 0.6 ] >
]#;
Capacity = [ 20, 40 ];
The instruction
tuple productData {
float demand;
float insideCost;
float outsideCost;
float consumption[Resources];
}
declares a tuple type with four fields. The first three fields, of type float, are used to represent the demand and costs of a product; the last field is an array representing the resource consumptions of the product. These fields are intended to hold all the data related to a given product.
The instruction
ProductData product[Products] = ...;
declares an array of these tuples, one for each product. The initialization
Product = #[
kluski : < 100, 0.6, 0.8, [ 0.5, 0.2 ] >,
capellini : < 200, 0.8, 0.9, [ 0.4, 0.4 ] >,
fettuccine : < 300, 0.3, 0.4, [ 0.3, 0.6 ] >
]#;
from Data for the revised production-planning problem (product.dat) specifies these various data items: tuples are initialized by giving values for each of their fields. It is of course possible to use a named initialization for the tuple, as shown in Named data for the revised production-planning problem (productn.dat), in which case the initialization is enclosed with #< and >#. Tuple fields can be obtained by suffixing the tuple with a dot and the field name. For instance, in the objective function
minimize
sum( p in Products )
(Product[p].insideCost * Inside[p] +
Product[p].outsideCost * Outside[p] );
the expression product[p].insideCost represents the field insideCost of the tuple product[p].
Similarly, in the constraint
forall(r in Resources)
sum(p in Products) product[p].consumption[r] * inside[p] <= capacity[r];
the expression product[p].consumption represents the field consumption of tuple product[p]. This field is an array that can be subscripted in the traditional way.