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.
Please note that all classes and functions of the AMPL API are declared in the ampl
and ampl.Entities
namespaces.
The main class used to interact with AMPL, instantiate and interrogate the models is ampl.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.
AMPL class¶
For all calculations, AMPL API uses an underlying AMPL execution engine, which is wrapped by the class ampl.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.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 ampl.Entity
, as shown in section C# API Reference and provides several other functionalities.
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.AMPL.Eval()
, ampl.AMPL.Read()
and ampl.AMPL.ReadData()
;
they send the strings specified (or the specified files) to the AMPL engine for interpretation.
Their async versions: ampl.AMPL.EvalAsync()
, ampl.AMPL.ReadAsync()
and ampl.AMPL.ReadDataAsync()
,
permit the calling program to continue the execution while the underlying AMPL process is busy in some time consuming operation,
and to define a callback to be executed when the operation is over.
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.
Get a collection of all available entities, to iterate through them. The methods to obtain such lists are:
ampl.AMPL.GetVariables()
gets the map of all the defined variablesampl.AMPL.GetConstraints()
gets the map of all the defined constraintsampl.AMPL.GetObjectives()
gets the map of all the defined objectivesampl.AMPL.GetSets()
gets the map of all the defined setsampl.AMPL.GetParameters()
gets the map of all the defined parameters
Knowing the AMPL name of an entity, use commands to get the specific entity directly:
ampl.AMPL.GetVariable()
returns theampl.Entities.Variable
representing the AMPL variable with the specified name, if it existsampl.AMPL.GetConstraint()
returns theampl.Entities.Constraint
representing the AMPL constraint with the specified name, if it existsampl.AMPL.GetObjective()
returns theampl.Entities.Objective
representing the AMPL objective with the specified name, if it existsampl.AMPL.GetParameter()
returns theampl.Entities.Parameter
representing the AMPL parameter with the specified name, if it existsampl.AMPL.GetSet()
returns theampl.Entities.Set
representing the AMPL set with the specified name, if it exists
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.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.AMPL
class for ease of access.
These comprise ampl.AMPL.Solve()
and others.
To access and set options in AMPL, the functions ampl.AMPL.GetOption()
and ampl.AMPL.SetOption()
are provided.
Together with their type-safe alternatives (e.g. ampl.AMPL.GetBoolOption()
, ampl.AMPL.GetIntOption()
), these functions
provide an easier programmatic access to the AMPL options.
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.AMPL.Eval()
.
Output and errors handling¶
Normally, output from the AMPL translator is directed to the console without passing through the C# API.
This behaviour can be overriden by calling the method ampl.AMPL.EnableOutputRouting
then overriding the event ampl.AMPL.Output
, which is raised at each block of output from the translator.
Error handling is two-faced:
Errors coming from the underlying AMPL translator (e.g. syntax errors and warnings obtained calling the
ampl.AMPL.Eval()
method) are handled by the eventsampl.AMPL.Error
andampl.AMPL.Warning
. Note that redirection must be explicitly enabled via a call toampl.AMPL.EnableErrorAndWarningRouting
.Generic errors coming from the API, which are detected outside the translator are thrown as exceptions.
The default implementation of the error handler throws exceptions on errors and prints the warnings to the 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 class), through the methods ampl.AMPL.Eval()
and ampl.AMPL.Read()
. The two base classes are ampl.Entities.Entity
and ampl.Instance
.
The classes derived from ampl.Entities.Entity
represent algebraic entites
(e.g. a variable indexed over a set in AMPL), and are implemented as a dictionary <object[], Instance
, so its instances can be
enumerated via a foreach
loop or can be accessed by indexing value via the indexing operator or via the function
ampl.Entities.Entity.Get
).
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 derived classes are: ampl.Entities.Variable
, ampl.Entities.Constraint
, ampl.Entities.Parameter
,
ampl.Entities.Objective
and AMPL.Entities.Set
.
Any object of a class derived from ampl.Instance
represents a single instance of an algebraic entity
(e.g. the value of a variable for a specific value of its indexing set).
The derived classes are: ampl.VariableInstance
, ampl.ConstraintInstance
,
ampl.ObjectiveInstance
and AMPL.SetInstance
.
The composition of these classes can be described as shown below:
The UML diagram in figure Relationship between Entity and Instance illustrates that each ampl.Entities.Entity
(algebraic entity in AMPL)
can contain various ampl.Instance
objects (instances in AMPL), 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 ampl.Entities.Variable
has functionalities like ampl.Entities.Variable.Fix()
and ampl.Entities.Variable.Unfix()
, which would fix or unfix all instances which are part of the algebraic entity,
and its corresponding instance class ampl.VariableInstance
has properties like ampl.VariableInstance.Value
and ampl.VariableInstance.Dual
(together with instance level fix and unfix methods).
The class ampl.Entities.Constraint
has functionalities like ampl.Entities.Constraint.Drop()
and
ampl.Entities.Constraint.Restore()
,
and its instance level class ampl.ConstraintInstance
properties like ampl.ConstraintInstance.Body
and
ampl.ConstraintInstance.Dual
(and methods like drop and restore for the single instance).
Note that the class ampl.Entities.Parameter
, which represent an algebraic parameter, does not have
an instance level class; its instances are represented by ampl.Variant
objects instead (which can represent double numbers or strings).
Access to instances and values¶
The instances can be accessed from the parent Entity through functions like ampl.BasicEntity.Get()
, available for
all entity classes or via the indexing operator.
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 ampl.DataFrame
object;
reference to these object can be obtained using ampl.Entity.GetValues
methods.
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:
double value;
ampl.AMPL ampl = new AMPL();
ampl.Eval("var x;");
Variable x = ampl.GetVariable("x");
double value = x.Value; // Compact access to scalar entities
value = x.Get().Value; // Access through explicit reference to the instance
Indexed entities are central in modelling via AMPL. This is why the ampl.BasicEntity.Get()
method
and the indexing operator have various overloads and can be used in multiple ways, to adapt to specific use cases.
These will be presented below, by mean of some examples.
Scalar Entities 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. Anyway,
to gain explicit access to an instance, the function ampl.BasicEntity.Get()
can be used without parameters, as shown below.
ampl.Eval("var x;");
VariableInstance x = ampl.GetVariable("x").Get();
Indexed Entities To gain access to instances in indexed entities,
this set of functions can be used, depending on the context. For specialised conversion of indices, see the function ampl.Tuple.Join
.
See the examples below:
Each item is a index value : Each item passed to the function is interpreted as the value of one of its indices (up to 4 indices)
The (only) item is an array containing all the indices
The (only) item is a
ampl.Tuple
representing all the indices
ampl.Eval("var x{1..2, 4..5, 7..8};");
var x = a.GetVariable("x");
// Each item an index
VariableInstance instance = x.Get(1, 4, 7); // or
// The item is an array
object[] values = { 1, 4, 7 };
instance = x.Get(values);
instance = x[new ampl.Tuple(values)]; // explicit conversion to Tuple
// The item is a tuple
ampl.Tuple t = new ampl.Tuple(1, 4, 7);
instance = x.Get(t); // or
instance = x[t];
For a more idiomatic style, AMPL API allows access to the instances through iterators. See the examples below which use the same declarations of the example above to illustrate how to enumerate all the instances:
// Access all instances using enumerated loop
foreach (VariableInstance i in x)
Console.WriteLine(i.Name);
// Access all instances using dynamic types
foreach (var i in x)
Console.WriteLine(i.Name);
The currently defined entities are obtained from the various get methods of the ampl.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 interpreter. That is, if a reference to a newly created AMPL variable
is obtained by means of ampl.AMPL.GetVariable()
, and the model the variable is part of is then solved
by means of ampl.AMPL.Solve()
, the values of the instances of the variable will automatically be updated.
The following code snippet should demonstrate the concept.
ampl.Eval("var x;");
ampl.Eval("maximize z: x;");
ampl.Eval("subject to c: x<=10;");
Variable x = ampl.GetVariable("x");
// At this point x.Value evaluates to 0
Console.WriteLine(x.Value); // prints 0
ampl.Solve();
// At this point x.Value evaluates to 10
Console.WriteLine(x.Value); // prints 10
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 ampl.Entities.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.Eval("set A := 1..100; param c{i in A} default 0; var x{i in 1..100} := c[i];");
// Enumerate through all the instances of c and set their values
var c = ampl.GetParameter("c");
for (int i = 1; i <= c.NumInstances; i++)
c.Set(new ampl.Tuple(i), i * 1.1);
// Enumerate through all the instances and print their values
var x = ampl.GetVariable("x");
foreach (var xi in x)
Console.WriteLine(xi.Value); // will be 1.1, 2.2, ...
// Try again
for (int i = 1; i <= c.NumInstances; i++)
c[i] = new Variant(i * 2.2); // Assign values directly
// Enumerate through all the instances and print their values
foreach (var xi in x)
Console.WriteLine(xi.Value); // will be 2.2, 4.4, ...
will check at each access if the referenced instance is valid or not, resulting in a computational overhead. Moreover,
in a multi-threaded environment (like when using ampl.AMPL.EvalAsync()
), the value of the underlying collection of instances
could be be changed by the interpreter while the main program is iterating through them, leading to undetermined results.
To ease data communication and handling, the class ampl.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 and risks related to concurrency
ampl.DataFrame
objects should therefore be used in these circumnstances, together with the methods
ampl.AMPL.SetData()
, ampl.Entities.Entity.GetValues()
and ampl.AMPL.SetData()
.
// Create a new dataframe with one indexing column (A) and another column (c)
var df = new ampl.DataFrame(1, "A", "c");
for (int i = 1; i <= 100; i++)
df.AddRow(i, i * 1.1);
using (AMPL ampl = new AMPL())
{
ampl.Eval("set A; param c{i in A} default 0; var x{i in A} := c[i];");
// Assign data to the set A and the parameter c in one line
ampl.SetData(df, "A");
var x = ampl.GetVariable("x");
// From the following line onwards, df is uncoupled from the modelling system,
df = x.GetValues();
} // ampl object is now closed
// Prints all the values
foreach (var row in df)
Console.WriteLine(row[1].Dbl);
// Prints all the values using DataFrame's routine
Console.WriteLine(df.ToString());
The underlying AMPL interpreter does not need to be open when using the dataframe object, but it maintains all the correspondence between indexing set and actual value of the instances.
Access to scalar values¶
Simplified access 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, is possible using the convenience method ampl.AMPL.GetValue()
.
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 = new 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
Console.WriteLine(a.GetValue("x[2]").Dbl);
// p will have the value "test"
Console.WriteLine(a.GetValue("p").Str);
// pp will have the value 4
Console.WriteLine(a.GetValue("pp").Dbl);
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;"