At operator

The operator “at” is an operator whose first operand is an array or a list and whose following operands are integers (the indices). The number of indices must match the dimension of the array (1 in case of a list). Its value is the value of the element in the array at the given indices.

Note

This page covers the use of the “at” operator on arrays (mono-dimensional or multi-dimensional). For a description of the use of the “at” operator on list variables, see List variables.

The 1-dimensional at operator

Assume that we model the behavior of a machine whose status can be on (1) or off (0). This status is modeled as a binary variable. Now the operating cost of this machine can depend on its status what would be modeled straightforwardly in LocalSolver as:

status <- bool();
mcost <- status ? onCost : offCost;

Note that onCost and offCost are not necessarily constants. They can depend on other expressions, for instance we may have an optional special technology (say a binary variable specialTechno) impacting the cost as follows: onCost <- 100 + 20 * specialTechno.

The at operator can be seen as a generalization of this conditional expression (c ? a : b). Let us generalize the above example to a machine having several possible operating modes. Now the status is an integer variable whose value is among {0, 1, 2, 3, 4, 5}, and the operating cost depends on the operating mode (the status).

Operating mode Operating cost
0 Cost 0
1 Cost 1
2 Cost 2
3 Cost 3
4 Cost 4
5 Cost 5

Here again cost0, ... cost5 are not necessarily constants. They can depend on other expressions or decisions. It would be possible to model the operating cost of this machine with 5 nested conditional expressions but this approach would be tedious and inappropriate in case of hundreds of operating modes. Instead the at operator allows us to have all costs in an array and to define the operating cost by simply accessing this array with the index status:

status <- int(0,5);
costs = {cost0, cost1, cost2, cost3, cost4, cost5};

// In LSP the at operator is naturally written with the [] notation on a map
mcost <- costs[status];

In the APIs, the behavior is the same but requires a supplementary step. Indeed, although the LSP language enables you to directly use arrays, maps or lists, no such thing is possible for other languages and your array must be converted to an LSExpression before use. This LSExpression is an operator of type O_Array. It has no numeric value (calling getValue on it is forbidden) but contains an array of expressions instead. Expressions in the array can be constant or not.

To convert a Python list to a LocalSolver array, you can simply create an LSExpression with LSModel.create_expression() then add each element manually as operands or use the provided shortcut that takes a Python list or a python iterable object.

status = model.int(0, 5)
vv = [ cost0, cost1, cost2, cost3, cost4, cost5 ]
# Convert the list to LSExpression.
lsv = model.array(vv)
# Creates the at (operator [] is overloaded in the Python API).
mcost = lsv[status]

To convert a C++ structure to a LocalSolver array, you can simply create an LSExpression with createExpression() then add each element manually as operands, or use the provided shortcut that takes iterators pointing to the beginning and the end of your structure.

LSExpression status = model.intVar(0, 5);
std::vector<int> vv { cost0, cost1, cost2, cost3, cost4, cost5 };
// Convert the vector to LSExpression.
LSExpression lsv = model.array(vv.begin(), vv.end());
// Creates the at (operator [] is overloaded in the C++ API).
LSExpression mcost = lsv[status];

To convert a C# list to a LocalSolver array, you can simply create an LSExpression with LSModel.CreateExpression then add each element manually as operands, or use the provided shortcut that takes an enumerable object.

LSExpression status = model.Int(0, 5);
List<LSExpression> list = new List<LSExpression> { cost0, cost1, cost2, cost3, cost4, cost5 };
// Convert the list to LSExpression.
LSExpression lsv = model.Array(list);
// Creates the at
LSExpression mcost = model.At(lsv, status);

To convert a Java list to a LocalSolver array, you can simply create an LSExpression with LSModel.createExpression then add each element manually as operands, or use the provided shortcut that takes an iterable object.

LSExpression status = model.intVar(0, 5);
List<LSExpression> list = Arrays.asList(cost0, cost1, cost2, cost3, cost4, cost5);
// Convert the list to LSExpression.
LSExpression lsv = model.array(list);
// Creates the at
LSExpression mcost = model.at(lsv, status);

The multi-dimensional at operator

The at operator also provides a way to access arrays of arrays. It can be really convenient to avoid creating quadratic expressions.

Assume that we model a TSP where the ith city visited is stored in the expression city[i] and where the distance between each pair of cities is determined by the distance matrix distance. The distance travelled from the ith city to the next one can be simply defined by an at operator on the distance matrix with indices city[i] and city[i+1]:

city = {exprCity0, exprCity1, ..., exprCityn};
distance = {{d00, d01, ..., d0n}, {d10, d11, ...,d1n}, ..., {dn0, dn1, ..., dnn}};

// In LSP, even the multi-index at operator is naturally written
// with the [][] notation on a map
distanceTravelled <- distance[city[i]][city[i+1]];

As is the case with the 1-dimensional at, the APIs’ equivalent of the above code also requires creating an O_Array expression, but this time it will be an array of arrays.

std::vector<LSExpression> city {exprCity0, exprCity1, ..., exprCityn};
std::vector<std::vector<int> > distance =
    {{d00, d01, ..., d0n}, {d10, d11, ...,d1n}, ..., {dn0, dn1, ..., dnn}};
LSExpression distanceArray = model.array();
for(int k = 0; k < n; k++)
    distanceArray.addOperand(model.array(distance[k].begin(), distance[k].end()));
LSExpression distanceTravelled = model.at(distanceArray, city[i], city[i+1]);

Jagged arrays

Arrays of arrays do not have to represent square matrices. They can perfectly store jagged arrays:

a = { 1, 2, 3 };
b = { 4, 5 };
c = { 6 };

myArray <- {a, b, c};

Pitfalls

Arrays must have a uniform dimension

It is not required that arrays have the same number of elements (jagged arrays), but they must have the same dimension. Thus, it is not possible to mix, in the same expression, a 2-dimensional array, with a 3-dimensional array.

// Will throw an error because the first operand is an expression of
// dimension 0 (a boolean decision) whereas the second is an array of
// dimension 1
std::vector<int> a { 1, 2, 3 };
LSExpression array = model.array(a.begin(), a.end());
LSExpression error = model.array();
error.addOperand(model.bool());
error.addOperand(array);

Indices start at zero

The indexing of the array starts at 0. Hence when creating an at expression in the LSP language, the map must have a value defined for index 0.

For example, the following code will throw an exception:

status <- int(1,3);
costs[1] = 10;
cost[2] = 18.2;
cost[3] = 20;

// will throw an exception because indices of map "costs" do not start at zero
mcost <- costs[status];

Indeed when creating such an expression through a map, an O_Array expression is implicitly created, hence indices in the map must be consecutive and starting at 0, as in a regular array.

Array overflows

When the indices of an at expression take values outside of the bounds of the array, this expression is considered as undefined. Undefined expressions are allowed in LocalSolver provided that they do not result in an undefined contraints or an undefined objective, in which case the LocalSolver solution has the Infeasible status (or Inconsistent if the solver can prove that no feasible solution exists).

For instance the following code has no explicit constraint but the model will be proven inconsistent because of the implicit constraints induced by the at operator. Indeed you can observe that no value of x can prevent the objective function to be undefined:

x <- int(-3, 3);
c[i in 0..30] <- 3*i + 1;
maximize c[-3 + 2*x] + c[3 - 2*x];

On the contrary, the following model is consistent even if for all values of x one branch of the condition will remain undefined:

x <- int(-3, 3);
c[i in 0..30] <- 3*i + 1;
maximize 2*x >= 3 ? c[-3 + 2*x] : c[3 - 2*x];

In the APIs, an operator of type O_Array must be created

Altough the LSP language allows creating array expressions from maps, API functions require the explicit creation of an O_Array expression. See the C++ code below.

std::vector<int> vv { 10, 18, 20 };
LSExpression x = model.intVar(0, 2);

// Will not compile because vv is not an LSExpression
LSExpression wwx_bad = vv[x];

// OK since lsv is an LSExpression of type O_Array
LSExpression lsv = model.array(vv.begin(), vv.end());
LSExpression wwx_good = lsv[x];