LocalSolver logo
is now
Hexaly logo

We're excited to share that we are moving forward. We're leaving behind the LocalSolver brand and transitioning to our new identity: Hexaly. This represents a leap forward in our mission to enable every organization to make better decisions faster when faced with operational and strategic challenges.

This page is for an old version of Hexaly Optimizer. We recommend that you update your version and read the documentation for the latest stable release.

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

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

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 don’t 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 a 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.

Implicit constraints are induced by the use of at operators

When the indices of an at expression take values outside of the bounds of the array, this expression is considered as violated and 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:

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

All elements must have a valid value

In order to have a feasible solution, not only all constraints must be respected, but all expressions must also have a valid value. This condition still stands for at and if expressions. Even though the at operator only selects one expression in an array, all the other expressions must still have a valid value.

For instance the decisions (0, 0) will be considered as an infeasible solution with the following model:

x <- int(0, 5);
y <- int(0, 5);
division <- x == 0 ? y : y/x;
minimize x + y;

The value of division is 0 with this set of decisions. However, the expression x/y has a value Not a Number (NaN) which makes the solution infeasible.

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