Indexed Components

Overview

Dense vs Sparse Indexing

Indexed components generalize the declaration of elementary components to declare multi-dimensional arrays and indexed associative arrays of components. The syntax for declaring indexed components is the same across modeling components, but there is a fundamental distinction between indexing strategies used for variable and parameter components, and indexing used for constraints and objectives. Variable and parameter components represent mutable values, and properties, like variable initial values and bounds can be readily applied across all indices. When declaring a multi-dimensional array or associative array for variables or parameters, it is intuitive to treat these as values defined for all indices. Thus, these are dense declarations, where components are defined for all elements in the index set.

By contrast, declarations of constraints and objectives do not simply have a value. Thus, it is intuitive to declare an indexed component for which the indices may be used sparsely. That is, it is natural to only use a subset of the declared indices in the indexed component.

Index Sets

Associative arrays in Coek are declared using an index set, and Coek includes several ways of declaring index sets. The SetOf() function converts integer data in an STL vector or initializer list into a Coek set object:

std::vector<int> v = {1,5,3,7};
auto s = coek::SetOf( v );

auto t = coek::SetOf( {1,5,3,7} );

Similarly, the RangeSet() function declares a sequence of integer values with a specified start, stop and step value:

auto s = coek::RangeSet(1, 10);     // 1, 2, 3, ..., 10
auto t = coek::RangeSet(1, 10, 2);  // 1, 3, ..., 9

A variety of standard set operations are supported in Coek, include set products and set difference:

auto s = coek::RangeSet(1, 10);     // 1, 2, 3, ..., 10
auto t = coek::RangeSet(1, 10, 2);  // 1, 3, ..., 9
auto u = s - t;                     // 2, 4, ..., 10
auto v = s * t;                     // (1, 1), (2, 1), (3, 1), ..., (10, 9)

See Set Declarations and Operations for further details.

Warning

Associative arrays are currently only defined with configuring Coek using the with_compact build option. This is likely to be the default build mode in the future, so these capabilities are not documented separately.

Variables

A minimal variable declaration includes a name and/or indexing information. The following are basic examples:

// A single continuous variable
auto x = coek::variable();
auto y = coek::variable("y");

// An array of continuous variables of length 'n'
size_t n=100;
auto x = coek::variable(n);
auto y = coek::variable("y", n);

// A multi-dimensional array of continuous variables:  R^{2 x 3 x 5}
auto x = coek::variable({2,3,4});
auto y = coek::variable("y", {2,3,4});

// A tensor of continuous variables indexed by Coek set objects
auto A = coek::RangeSet(1,10);
auto B = coek::RangeSet(11,20);
auto x = coek::variable(A*B);
auto y = coek::variable("y", A*B);

Variable declarations require the specification of various information:

  • Lower bound values

  • Upper bound values

  • Initial values

  • Variable type (continuous, binary, integer, etc)

Indexed variable declarations support function chaining for these specifications, which are applied to all variables in the indexed component:

auto x = coek::variable("x", A*B).
                lower(2).
                upper(10).
                value(3).
                within(coek::Integers);

Similarly, the Variable::bounds() function can be used instead of Variable::lower() and Variable::upper():

WEH

Specifying name and dimension of variables seems fundamental and something that would be done commonly, so I’m inclined to keep those arguments as part of the function:

For example, the indexing option determines the type of variable object returned, so I think we need to include this and not treat it as something that is returned later. (Yes, we could have a unified variable object … but it’s API would be much less clean IMHO.)

Note

The use of function chaining for indexed variables simplifies the specification of common values across an indexed variable. However, these values are set for each of the indexed variables, and the values of each indexed variable can be separately specified. Thus, this notation does not imply that indexed variables are required to have consistent values for all indices.

Variables declared over sets can be indexed using the () operator in a natural manner. For example:

// An array of continuous variables of length 'n'
size_t n=100;
auto x = coek::variable(n);
// Value of the 4th element of the array
auto v = x(3).value();

// A tensor of continuous variables:  R^{2 x 3 x 5}
auto x = coek::variable({2,3,5});
// Value of the variable indexed by (0,2,1)
auto v = x(0,2,1).value();

// A tensor of continuous variables indexed by Coek set objects
auto A = coek::RangeSet(1,10);
auto B = coek::RangeSet(11,20);
auto x = coek::variable(A*B);
// Value of the variable indexed by (1,11)
auto v = x(1,11).value();

Note

For historical reasons, it would be preferable to use the [] operator. However, this operator cannot be overloaded with C++ while allowing multiple subscripts. This will change with C++23, but for now we restrict Coek to the use of operator() logic.

Note that arguments of the () operator may be constant expressions with mutable values. For example, the following are valid expressions:

auto x = coek::variable(10);

auto p = coek::parameter().value(1);
x(p+1).value();         // The value of the x(2)

auto i = coek::set_element();
x(i+1);                 // A reference to x(i+1), which is resolved in a quantified expression

The variable() function provides a uniform interface for declaring both multi-dimensional arrays and associative arrays of variables. The variable_array() and variable_map() functions can be used to more explicitly declare these two types of indexed variables, but there is no practical advantage for using these functions. When iterating over indices, there may be slight computational advantages for using multi-dimensional arrays, which are stored compactly and thus are more cache-efficient data structures for iteration.

Note

Coek confirms that expressions used to index variables do not contain a variable unless it is fixed. Thus, the following creates a runtime error:

auto x = coek::variable(100);
auto y = coek::variable();
auto v = x(y+3).value();

Parameters

Indexed parameters are declared in a similar manner to indexed variables:

// A single parameter
auto p = coek::parameter();
auto q = coek::parameter("q");

// An array of parameters of length 'n'
size_t n=100;
auto x = coek::parameter(n);
auto q = coek::parameter("q", n);

// A tensor of parameters:  R^{2 x 3 x 5}
std::vector<size_t> dim = {2,3,5};
auto x = coek::parameter(dim);
auto q = coek::parameter("q", dim);

// A tensor of parameters indexed by Coek set objects
auto A = coek::RangeSet(1,10);
auto B = coek::RangeSet(11,20);
auto p = coek::parameter(A*B);
auto q = coek::parameter("q", A*B);

Note that parameter are always continuous, and their value defaults to zero. Initializing parameters can be similarly executed using function chaining:

// A single parameter initialized to 1.0
auto q = coek::parameter("q").value(1.0);

// An array of parameter of length 'n' initialized to 1.0
size_t n=100;
auto q = coek::parameter(n).value(1.0);

// A tensor of parameters:  R^{2 x 3 x 5}, initialized to 1.0
std::vector<size_t> dim = {2,3,5};
auto q = coek::parameter("q", dim).value(1.0);

// A tensor of parameters indexed by Coek set objects, initialized to 1.0
auto A = coek::RangeSet(1,10);
auto B = coek::RangeSet(11,20);
auto q = coek::parameter("q", A*B).value(1.0);

The () operator also has the same behavior as for variable components.

Objectives

Indexed objectives are not currently supported in Coek.

WEH

Although not often used, we could also support various ways to declare groups of objectives:

// A single objective
auto a = coek::objective(2*x);
auto b = coek::objective("b", 2*x);

// An array of objectives
size_t n=100;
auto a = coek::objective(n);
auto b = coek::objective("y", n);

// A tensor of objectives:  R^{2 x 3 x 5}
std::vector<size_t> dim = {2,3,5};
auto a = coek::objective(dim);
auto b = coek::objective("b", dim);

// A tensor of objectives indexed by Coek set objects
auto A = coek::RangeSet(1,10);
auto B = coek::RangeSet(11,20);
auto a = coek::objective(A*B);
auto b = coek::objective("b", A*B);

Constraints

Indexed constraints are declared in a similar manner to indexed variables:

// A single constraint
auto a = coek::constraint(2*x == 0);
auto b = coek::constraint("b", 2*x == 0);

// An array of constraints
size_t n=100;
auto a = coek::constraint(n);
auto b = coek::constraint("b", n);

// A tensor of constraints:  R^{2 x 3 x 5}
std::vector<size_t> dim = {2,3,5};
auto a = coek::constraint(dim);
auto b = coek::constraint("b", dim);

// A tensor of constraints indexed by Coek set objects
auto A = coek::RangeSet(1,10);
auto B = coek::RangeSet(11,20);
auto a = coek::constraint(A*B);
auto b = coek::constraint("b", A*B);

A declaration of an indexed constraint indicates the space of possible indices associated with the constraint, but only elementary constraints have a specific value. The () operator can be used to index constraint objects and specify the constraint value:

auto x = coek::variable(10);

auto c = coek::constraint("c", 10);
for (int i=0; i<10; i++)
    c(i) = (i+1)*x(i) <= i;
model.add(c);

As noted earlier, not all indices need to be added to an indexed constraint:

auto x = coek::variable(10);

auto c = coek::constraint("c", {10,10});
for (int i=0; i<10; i++)
    c(i,i) = (i+1)*x(i) <= i;
model.add(c);