This section introduces these commands, using formatted printing and sensitivity analysis as examples. For those who want to get started faster, a syntax summary for all these commands appears at the end of this section.
The following section describes additional commands and features that permit you to express new algorithmic schemes by means of AMPL scripts.
As an example, consider how we might perform a simple sensitivity analysis on the multi-period production problem of Section 4.2. Only 32 hours of production time are available in week 3, compared to 40 hours in the other weeks. Suppose that we want to see how much extra profit could be gained for each extra hour in week 3. We can accomplish this by repeatedly solving, displaying the solution values, and increasing avail[3]:
ampl: model steelT.mod; ampl: data steelT.dat; ampl: solve; MINOS 5.4: optimal solution found. 15 iterations, objective 515033 ampl: display total_profit >steelT.sens; ampl: option display_1col 0; ampl: option omit_zero_rows 0; ampl: display Make >steelT.sens; ampl: display Sell >steelT.sens; ampl: option display_1col 20; ampl: option omit_zero_rows 1; ampl: display Inv >steelT.sens; ampl: let avail[3] := avail[3] + 5; ampl: solve; MINOS 5.4: optimal solution found. 6 iterations, objective 532033 ampl: display total_profit >steelT.sens; ampl: option display_1col 0; ampl: option omit_zero_rows 0; ampl: display Make >steelT.sens; ampl: display Sell >steelT.sens; ampl: option display_1col 20; ampl: option omit_zero_rows 1; ampl: display Inv >steelT.sens; ampl: let avail[3] := avail[3] + 5; ampl: solve; MINOS 5.4: optimal solution found. 6 iterations, objective 549033 ampl:To try all integer values of avail[3] from 32 to 77, we would complete another seven cycles in the same way. All of the solution displays are saved in the file steelT.sens, although we could just as well have made them appear on the screen.
To avoid having to type the same commands again and again, we can create a new file listing the commands to be repeated:
solve; display total_profit >steelT.sens; option display_1col 0; option omit_zero_rows 0; display Make >steelT.sens; display Sell >steelT.sens; option display_1col 20; option omit_zero_rows 1; display Inv >steelT.sens; let avail[3] := avail[3] + 5;If we call this file steelT.sa1, then we can execute all the commands in it by typing just the one line commands steelT.sa1:
ampl: model steelT.mod; ampl: data steelT.dat; ampl: commands steelT.sa1; MINOS 5.4: optimal solution found. 15 iterations, objective 515033 ampl: commands steelT.sa1; MINOS 5.4: optimal solution found. 6 iterations, objective 532033 ampl: commands steelT.sa1; MINOS 5.4: optimal solution found. 6 iterations, objective 549033 ampl: commands steelT.sa1; MINOS 5.4: optimal solution found. 7 iterations, objective 565193 ampl:We refer to the contents of a file like steelT.sa1 as a script. The looping and testing commands to be described in this section are most valuable when they are used within scripts.
A script can itself contain a commands command that refers to another script. Scripts can be nested in this way to a depth of up to 10.
[[Can alternatively use the include command -- makes no difference here, but does inside a loop as in the next section]]
We begin with the for command, which executes a statement or collection of statements once for each member of some set. To execute our multi-period production problem sensitivity analysis script four times, for example, we need only type a single for command followed by the command that we want to repeat:
ampl: model steelT.mod;
ampl: data steelT.dat;
ampl: for {1..4} commands steelT.sa1;
MINOS 5.4: optimal solution found.
15 iterations, objective 515033
MINOS 5.4: optimal solution found.
6 iterations, objective 532033
MINOS 5.4: optimal solution found.
6 iterations, objective 549033
MINOS 5.4: optimal solution found.
7 iterations, objective 565193
ampl:
The expression between for and the command can be any AMPL
indexing expression, as we will see in later examples.As an alternative, we can add the for command to the script. We then want it to loop over a whole series of commands; this is accomplished by enclosing the commands in braces:
model steelT.mod;
data steelT.dat;
for {1..4} {
solve;
display total_profit >steelT.sens;
option display_1col 0;
option omit_zero_rows 0;
display Make >steelT.sens;
display Sell >steelT.sens;
option display_1col 20;
option omit_zero_rows 1;
display Inv >steelT.sens;
let avail[3] := avail[3] + 5;
}
If this script is stored in steelT.sa2, then the whole
iterated sensitivity analysis is carried out by simply typing
commands steelT.sa2.This latter approach tends to be clearer and easier to work with, particularly as we make the loop more sophisticated. As a first example, consider how we would go about compiling a table of the objective and the dual value on constraint time[3], for successive values of avail[3]. A script for this purpose is shown in Figure 1. After the model and data are read, the script provides additional declarations for the table of values:
set AVAIL3;
param avail3_obj {AVAIL3};
param avail3_dual {AVAIL3};
The set AVAIL3 will contain all the different values for
avail[3] that we want to try; for each such value a,
avail3_obj[a] and avail3_dual[a] will be the
associated objective and dual values. Once these are set up, we
assign the set value to AVAIL3,
let AVAIL3 := avail[3] .. avail[3] + 15 by 5;and then use a for loop to iterate over this set:
for {a in AVAIL3} {
let avail[3] := a;
solve;
let avail3_obj[a] := total_profit;
let avail3_dual[a] := time[3].dual;
}
We see here that a for loop can be over an arbitrary set, and
that the index running over the set (a in this case) can be
used in statements within the loop. After the loop is complete, the
desired table is produced by displaying avail3_obj and
avail3_dual, as shown at the end of the script in Figure 1.
If this script is stored in steelT.sa3, then the desired
results are produced with a single command:
ampl: include steelT.sa3 : avail3_obj avail3_dual := 32 515033 3400 37 532033 3400 42 549033 3400 47 565193 2980 ;In this case we have suppressed the messages from the solver, by including the command option solver_msg 0 in our script.
Figure 1. A script for recording sensitivity to the value of the parameter avail[3], using a solve within a for loop (steelT.sa3).
--------------------------------------------------------------- model steelT.mod; data steelT.dat; set AVAIL3; param avail3_obj {AVAIL3}; param avail3_dual {AVAIL3}; let AVAIL3 := avail[3] .. avail[3] + 15 by 5; option solver_msg 0; for {a in AVAIL3} { let avail[3] := a; solve; let avail3_obj[a] := total_profit; let avail3_dual[a] := time[3].dual; } display avail3_obj, avail3_dual; ---------------------------------------------------------------
AMPL's for loops are also very convenient for
generating formatted tables. Suppose that after solving the
multi-period production problem, you want to display sales both in
tons and as a percentage of the market limit. You could use a
display command to produce a table like this:
ampl: display {t in 1..T, p in PROD}
ampl? (Sell[p,t], 100*Sell[p,t]/market[p,t]);
: Sell[p,t] 100*Sell[p,t]/market[p,t] :=
1 bands 6000 100
1 coils 307 7.675
2 bands 6000 100
2 coils 2500 100
3 bands 1400 35
3 coils 3500 100
4 bands 2000 30.7692
4 coils 4200 100
;
By writing a script that uses AMPL's printf command (Section
A.13.1), you can create a much more effective table:
ampl: commands steelT.tab1 SALES bands coils week 1 6000 100.0% 307 7.7% week 2 6000 100.0% 2500 100.0% week 3 1399 35.0% 3500 100.0% week 4 1999 30.8% 4200 100.0%The script to write this table can be as short as two printf commands:
printf "\n%s%14s%17s\n", "SALES", "bands", "coils";
printf {t in 1..T}: "week %d%9d%7.1f%%%9d%7.1f%%\n", t,
Sell["bands",t], 100*Sell["bands",t]/market["bands",t],
Sell["coils",t], 100*Sell["coils",t]/market["coils",t];
This approach is undesirably restrictive, however, because it assumes
that there will always be two products and that they will always be
named coils and bands. In fact the printf
statement cannot write a table in which both the number of rows and
the number of columns depend on the data, because the number of
entries in its format string is always fixed.A more generally applicable script for generating the above table, using a for loop over 1..T, is shown in Figure 2. Each pass through the loop generates one row of the table. There are more printf statements than in the previous example, but they are shorter and simpler. We use several statements to write the contents of each line; printf does not begin a new line except where \n appears in its format string.
Figure 2. A script for generating a formatted sales table, using nested for loops (steelT.tab).
--------------------------------------------------------------- printf "\nSALES"; printf {p in PROD}: "%14s ", p; printf "\n"; for {t in 1..T} { printf "week %d", t; for {p in PROD} { printf "%9d", Sell[p,t]; printf "%7.1f%%", 100 * Sell[p,t]/market[p,t]; } printf "\n"; } ---------------------------------------------------------------
Within the "outer" loop over 1..T we generate each
line's product entries by use of an "inner" loop over PROD.
Loops can in general be nested to any depth, and may be over any set
that can be represented by an AMPL set expression. There is one pass
through the loop for every member of the set, and if the set is
ordered -- any set of numbers like 1..T, or a set declared
ordered or circular -- then the order of the passes
is determined by the ordering of the set. If the set is unordered
(like PROD) then AMPL chooses the order of the passes, but
the choice is the same every time; the Figure 2 script relies on this
consistency to insure that all of the entries in one column of the
table refer to the same product.
[[Note: alternatives not using for]]
Returning to the multi-period production example, we observe that the dual value on the constraint time[3] provides an upper limit on the additional profit that can be realized from each extra hour added to avail[3]. When avail[3] is made sufficiently large, so that there is more third-week capacity than can be used, the associated dual value falls to zero and further increases in avail[3] have no effect on the optimal solution. Either the expression time[3] alone, or time[3].dual, represents the dual value in AMPL expressions.
We can specify that looping should stop once the dual value falls to zero, by writing a repeat statement that has one of the following forms:
repeat while time[3].dual > 0 { . . . };
repeat until time[3].dual = 0 { . . . };
repeat { . . . } while time[3].dual > 0;
repeat { . . . } until time[3].dual = 0;
The loop body, here indicated by the placeholder { . . .
}, must be enclosed within braces. Passes through the
loop continue as long as the condition after while is true, or
as long as the condition after until is false. A condition
written before the loop body is tested before every pass; if a
while condition is false or an until condition is
true before the first pass, then the loop is never executed. A
condition written after the loop body is tested after every pass, so
that the loop is executed at least once in every case.A complete script using repeat is shown in Figure 3. The until phrase is placed after the loop body, so that time[3].dual will not be tested until after a solve has been executed in the first pass. Two other features of this script are worth noting, as they are relevant to many AMPL scripts of this kind.
Figure 3. A script for recording sensitivity to the value of avail[3], using a repeat statement to continue until the associated dual value is zero (steelT.sa4).
--------------------------------------------------------------- model steelT.mod; data steelT.dat; option solution_precision 10; option solver_msg 0; set AVAIL3 default {}; param avail3_obj {AVAIL3}; param avail3_dual {AVAIL3}; param avail3_step := 5; repeat { solve; let AVAIL3 := AVAIL3 union {avail[3]}; let avail3_obj[avail[3]] := total_profit; let avail3_dual[avail[3]] := time[3].dual; let avail[3] := avail[3] + avail3_step; } until time[3].dual = 0; display avail3_obj, avail3_dual; ---------------------------------------------------------------
At the beginning of the script, we don't know how many passes
the repeat statement will make through the loop. Thus we
cannot determine AVAIL3 in advance as we did in Figure 1.
Instead, we declare it initially to be the empty set,
set AVAIL3 default {};
param avail3_obj {AVAIL3};
param avail3_dual {AVAIL3};
and add each new value of avail[3] to it after solving:
let AVAIL3 := AVAIL3 union {avail[3]};
let avail3_obj[avail[3]] := total_profit;
let avail3_dual[avail[3]] := time[3].dual;
By adding a new member to AVAIL3, we also create new
components of the parameters avail3_obj and
avail3_dual that are indexed over AVAIL3, and so we
can proceed to assign the appropriate values to these components. Any
change to an AMPL set is propagated to all declarations that use the
set, in the same way that any change to a parameter is propagated.Because numbers in the computer are represented with a limited number of bits of precision, a solver may return values that differ very slightly from the solution that would be computed using exact arithmetic. Ordinarily you don't see this, because AMPL's display command is set by default to round values to six significant digits. Compare what is shown when rounding is dropped, by setting display_precision to 0:
ampl: display Make; Make [*,*] (tr) : bands coils := 1 5990 1407 2 6000 1400 3 1400 3500 4 2000 4200 ; ampl: option display_precision 0; ampl: display Make; Make [*,*] (tr) : bands coils := 1 5989.999999999999 1407.0000000000002 2 6000 1399.9999999999998 3 1399.9999999999995 3500 4 1999.9999999999993 4200 ;These seemingly tiny differences can have undesirable effects whenever a script makes a comparison that uses values returned by the solver. The rounded table would lead you to believe that Make["coils",2] >= 1400 is true, for example, whereas from the second table you can see that really it is false.
You can avoid this kind of surprise by writing your tests more carefully; instead of until time[3].dual = 0, for instance, you might say until time[3].dual <= 0.0000001. Alternatively, you can instruct AMPL to round all solution values that are returned by the solver, so that number that are supposed to be equal really do come out equal. The statement
option solution_precision 10;
toward the beginning of Figure 3 has this effect; it states that
solution values are to be rounded to 10 significant digits. These and
related options are discussed and illustrated in Section 10.5 of the
AMPL book.Note finally that the script declares set AVAIL3 as default {} rather than := {}. The former allows AVAIL3 to be changed by let commands as the script proceeds, whereas the latter permanently defines AVAIL3 to be the empty set.
if Make["coils",2] < 1500 then printf "under 1500\n";The action may also be a series of AMPL commands grouped by braces as in the for and repeat commands:
if Make["coils",2] < 1500 then {
printf "Fewer than 1500 coils in week 2.\n";
let market["coils",2] := market["coils",2] * 1.1;
}
An optional else specifies an alternative action that also
may be either a single command,
if Make["coils",2] < 1500 then {
printf "Fewer than 1500 coils in week 2.\n";
let market["coils",2] := market["coils",2] * 1.1;
}
else printf "More than 1500 coils in week 2.\n";
or a group of commands:
if Make["coils",2] < 1500 then printf "under 1500\n";
else {
printf "over 1500\n";
let market["coils",2] := market["coils",2] * 0.9;
};
AMPL executes these commands by first evaluating the logical
expression following if. If the expression is true then the
command or commands following then are executed. If the
expression is false then the command or commands following
else, if any, are executed.The if command is most useful for regulating the flow of control in scripts. In Figure 2, we could suppress the many occurrences of 100% by placing the statement that prints Sell[p,t]/market[p,t] inside an if:
if Sell[p,t] < market[p,t] then printf "%7.1f%%", 100 * Sell[p,t]/market[p,t]; else printf " --- ";In Figure 3, we can use an if command in the repeat loop to test whether the dual value has changed since the previous pass through the loop:
let avail[3] := 1;
param avail3_step := 1;
param previous_dual default Infinity;
repeat while previous_dual > 0 {
solve;
if time[3].dual < previous_dual then {
let AVAIL3 := AVAIL3 union {avail[3]};
let avail3_obj[avail[3]] := total_profit;
let avail3_dual[avail[3]] := time[3].dual;
let previous_dual := time[3].dual;
}
let avail[3] := avail[3] + avail3_step;
}
When used within the Figure 3 script, this loop creates a table that
has exactly one entry for each different dual value discovered.If you run the Figure 3 script with the above changes (steelT.sa5), you will find that the dual value changes when avail[3] is increased from 22 to 23 hours. Figure 4 exhibits a script (steelT.sa6) that carries out a binary search to determine more exactly (to 7 decimal places) the value of avail[3] where the dual value changes:
ampl: commands steelT.sa6; Dual value 3620.000000 for avail[3] < 22.807142 Dual value 3500.000000 for avail[3] > 22.807144The if in this script,
if time[3].dual = dual_lower then let avail3_lower := avail[3]; else let avail3_upper := avail[3];is key to the binary search algorithm, since it determines which half of the current search interval is removed at each step. The equality at the beginning of this if statement is another example of a comparison involving numbers returned by the solver. Thus as before the option solution_precision is set to 10 at the start of the script; in this case the results would be wrong otherwise.
Figure 4. A script that performs a binary search to determine the level of avail[3] at which the associated dual value changes (steelT.sa6).
--------------------------------------------------------------- model steelT.mod; data steelT.dat; option solution_precision 10; option solver_msg 0; param avail3_lower default 22; param dual_lower; param avail3_upper default 23; param dual_upper; let avail[3] := avail3_lower; solve; let dual_lower := time[3].dual; let avail[3] := avail3_upper; solve; let dual_upper := time[3].dual; repeat { let avail[3] := (avail3_lower + avail3_upper) / 2; solve; if time[3].dual = dual_lower then let avail3_lower := avail[3]; else let avail3_upper := avail[3]; } until (avail3_upper - avail3_lower) / avail[3] < 0.0000001; printf "Dual value %11.6f for avail[3] < %9.6f\n", dual_lower, avail3_lower; printf "Dual value %11.6f for avail[3] > %9.6f\n", dual_upper, avail3_upper; ---------------------------------------------------------------
The Figure 4 script requires an initial interval, bounded by
avail3_lower and avail3_upper, such that the dual
value changes once as avail[3] moves through the interval.
If the dual value might change more than once, then the following
if ensures that the script will find the largest value of
avail[3] at which there is a change:
if time[3].dual > dual_upper
then {
let avail3_lower := avail[3];
let dual_lower := time[3].dual;
}
else let avail3_upper := avail[3];
A further refinement counts the number of times that the search detects
an additional change in the dual value within the initial interval:
if time[3].dual = dual_lower
then let avail3_lower := avail[3];
else if time[3].dual = dual_upper
then let avail3_upper := avail[3];
else {
let other_bkpts := other_bkpts + 1;
let avail3_lower := avail[3];
let dual_lower := time[3].dual;
};
Here we see how the statement following else can be another
if, giving a chain of tests. [[Each nested else paired with
nearest available if.]]
As an example of both these commands, Figure 6 exhibits another way of rewriting the loop from the Figure 3 script, so that a table entry is made only when there is a change in the dual value associated with avail[3]. After solving, we test to see if the new dual value is equal to the previous one:
if time[3].dual = previous_dual then continue;If yes, there is nothing to be done for this value of avail[3]. Thus we use the continue command to jump to the end of the current pass; execution resumes with the next pass, starting at the beginning of the loop.
Figure 5. An alternative to the sensitivity analysis script in Figure 3, using the continue and break commands (steelT.sa7).
--------------------------------------------------------------- model steelT.mod; data steelT.dat; option solution_precision 10; option solver_msg 0; set AVAIL3 default {}; param avail3_obj {AVAIL3}; param avail3_dual {AVAIL3}; let avail[3] := 0; param previous_dual default Infinity; repeat { let avail[3] := avail[3] + 1; solve; if time[3].dual = previous_dual then continue; let AVAIL3 := AVAIL3 union {avail[3]}; let avail3_obj[avail[3]] := total_profit; let avail3_dual[avail[3]] := time[3].dual; if time[3].dual = 0 then break; let previous_dual := time[3].dual; } display avail3_obj, avail3_dual; ---------------------------------------------------------------
After adding an entry to the table, we test to see if the dual
value has fallen to zero:
if time[3].dual = 0 then break;If yes, we are done with the loop. We use the break command to jump out of the loop; execution passes to the display command that follows the loop in the script. Since the repeat statement in this example has no while or until conditions, it relies on the break command for termination.
When a break or continue lies within more than one loop, it applies only to the innermost loop. This convention generally has the effect desired. As an example, consider how we could expand Figure 5 to perform a separate sensitivity analysis on each avail[t]. The repeat loop would be nested in a for {t in 1..T} loop (steelT.sa7a), but the continue and break commands would apply to the inner repeat as before.
There do exist situations in which the logic of a script requires breaking out of a loop other than the inner loop. In the script of Figure 5, for instance, we can imagine that instead of stopping when the time[3].dual is zero,
if time[3].dual = 0 then break;we want to stop when time[t].dual falls below 2700 for any t. One way to implement this criterion is:
for {t in 1..T}
if time[t].dual < 2700 then break;
This statement does not have the desired effect, however, because
break applies only to the inner for loop that
contains it, rather than to the outer repeat loop as we
desire. To deal with these kinds of situations, AMPL lets you give a
name to any loop, so that any break or continue may
specify by name the loop to which it should apply. Using this
feature, the outer loop in our example could be named
sens_loop:
repeat sens_loop {
. . .
}
and the inner loop would be
for {t in 1..T}
if time[t].dual < 2700 then break sens_loop;
The loop name always comes immediately after repeat or
for, and after break or continue.
To step through a script that does not execute any other scripts, simply reset the AMPL option single_step from its default value of zero to one. For example, here is how you might begin stepping through the script in Figure 4:
ampl: option single_step 1; ampl: commands steelT.sa6; steelT.sa6:3(19) data ... <2>ampl:The expression steelT.sa6:3(19) gives the file name, line number and character number where AMPL has stopped in its processing of the script. It is followed by the beginning of the next command (data) to be executed. One the next line you are returned to the ampl prompt; the <2> in front means [[level of prompt, to be explained]].
At this point you may use the step command to execute individual commands of the script. Type step by itself to execute one command,
<2>ampl: step steelT.sa6:5(37) option ... <2>ampl: step steelT.sa6:6(67) option ... <2>ampl: step steelT.sa6:11(188) let ... <2>ampl:or type step followed by a number to execute that number of commands:
<2>ampl: step 3 steelT.sa6:14(258) let ... <2>ampl:Every command is counted except those having to do with model declarations, such as model and param in this example.
Each step returns you to an AMPL prompt as before. You may continue in this way, but at some point you will want to display some values to see if the script is working as you intended:
<2>ampl: display avail[3], dual_lower; avail[3] = 22 dual_lower = 3620 <2>ampl: step 3 steelT.sa6:17(328) repeat ... <2>ampl: display avail[3], dual_upper; avail[3] = 23 dual_upper = 3500 <2>ampl:Any series of AMPL commands may be typed while single-stepping. After each command, the <2>ampl prompt returns to remind you that you are still in this mode and may use step to continue executing the script.
At this point, execution of the script has reached the beginning of a repeat loop. The step command takes you into the first pass through the loop,
<2>ampl: step steelT.sa6:19(341) let ... <2>ampl: step steelT.sa6:21(396) solve ... <2>ampl:and into the branch of the enclosed if command that is executed:
<2>ampl: step steelT.sa6:23(407) if ... <2>ampl: step steelT.sa6:24(448) let ... <2>ampl:Further use of step will take you to the next pass of the loop, back at line 19,
<2>ampl: step steelT.sa6:19(341) let ... <2>ampl: step steelT.sa6:21(396) solve ... <2>ampl:and similarly through all of the subsequent passes, after which control passes to the command following repeat.
To help you step through lengthy compound commands (for, repeat, or if) AMPL provides several alternatives to step. The next command steps past a compound command rather than into it. In our example, next would cause the entire repeat command to be executed, stopping again only at the following printf command on line 29:
<2>ampl: step steelT.sa6:17(328) repeat ... <2>ampl: next steelT.sa6:29(586) printf ... <2>ampl:Type next n to step past n commands in this way.
The commands skip and skip n work like step and step n, except that they skip the next 1 or n commands in the script rather than executing them.
Return to the AMPL update page.