LOOPING AND TESTING 1:
writing "scripts" in the AMPL command language

New for, repeat, if and related
commands let you write small programs, or scripts, in the AMPL
command language. Another collection of new commands let you step
through a script, for purposes of observation or debugging.

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.

Running scripts

Just as the model command reads files of model
declarations, and the data command reads files of data
statements, the AMPL commands command reads files containing
AMPL scripts -- lists of commands -- that are to be executed. Simple
scripts can save you the trouble of typing the same sequences of
commands again and again. More sophisticated scripts can carry out
repetitive actions that would be impractical to type one at a time, as
we will show shortly.

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]:

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:

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]]

Iterating over a set

The above example still requires that some command be typed
over and over again. AMPL provides looping commands that can do this
work automatically, with various options to determine how long the
looping should continue.

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:

If this script is stored in steelT.sa2, then the whole
iterated sensitivity analysis is carried out by simply typing
commandssteelT.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,

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:

Figure 1. A script for recording sensitivity to the value of
the parameter avail[3], using a solve within a
for loop (steelT.sa3).

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:

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).

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]]

Iterating subject to a condition

A second kind of AMPL looping construct, the repeat
statement, continues iterating as long as some logical condition is
satisfied.

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:

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).

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,

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:

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 untiltime[3].dual = 0, for
instance, you might say untiltime[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.

Testing a condition

AMPL's new if command takes a specified action
conditionally. In the simplest case, the action is the execution of
one AMPL command:

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:

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:

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:

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).

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:

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.]]

Terminating a loop

Two other new AMPL commands work with looping statements to
make some scripts easier to write. The continue command
terminates the current pass through a for or while
loop; all further statements in the current pass are skipped, and
execution continues with the start of the next pass (if any). The
break command completely terminates a for or
while loop, sending control immediately to the statement
following the end of the loop.

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).

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.

Stepping through a script

If you think that a script might not be doing what you want it
to, you can direct AMPL to step through it one command at a time.
This facility can be used to provide an elementary form of "symbolic
debugger" for AMPL scripts.

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:

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,

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,

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: