| Proposed New AMPL Features | Draft of August 19, 1996 |
Optimization systems for constraint logic programming also use a branching or tree search, but are able to work with a much broader variety of combinatorial expressions. In exchange for this advantage, logic programming solvers typically forego the ability to generate the bounds and cuts that are used to great advantage to limit the search in integer programming. The logic programming approach thus tends to be advantageous for problems that are "highly" combinatorial, in the sense that their formulation in terms of integer variables is artificial and hard to work with, or requires a very large number of variables or constraints, or fails to yield strong bounds.
AMPL could be extended in a variety of ways to take better advantage of the strengths of constraint logic programming solvers. Several kinds of constraints useful in logic programming are naturally expressed by allowing variables in certain operands or arguments where only constant expressions are currently permitted. The features of constraint logic programming systems also suggest some operators and functions that should be added to the AMPL language. Possibilities include:
- constraint-expr1 or constraint-expr2
- Satisfied iff at least one of the operands is satisfied.
- constraint-expr1 and constraint-expr2
- Satisfied iff both of the operands are satisfied.
- not constraint-expr
- Satisfied iff the operand is not satisfied.
- exists {indexing} constraint-expr
- Satisfied iff the operand is satisfied for at least one member of the indexing set (the iterated form of or).
- forall {indexing} constraint-expr
- Satisfied iff the operand is satisfied for all members of the indexing set (the iterated form of and).
- ( constraint-expr )
- Satisfied iff the constraint-expr is satisfied.
The simplest useful application of this extension would be to specify disjunctive constraints. For example, in multmip3.mod from the AMPL book, we require for every origin i and destination j that total shipments sum {p in PROD} Trans[i,j,p] lie either at zero or between minload and limit[i,j]. This is accomplished in an integer programming framework by introducing auxiliary binary variables Use[i,j],
var Trans {ORIG,DEST,PROD} > = 0;
var Use {ORIG,DEST} binary;
and forming the following constraints:
subject to Multi {i in ORIG, j in DEST}:
sum {p in PROD} Trans[i,j,p] < = limit[i,j] * Use[i,j];
subject to Min_Ship {i in ORIG, j in DEST}:
sum {p in PROD} Trans[i,j,p] > = minload * Use[i,j];
The same restrictions could be stated using the or operator
in this way:
subject to Multi_Min_Ship {i in ORIG, j in DEST}:
sum {p in PROD} Trans[i,j,p] = 0 or
minload < = sum {p in PROD} Trans[i,j,p] < = limit[i,j];
Fewer variables and constraints are required, and the statement of
the constraint is much closer to its original formulation.In another example, variables Assign[i1,i2,j] have been defined to represent the number of people of type (i1,i2) assigned to location j. The following declarations define an additional restriction in integer programming terms:
param upperbnd {(i1,i2) in ISO, j in REST} :=
min (ceil((number2[i1,i2]/card {PEOPLE}) * hiDine[j]) + give[i1,i2],
hiTargetTitle[i1,j] + giveTitle[i1],
hiTargetLoc[i2,j] + giveLoc[i2],
number2[i1,i2]);
var Lone {(i1,i2) in ISO, j in REST} binary;
subj to Isolation1 {(i1,i2) in ISO, j in REST}:
Assign2[i1,i2,j] < = upperbnd[i1,i2,j] * Lone[i1,i2,j];
subj to Isolation2a {(i1,i2) in ISO, j in REST}:
Assign2[i1,i2,j] +
sum {ii1 in ADJACENT[i1]: (ii1,i2) in TYPE2} Assign2[ii1,i2,j]
> = 2 * Lone[i1,i2,j];
subj to Isolation2b {(i1,i2) in ISO, j in REST}:
Assign2[i1,i2,j] > = Lone[i1,i2,j];
Using the or operator, the same thing can be said much more
directly and concisely:
subj to Isolation {(i1,i2) in ISO, j in REST}:
Assign2[i1,i2,j] = 0 or
Assign2[i1,i2,j] +
sum {ii1 in ADJACENT[i1]:
(ii1,i2) in TYPE2} Assign2[ii1,i2,j] > = 2;
As a further complication under the integer programming approach,
there are many other ways that this either-or constraint can be
transformed, through addition of binary variables, to inequalities
suitable for integer programming. Other transformations differ in the
computation of the upper bound parameter, in the details of the
constraint expressions, and in the choice of "cuts" such as
Isolation2b to tighten the formulation. The transformation
exhibited above is not obviously superior to others, though it did
prove to be superior -- for the data of interest -- in a series of
empirical tests.Although the use of or could be limited to simple cases like this, it would be desirable to allow the full generality of logical operations on constraint equalities and inequalities. There are thorny issues to be dealt with in the general case, however:
minimize Obj: x;
subject to OpenCons: not (x < = 2);
The objective has an infimum of 2, but no minimum that
satisfies the constraint. (The same problem arises if we
allow < and > in the constraints.)
ILOG Solver provides analogues for AMPL's and (&&), or (||), and not (!) by overloading existing C++ operators. It also defines operators for exclusive or (!=), equivalence (), and implication (< =). The effect of AMPL's iterated logical operators can be achieved by using its IlcCard function to count the number of instances of a constraint-expr that are true; see also the discussion of counting operators below.
- if logical-expr then object-expr1
- if logical-expr then object-expr1 else object-expr2
- Takes the value of object-expr1 when the logical-expr is true, and the value of object-expr2 (or 0 if omitted) when the logical-expr is false. Each object-expr may be any expression that evaluates to a legal set member -- that is, either a number or a string.
- if logical-expr then set-expr1 else set-expr2
- Takes the value of set-expr1 when the logical-expr is true, and the value of set-expr2 when the logical-expr is false.
AMPL also has a similar if-then form of indexing expression, which is used in the context of constraints as follows:
- subject to {if const-logical-expr}: constraint-expr;
- Enforces the constraint specified by the constraint-expr if and only if the logical-expr is true.
subject to Time {if avail > 0}:
sum {p in PROD} (1/rate[p]) * Make[p] < = avail;
It is arguably more natural, howver, to make the if condition
part of the constraint expression:
subject to Time:
if avail > 0 then
sum {p in PROD} (1/rate[p]) * Make[p] < = avail;
By allowing variables in the logical-expr following if,
we arrive at another appealing form of constraint for logic programming:
- if logical-expr then constraint-expr1
- Satisfied if the logical-expr is true and constraint-expr1 is satisfied, or if the logical-expr is false.
- if logical-expr then constraint-expr1 else constraint-expr2
- Satisfied if the logical-expr is true and constraint-expr1 is satisfied, or if the logical-expr is false and constraint-expr2 is satisfied.
subject to Multi_Min_Ship {i in ORIG, j in DEST}:
if sum {p in PROD} Trans[i,j,p] > 0 then
minload < = sum {p in PROD} Trans[i,j,p] < = limit[i,j];
Again, the logical-expr could be any constraint-expr.
ILOG Solver can directly define if-then constraints like Time and Multi_Min_Ship above by use of either a constraint-posting function (IlcIfThen) or an implication operator (< =). There is no direct support for if-then-else constraints, however; they have to be built from two calls to IlcIfThen or an equivalent constraint expression using several logical operators.
AMPL's if-then and if-then-else operators, described at the beginning of this section, have a direct analogue as a C++ operator, but C++ does not allow the overloading of that operator that would be necessary to permit its use with Solver's constraint data types.
subject to Max_Serve {i in ORIG}:
sum {j in DEST} Use[i,j] < = maxserve;
Using card, the same thing could be expressed in terms of
the natural Trans[i,j,p] variables, without recourse to the
auxiliary zero-one Use[i,j] variables:
subject to Max_Serve {i in ORIG}:
card {j in DEST: sum {p in PROD} Trans[i,j,p] > 0} < = maxserve;
This form might be too general, however. The operand of card
could be any set expression. To implement the resulting constraint,
the solver would have to receive enough information to enable it to
evaluate that set expression given any values of the variables. We
could circumvent this difficulty by restricting the ways in which
variables may appear in the argument to card, but we prefer
to avoid complicating the language design with such restrictions.
Instead, we could define a new operator that explicitly counts the
number of times that a certain constraint is satisfied:
- count {indexing} constraint-expr
- The number of members of the indexing set such that the constraint-expr is satisfied.
subject to Max_Serve {i in ORIG}:
count {j in DEST} (sum {p in PROD} Trans[i,j,p] > 0) < = maxserve;
The constraint-expr could be any valid AMPL constraint. The
AMPL translator would instantiate it for each member of the
indexing set, and would communicate all of the instantiated
constraints to the solver using the conventions already in place.Additional iterated logical operators might be defined to simplify the descriptions of constraints in some common special cases:
- atmost1 {indexing} constraint-expr
- Satisfied iff the constraint-expr holds for at most one member of the indexing set.
- atleast1 {indexing} constraint-expr
- Satisfied iff the constraint-expr holds for at least one member of the indexing set.
- exactly1 {indexing} constraint-expr
- Satisfied iff the constraint-expr holds for exactly one member of the indexing set.
- atmost(k) {indexing} constraint-expr
- atleast(k) {indexing} constraint-expr
- exactly(k) {indexing} constraint-expr
- Satisfied iff the constraint-expr holds for at most (at least, exactly) k members of the indexing set.
Another particularly important special case occurs when counting the number of set members at which a given expression takes a particular value. As a simple example, consider the scheduling problem that assigns a number of jobs to a smaller number of machines, so that at most cap[k] jobs are assigned to machine k. The conventional formulation defines a binary (zero-one) variable Assign[j,k] for each job-machine pair, such that Assign[j,k] will be 1 if and only if job j is assigned to machine k (sched1.mod):
param n integer > 0;
set JOBS := 1..n;
set MACHINES := 1..n;
param cap {MACHINES} integer > = 0;
var Assign {JOBS,MACHINES} binary;
The requirements of the assignment can then be specified by one
algebraic constraint for each job and for each machine:
subj to OneMachinePerJob {j in JOBS}:
sum {k in MACHINES} Assign[j,k] = 1;
subj to CapacityOfMachine {k in MACHINES}:
sum {j in JOBS} Assign[j,k] < = cap[k];
As an alternative, we could associate with each job only one variable,
whose value is taken from the set of machines:
var MachineForJob {JOBS} integer > = 1, < = n;
For each j in JOBS, the value of
MachineForJob[j] would be the number of the machine that is
assigned to do job j. This approach requires fewer variables
by an order of magnitude, and automatically enforces the requirement
that one machine be assigned to each job. To specify that at most
cap[k] jobs are assigned to machine k, we could use
the proposed count operator (sched2.mod):
subj to CapacityOfMachine {k in MACHINES}:
count {j in JOBS} (MachineForJob[j] = k) < = cap[k];
This is not as readable a statement of the constraint as one might
like, however, and it is likely to be inefficient. Because the
count operator can be applied to any AMPL
constraint-expr, its implementation in the AMPL translator
would scan through the entire set JOBS for each constraint,
testing MachineForJob[j] k for every
combination of job j and machine k -- even though
only one pass through the jobs is necessary to accumulate the counts
for all machines. These circustances suggest that AMPL should instead
offer a more specialized iterated operator for counting individual
values assumed by an indexed expression (sched3.mod):
subj to CapacityOfMachine {k in MACHINES}:
countof(k) {j in JOBS} MachineForJob[j] < = cap[k];
The general form would be:
- countof(k) {indexing} object-expr
- The number of members of the indexing set such that the object-expr is equal to k.
ILOG Solver's analogue to AMPL's cardinality operators is the IlcCard function. IlcCard acts like card when called with one argument representing a set of integers (type IlcIntSetVar) or objects (type IlcAnySetVar). It acts like count when called with an index (type IlcIndex) and a constraint using that index (type IlcConstraint). The special case of the countof(k) operator is implemented efficiently through Solver's IlcDistribute function.
In the case where IlcCard is called with two arguments, an index is implicitly defined to run over the elements of one or more arrays of integers (type IlcIntVarArray) or objects (type IlcAnyVarArray). To get the effect of AMPL's count applied to an arbitrary indexed collection of constraints, Solver must introduce auxiliary binary variables via its metaconstraint feature. The association between constraints and these binary variables is maintained automatically, however, in contrast to the integer programming approach where it is necessary to form additional constraints (such as Multi and Min_Ship above) to enforce the definition of the binary variables.
An example is given by the assignment problem that resembles the one defined above, but with equal numbers of jobs and machines. Each job is assigned to one machine, as before, but also each machine gets one job. In describing the constraints on the machines, we can dispense with the parameters cap[k] (which would all be 1) and can instead simply say that no two variables MachineForJob[j1] and MachineForJob[j2] have the same value. Such a restriction can be stated in AMPL as follows:
subj to OneJobPerMachine {j1 in JOBS, j2 in JOBS: j1 < j2}:
MachineForJob[j1] != MachineForJob[j2];
This is a cumbersome way to express the simple idea of being pairwise
different, however, and it gives rise to an order of magnitude more
constraints than the conventional formulation (assign1.mod). We would prefer to
regard pairwise inequality as a property of the entire collection of
MachineForJob variables, rather than as a collection of
binary relations between individual variables.In AMPL, properties of indexed collections are defined by means of iterated operators such as sum and exists. Thus it would make sense to introduce an analogous operator for pairwise difference in an indexed collection of variables (assign2.mod):
subj to OneJobPerMachine: alldiff {j in JOBS} MachineForJob[j];
In general, this operator could be applied to any collection of
expressions:
- alldiff {indexing} object-expr
- Satisfied iff the object-expr takes different values for all pairs of different members of the indexing set.
A similar operator could be introduced for indexed collections of set expressions, to express the property of being pairwise disjoint:
- alldisjoint {indexing} set-expr
- Satisfied iff the set-expr evaluates to nonintersecting sets for all pairs of different members of the indexing set.
ILOG Solver provides functions that correspond closely to these iterated pairwise operators. Function IlcAllDiff specifies that the components of an array of integers (type IlcIntVarArray) or an array of objects (type IlcAnyVarArray) must be pairwise different. Function IlcAllNullIntersect specifies that the components of an array of integer sets (type IlcIntSetVarArray) or an array of object sets (type IlcAnySetVarArray) must be pairwise disjoint. Solver's search procedure handles these relationships directly in an efficient way.
minimize TotalCost:
sum {j in JOBS, k in MACHINES} cost[j,k] * Assign[j,k];
To use the MachineForJob variables, however, we are
currently forced to resort to a more awkward formulation:
minimize TotalCost:
sum {j in JOBS, k in MACHINES}
if MachineForJob[j] = k then cost[j,k];
If we could simply write the cost of assigning machine j to
job MachineForJob[j] as cost[j,MachineForJob[j]],
then the objective could be expressed much more naturally (assign3.mod):
minimize TotalCost:
sum {j in JOBS, k in MACHINES} cost[j,MachineForJob[j]];
This would require an extension to AMPL, since currently variables are
not allowed to appear in subscripts in the objective of a model.There is also good reason to want to use variables within subscripts in the constraints, as may be seen by considering the related problem of sequencing. In a representative sequencing problem, we may want to assign a number of jobs to an equal number of "slots" so that each job is assigned to a different slot. The key difference between these slots and the machines of the assignment example is that the slots have a significant ordering. Setup costs and times between jobs are a function of this slot ordering, in ways that can be hard to express using current AMPL features.
To illustrate the difficulties and a prospective remedy, we define data for a simple sequencing problem as follows:
param n integer > 0;
set JOBS := 1..n;
set SLOTS := 1..n;
param procTime {JOBS} > 0;
param dueTime {JOBS} > 0;
param duePenalty {JOBS} > 0;
set JOBPAIRS := {j1 in JOBS, j2 in JOBS: j1 != j2};
set PREC within JOBPAIRS;
param setupCost {JOBPAIRS} > = 0;
param setupTime {JOBPAIRS} > = 0;
For each job j, procTime[j] is the processing time
required for the job, dueTime[j] is the deadline for
completion of the job, and duePenalty[j] is a cost per unit
of time that the job is finished early. For each pair of jobs
j1 and j2, (j1,j2) is in the set
PREC if and only if j1 must be finished before
j2; setupCost[j1,j2] and setupTime[j1,j2]
are the cost and time required to set up for job j2, if it is
assigned to the slot immediately following job j1's slot.Adopting the approach introduced in the assignment example, we could define the variables as follows:
var SlotForJob {JOBS} integer > = 1, < = n;
var JobForSlot {SLOTS} integer > = 1, < = n;
var FinishTime {{0} union JOBS};
SlotForJob[j] is the slot assigned to job j, and
conversely JobForSlot[k] is the job assigned to slot
k. FinishTime[j] is the time that job j is
finished, with FinishTime[0] being the same as the start
time. The number of variables remains proportional to the number of
jobs, but we must figure out how to express the objective and
constraints in terms of these variables.What is the setup cost associated with the kth slot? It is the cost of going from JobForSlot[k-1] to JobForSlot[k], or
setupCost[JobForSlot[k-1],JobForSlot[k]]This is not currently a valid AMPL expression, because it uses variables as subscripts to a parameter. It is certainly a very natural expression for this kind of problem, however, when the variables are defined in the efficient way we have given. The setup times are represented by an analogous expression. The completion time of the job in the kth slot is given by the expression
FinishTime[JobForSlot[k]]which illustrates that it is equally natural to subscript a variable with another variable. Using these expressions, we can write the lower limit for the completion time of the job in the kth slot as the following constraint:
FinishTime[JobForSlot[k-1]]
+ setupTime[JobForSlot[k-1],JobForSlot[k]]
+ procTime[JobForSlot[k]]
< = FinishTime[JobForSlot[k]]
The SlotForJob variables are needed for the precedence
constraints; for each pair of jobs (j1,j2) in set
PREC,
SlotForJob[j1] < SlotForJob[j2]Finally, to ensure that the JobForSlot and SlotForJob variables describe the same valid sequencing, we need one more instance of a variable subscripted by a variable; for each job j,
JobForSlot[SlotForJob[j]] = jThis constraint also indirectly ensures that each job is assigned a different slot, and each slot is assigned a different job. In the complete model (seq1.mod) the number of constraints is only on the order of the number of jobs (unless there is a huge number of precedence relations).
In general, any subscript in a constraint might be an expression that contains variables. Unlike current AMPL subscripts, these could not simply be instantiated when AMPL generates constraints. Rather, for each such subscript, AMPL would have to provide the solver with instructions for evaluating the expression, in the form of an expression tree or (for linear expressions) a list of coefficients.
ILOG Solver's arrays of integers (type IlcIntArray) and integer variables (type IlcIntVarArray) have a subscripting operator that admits any integer expression (type IlcIntExp) as its argument. Since an integer expression may in general contain variables, subscripts containing variables could be implemented straightforwardly. This observation also extends to Solver's analogous arrays of floating-point numbers (types IlcFloatArray, IlcFloatVarArray) and of objects (types IlcAnyArray, IlcAnyVarArray).
- var indexingopt var-name in const-set-expr ;
- Restricts the defined variable(s) to values that lie within the set specified by the const-set-expr.
As an example, in the preceding assignment model we artificially defined JOBS and MACHINES to be sets of the first n integers. If object-valued variables were allowed, however, these could be modeled as sets of objects to be read from the problem data, with a check that each contained the same number of objects:
set JOBS;
set MACHINES;
check card (JOBS) = card (MACHINES);
For each j in JOBS, the variable
MachineForJob[j] would then have as its value the member of
set MACHINES that is assigned to do job j. Hence it
would be declared as an object-valued variable:
var MachineForJob {JOBS} in MACHINES;
The remainder of the formulation (assign4.mod) would be the same as
before. A similarly minor change to our sequencing model (seq2.mod) would permit it also to use
a set of objects for the jobs.ILOG Solver provides constrained enumerated variables (types IlcAnyVar and IlcAnyVarArray) whose values may be arbitrary objects. Solver's integer-valued variables can also be used in contexts where each integer stands for an object (rather than for a numerical value that participates in arithmetic expressions). The IlcAllDiff operator required for our formulation is applicable to both object and integer arrays.
As an example, consider first a simple knapsack problem that concerns a set of objects having given values and weights, and an overall capacity:
set OBJECTS;
param value {OBJECTS} > 0;
param weight {OBJECTS} > 0;
param capacity > 0;
The problem can be stated concisely and naturally as follows: find a
subset of some given set of objects, such that the total value of the
subset is maximized, subject to the total weight of the subset being
less than the capacity. This statement can be converted to a
conventional algebraic formulation, by defining binary variable
corresponding to each object:
var In {OBJECTS} binary;
maximize Total_Value:
sum {i in OBJECTS} value[i] * In[i];
subject to Weight_Limit:
sum {i in OBJECTS} weight[i] * In[i] < = capacity;
The concise and natural original description could be expressed
directly in AMPL, however, by use of a set-valued variable:
var Knapsack within OBJECTS;
maximize Total_Value:
sum {i in Knapsack} value[i];
subject to Weight_Limit:
sum {i in Knapsack} weight[i] < = capacity;
The alternative objective and constraint superficially resemble the
standard algebraic constraints, but they sum over the members of the
set-valued variable Knapsack rather than over the given set
OBJECTS.The budgeted traveling salesman problem offers a somewhat different example. The problem is stated in terms of a set of cities, one designated the home city; a set of city pairs on which travel is possible, and travel costs between these pairs; and an overall budget:
set CITIES;
param Home symbolic in CITIES;
set LINKS within {i in CITIES, j in CITIES: i < > j};
param cost {LINKS};
param budget > 0;
In one version of the problem, the goal is to plan a tour from the
home city visiting as many cities as possible, using only available
city-pair links, and subject to the total travel cost being within the
budget. For this case we need a variable whose values are
(circularly) ordered subsets of cities:
var Tour circular within CITIES;
maximize Cities_Visited: card {Tour};
subject to Budget_Limit:
sum {c in Tour} cost[c,next(c)] < = budget;
subject to Leave_Home: first(Tour) = Home;
subject to Link_Exists {c in Tour}: (c,next(c)) in LINKS;
Unlike the knapsack problem, this one has no especially obvious
equivalent as an integer program. The equivalent IPs that do exist,
moreover, are generally too hard for general-purpose integer
programming codes except in very small instances. Most combinatorial
optimization problems of practical complexity share these
characteristics.The general forms for declarations of set-valued variables would be as follows:
- var indexingopt var-name within const-set-expr ;
- Defines set-valued variables whose domain consists of all unordered subsets of the set specified by the const-set-expr.
- var indexingopt var-name ordered within const-set-expr ;
- The same as above, except the const-set-expr must describe an ordered set, and induces an ordering on all of the subsets in the variable's domain. Ordered set functions such as first and next may thus be applied to the variable and its members.
- var indexingopt var-name circular within const-set-expr ;
- The same as above, except the orderings are circular.
ILOG Solver includes data types for sets of objects (IlcAnySet) and sets of integers (IlcIntSet). Set-valued variables are provided by corresponding constrained set variables of objects (types IlcAnySetVar and IlcAnySetVarArray) and integers (types IlcIntSetVar and IlcIntSetVarArray). Functions are provided for the standard set operations, including membership, containment, union, intersection, and cardinality.
Return to the AMPL proposed new features page.
Return to the AMPL update page.