Class structure

AMPL API library consists of a collection of classes to interact with the underlying AMPL interpreter and to access its inputs and outputs. It uses generic collections to represent the various entities which comprise a mathematical model. The structure of these entities is explained in this section.

The main class used to interact with AMPL, instantiate and interrogate the models is AMPL. One object of this class represents an execution of an AMPL translator, and is the first class that has to be instantiated when developing a solution based on AMPL API. It allows the interaction with the underlying AMPL translator, issuing commands, getting diagnostics and controlling the process.

The model entities are represented by a set of classes, schematized in figure Model entities classes overview. These classes represent the optimisation model being created and allow some manipulation and data assignments operations on such entities and will be presented more in detail in the section Modelling entities classes.

Model entities class diagram

Model entities classes overview

AMPL class

For all calculations, AMPL API uses an underlying AMPL execution engine, which is wrapped by the class AMPL. Thus, one instance of this class is the first object to be created when writing a program which uses the AMPL API library. The object is quite resource-heavy, therefore it should be explicitly closed as soon as it is not needed anymore, with a call to AMPL.close.

All the model creation and structural alteration operations are to be expressed in AMPL language through the AMPL main object; moreover, the class provides access to the current state represented via the classes derived from Entity, as shown in section Modelling entities classes and provides several other functionalities (see the reference at AMPL classes).

The functions can be split in three groups: direct AMPL interaction, model interrogation and commands.

Direct interaction with AMPL

The methods available to input AMPL commands are AMPL.eval, AMPL.read and AMPL.readData; they send the strings specified (or the specified files) to the AMPL engine for interpretation.

Model interrogation

Evaluating AMPL files or statements creates various kind of entities in the underlying AMPL process. To get the Java (or, in general, programmatic) representation of such entities, the programmer can follow two main courses.

Once the desired entities have been created, it is possible to use their properties and methods to manipulate the model and to extract or assign data. Updating the state of the programmatic entities is implemented lazily and uses proper dependency handling. Communication with the underlying engine is therefore executed only when an entity’s properties are being accessed and only when necessary. An entity is invalidated (needs refreshing) if one of the entities it depends from has been manipulated or if a generic AMPL statement evaluation is performed (through AMPL.eval or similar routines). This is one of the reasons why it is generally better to use the embedded functionalities (e.g. fixing a variable through the corresponding API function call) than using AMPL statements: in the latter case, the API invalidates all entities, as the effects of such generic statements cannot be predicted. Refreshing is transparent to the user, but must be taken into account when implementing functions which access data or modify entities frequently.

Commands and options

Some AMPL commands are encapsulated by functions in the AMPL class for ease of access. These comprise AMPL.solve and others. To access and set options in AMPL, the functions AMPL.setOption and AMPL.getOption are provided. In general, when an encapsulation is available for an AMPL command, the programmatic access to it is to be preferred to calling the same command using AMPL.eval.

Output and errors handling

The output of every user interaction with the underlying translator is printed to the MATLAB console. The normal AMPL options control this output. Regarding errors, by default errors throw exceptions whilst warnings are printed to the MATLAB console.

Modelling entities classes

This group of classes represents the basic entities of an AMPL optimisation model: variables, constraints, objectives, parameters and sets. They are used to access the current state of the AMPL translator (e.g. to find the values of a variable), and to some extent they can be used for data input (e.g. assign values to a parameter, fix a variable).

Objects of these classes cannot be created programmatically by the user: the model creation and structural modification is handled in AMPL (see section AMPL), through the methods AMPL.eval and AMPL.read.

The case of scalar entities (like the AMPL entity defined by var x;) is handled at Entity level, and will be illustrated in the paragraph regarding instances below. The following classes represent algebraic entites (e.g. a variable indexed over a set in AMPL), and are implemented as a map from an object (number, string or tuple) to an instance, which allow access to its instances (method Variable.get and similar for all other entities). Such classes are: Variable, Constraint, Parameter, Objective and Set.

Any instance level object represents a single instance of an algebraic entity (e.g. the value of a variable for a specific value of its indexing set). Such classes are: VariableInstance, ConstraintInstance, ObjectiveInstance and SetInstance. The composition of these classes can be described as shown below:

Relationship between Entity and Instance

Relationship between Entity and Instance

The UML diagram in figure Relationship between Entity and Instance illustrates that each Entity (algebraic entity in AMPL) can contain various instances, while each Instance has to be part of exactly one Entity. The exact methods and properties of the entity depend on the particular kind of entity under consideration (i.e. variable, constraint, parameter).

As an example, the class Variable has functionalities like Variable.fix and Variable.unfix, which would fix or unfix all instances which are part of the algebraic entity, and its corresponding instance class VariableInstance has properties like VariableInstance.value and VariableInstance.dual (together with instance level fix and unfix methods).

The class Constraint has functionalities like Constraint.drop and Constraint.restore, and its instance level class ConstraintInstance properties like ConstraintInstance.body and ConstraintInstance.dual (and methods like drop and restore for the single instance).

Note that the class Parameter, which represent an algebraic parameter, does not have an instance level class; its instances are represented by objects instead (typically double numbers or strings).

Access to instances and values

The instances can be accessed from the parent Entity through the functions Variable.get, Constraint.get, Objective.get and Set.get; all data corresponding to the entity can be accessed through the instances, but the computational overhead of such kind of access is quite considerable. To avoid this, the user can gain bulk data access through a DataFrame object; reference to these object can be obtained using Variable.getValues and similar method for other entities. In case of scalar entities (e.g. the entity declared in AMPL with the statement var x;), all the instance specific methods are replicated at Entity level, to allow the code fragment value = x.value(); instead of the more explicit value = x.get().value(). See example below:

ampl = AMPL;
ampl.eval('var x := 42;');
x = ampl.getVariable('x');

% Compact access to scalar entities
value = x.value

% Access through explicit reference to the instance via get function
value = x.get().value

% Access through explicit reference to the instance via indexing operator
value = x().value
ampl.close;

Indexed entities are central in modelling via AMPL. Generally, an entity is indexed over zero or more sets; this menas that to access a specific instance, the user il need to provide a tuple composed of an appropriate number of items The AMPL API for MATLAB provides overloads to the indexing operators, hence allowing a simple access to the entities. A few examples of accessing indexing entities are presented below.

ampl.eval('var x{1..2, 4..5, {"a", "b"}};');

x = ampl.getVariable('x');

% Each item a member of the Tuple
instance = x(1,4,'a');

% The item is a cell array
index = {1, 4, 'a'};
instance =  x(index);

Scalar Entities -> Variable.get (or indexing operator without index) In general, as seen above, access to an instance of a scalar entity is not needed, as all functionalities of the instance are replicated at entity level in this case. To gain explicit access to an instance anyway, the function Variable.get or the indexing operator with no parameterrs (and similar functions for other entities) can be used, as shown below.

ampl.eval('var x;');
% Get function
instance = ampl.getVariable('x').get();
% Indexing operator
x = ampl.getVariable('x')
instance - x();

Indexed Entities -> indexing operator, Variable.get and alikes. To gain access to instances in indexed entities, these functions can be used, depending on the context.

See the example below.

ampl.eval('var x{1..2, 4..5};');
ampl.eval('var y{1..2, {"a", "b"}};');

x = ampl.getVariable('x');
y = ampl.getVariable('y');

% Numeric indices
instance = x(1,4);
% Numeric indices from array
index = [1 4; 2 5];
% Using the first row of the matrix created above as index
instance = x(index(1,:));

% Mixed types indices
instance = y(1,'a');
% Mixed instances from cell array
index = {1 'a'; 2 'b'};
instance = y(index(1,:));

The currently defined entities are obtained from the various get methods of the AMPL object (see section AMPL class). Once a reference to an entity is created, the entity is automatically kept up-to-date with the corresponding entity in the AMPL interpeter. That is, if a reference to a newly created AMPL variable is obtained by means of AMPL.getVariable, and the model the variable is part of is then solved by means of AMPL.solve, the values of the instances of the variable will automatically be updated. The following code snippet should demonstrate the concept.

ampl = initAMPL;
ampl.eval('var x;');
ampl.eval('maximize z: x;');
ampl.eval('subject to c: x<=10;');
x = ampl.getVariable('x');

% At this point x.value evaluates to 0
x.value

% Solve
ampl.solve;

% At this point x.value evaluates to 10
x.value

% Close ampl
ampl.close;

Relation between entities and data

The entities and instances in AMPL store data (numbers or strings) and can be indexed, hence the instances available depend on the values in the indexing set(s). The order in which these indexing sets is handled in the AMPL entities is not always consistent with the ordering in which the data for such sets is defined, so it is often desirable, even when interested in only data (decoupled from the AMPL entities) to keep track of the indexing values which corresponds to each value.

Moreover, when dealing with AMPL entities (like Variable), consistency is guaranteed for every instance. This means that, if a reference to an instance is kept and in the underlying AMPL interpreter the value of the instance is changed, the value read from the instance object will be always consistent with the AMPL value and, if an instance is deleted in AMPL, an exception will be thrown when accessing it. This has the obvious benefit of allowing the user to rely on the values of the instances, but has a price in terms of computational overhead. For example, accessing in this way the value of 1000 instances:

ampl = AMPL;
ampl.eval('set A := 1..1000;');
ampl.eval('param c{i in A} default 0;');
ampl.eval('var x{i in 1..1000} := c[i];');

% Enumerate through all the instances of c and set their values
c = ampl.getParameter('c');
for i = 1:c.numInstances,
  c.set(i, i * 1.1);
end
% Enumerate through all the instances and print their values
x = ampl.getVariable('x');
for i = 1:x.numInstances,
  x.get(i).value
end

ampl.close;

will check at each access if the referenced instance is valid or not, resulting in a computational overhead. To ease data communication and handling, the class DataFrame is provided. Its usage is two-fold:

  • It allows definition of data for multiple parameters in one single call to the underlying interpterer

  • It decouples data and entities, reducing the computational overhead

DataFrame objects should therefore be used in these circumnstances, together with the methods AMPL.setData and Variable.getValues (and similar), as shown in the code below:

% Create a new dataframe with one indexing column (A) and another column (c)
df = DataFrame(1, 'A', 'c');
for i = 1:1000,
    df.addRow(i, i * 1.1);
end

ampl = AMPL;
ampl.eval('set A;');
ampl.eval('param c{i in A} default 0;');
ampl.eval('var x{i in A} := c[i];');

 % Assign data to the set A and the parameter c in one line
 ampl.setData(df, 'A');
 % Get the variable x
 x = ampl.getVariable('x');
 % From the following line onwards, df is uncoupled from the
 % modelling system,
 df = x.getValues;

 ampl.close();
 % Enumerate through all the instances and print their values


 for i = 1:df.getNumRows,
    df.getRow(i)
 end

The underlying AMPL interpreter does not need to be open when using the dataframe object, but it maintains all the correspondance between indexing set and actual value of the instances.

Access to scalar values

To simplify accessing to scalar values, like the value of a scalar variable or parameter or, in general, any AMPL expression that can be evaluated to a single string or number, the convenience method AMPL.getValue is provided. This method will fail if called on an AMPL expression which does not evaluate to a single value. See below for an example:

ampl = AMPL;
ampl.eval('var x{i in 1..3} := i;');
ampl.eval('param p symbolic := "test";');
ampl.eval('param pp := 4;');

% x2 will have the value 2
x2 = ampl.getValue('x[2]')
% p will have the value 'test'
p = ampl.getValue('p')
% pp will have the value 4
pp = ampl.getValue('pp')

Note on variables suffixes

For AMPL versions prior to 20150516, there was a glitch with v.lb, v.ub, v.lslack, v.uslack, and v.slack where v is a variable instantiated without need of presolve and after one or more other variables have been instantiated. Example:

var x <= 0;
var y <= 0;
display y.lb;
display x.ub;
# x.ub was wrong (with separate display commands)
# but all went well with "display y.lb, x.ub;"