Set Handling Functions
LINGO offers several functions that assist with the manipulation of sets:
• | @IN( set_name, set_member) — Returns true if a set member is contained in set set_name, else false. |
• | @INDEX( set_name, set_member) — Returns the index of set_member in set set_name. Returns 0 if the set member is not contained in the specified set. |
• | @INSERT( set_name, set_member) — Inserts a set member into the set set_name. |
• | @NULLSET( set_name) — Deletes all set members from the derived set, set_name. |
• | @SIZE( set_name) — Returns the number of set members in set set_name. |
• | @WRAP( index, limit) — Used to "wrap" a set index from one end of a time horizon to another in multiperiod planning models. |
Each of these functions are discussed in more detail below.
@IN( set_name, primitive_1_index [, primitive_2_index ...])
This returns TRUE if the set member referenced by the primitive set member index tuple (primitive_1_index, primitive_2_index, ...) is contained in the set_name set. As the following example shows, the @IN operator is useful for generating complements of subsets in set membership conditions.
Example 1:
For example, to derive a set of open plants based on a subset of closed plants, your sets section might resemble the following:
SETS:
PLANTS / SEATTLE, DENVER,
CHICAGO, ATLANTA/:;
CLOSED( PLANTS) /DENVER/:;
OPEN( PLANTS) |
#NOT# @IN( CLOSED, &1):;
ENDSETS
The OPEN set is derived from the PLANTS set. We use a membership condition containing the @IN function to allow only those plants not contained in the CLOSED set to be in the OPEN set.
Example 2:
In this example, we illustrate how to determine if the set element (B, Y) belongs to the derived S3 set. In this case, (B, Y) is indeed a member of S3, so X will be set to 1. Note that, in order to get the index of the primitive set elements B and Y, we made use of the @INDEX function, which is discussed next.
SETS:
S1 / A B C/:;
S2 / X Y Z/:;
S3( S1, S2) / A,X A,Z B,Y C,X/:;
ENDSETS
X = @IN( S3, @INDEX( S1, B), @INDEX( S2, Y));
@INDEX( set_name, set_member)
This returns the index of a set member set_member in the set set_name. If the specified set member does not belong to the set @INDEX will return 0. Unlike @IN, which requires you to specify the indices for the set elements, @INDEX allows you to refer to set member names directly.
Example 1:
In this example, we illustrate how to get the index of set member (R1, C3) in the derived S3 set. In this case, (R1, C3) is the third member SRXC, so NDX will be set to 3.
SETS:
ROWS /R1..R27/;
COLS /C1..C3/;
RXC( ROWS, COLS): XRNG;
ENDSETS
! return the index of (r1,c3) in the rxc set;
NDX = @INDEX( RXC, R1, C3);
LINGO allows you to omit the set name argument if the set member belongs to a primitive set. This is to maintain compatibility with earlier releases of LINGO. As the following example illustrates, it's good practice to always specify a set name in the @INDEX function:
Example 2:
A model's set elements can come from external sources that the modeler may have little control over. This can potentially lead to confusion when using the @INDEX function. Consider the sets section:
SETS:
GIRLS /DEBBIE, SUE, ALICE/;
BOYS /BOB, JOE, SUE, FRED/;
ENDSETS
Now, suppose you want to get the index of the boy named Sue within the BOYS set. The value of this index should be 3. Simply using @INDEX( SUE) would return 2 instead of 3, because LINGO finds SUE in the GIRLS set first. In this case, to get the desired result, you must specify the BOYS set as an argument and enter @INDEX( BOYS, SUE).
Note: | The set member argument to @INDEX is considered to be a text literal. The following example illustrates how this can potentially lead to unexpected results. |
Example 3:
In the model below, we loop over the GIRLS set using a set index variable named G. Given that @INDEX considers the set member argument (G in this case) to be a text literal, each element of XINDEX will be set to 0; this is because the GIRLS set does not contain a set member called G. On the other hand, the elements of XIN will all be set to 1, because @IN treats G as a set index variable, as opposed to a text literal.
SETS:
GIRLS /DEBBIE, SUE, ALICE/: XINDEX, XIN;
BOYS /BOB, JOE, SUE, FRED/;
ENDSETS
!XINDEX will be 0 because GIRLS does not contain the member 'G';
!XIN will be 1 because G is treated as a set index variable as
opposed to the text literal 'G';
@FOR( GIRLS( G):
XINDEX( G) = @INDEX( GIRLS, G);
XIN( G) = @IN( GIRLS, G);
);
@INSERT( set_name, primitive_1_index [, primitive_2_index ...])
This function may be used to dynamically add members to derived sets. Each of the primitive set members forming the new derived set member must have been included in their respective primitive sets. The following example illustrates:
MODEL:
SETS:
PRIMITIVE;
LATTICE( PRIMITIVE, PRIMITIVE);
ENDSETS
DATA:
PRIMITIVE = 1..10;
ENDDATA
CALC:
! Starting at 1,1, generate the lattice of points reachable
by the two integer vectors;
X1 = 1; Y1 = 3;
X2 = 7; Y2 = 5;
@INSERT( LATTICE, 1,1); ! insert the seed;
@SET( 'TERSEO', 3); !minimal output;
! Now generate all the points reachable, directly or
indirectly from the seed via the two vectors within
a finite region;
@FOR( LATTICE( I, J) | I #LE# @SIZE( PRIMITIVE):
I1 = I + X1;
J1 = J + Y1;
@IFC( #NOT# @IN( LATTICE, I1, J1):
@IFC( @IN( PRIMITIVE, I1) #AND# @IN( PRIMITIVE, J1):
@INSERT( LATTICE, I1, J1);
);
);
I1 = I + X2;
J1 = J + Y2;
@IFC( #NOT# @IN( LATTICE, I1, I1):
@IFC( @IN( PRIMITIVE, I1) #AND# @IN( PRIMITIVE, J1):
@INSERT( LATTICE, I1, J1);
);
);
);
ENDCALC
DATA:
!display the lattice set;
@TEXT() = @TABLE( LATTICE);
ENDDATA
END
Model: LATTICE
In this model, we want to find all the points on a 10-by-10 grid that are reachable, either directly or indirectly, from point (1,1) using combinations of the two vectors (1,3) and (7,5). The grid is represented by the 2-dimensional set LATTICE. Initially, LATTICE is empty, but we will add members using @INSERT if we discover they are reachable from the seed (1,1).
First, we place the seed into the LATTICE with the following insert statement:
@INSERT( LATTICE, 1, 1); ! insert the seed;
The following doubly-nested loop iterates through the entire 10x10 grid:
@FOR( LATTICE( I, J) | I #LE# @SIZE( PRIMITIVE):
@FOR( VECTORS( I2, J2):
I3 = I + I2;
J3 = J + J2;
@IFC( #NOT# @IN( LATTICE, I3, J3):
@IFC( @IN( PRIMITIVE, I3) #AND# @IN( PRIMITIVE, J3):
@INSERT( LATTICE, I3, J3);
);
);
);
);
Each of the two vectors are then added individually to the current point, with the resulting point tested to see if a) if it has not already been added to LATTICE, and b) if it lies within the 10x10 grid. If the new point passes both these tests, it gets added to the lattice using an @INSERT statement:
@INSERT( LATTICE, I3, J3);
At the end of the model we use the @TABLE output function to display the lattice we found:
1 2 3 4 5 6 7 8 9 10
1 X
2 X
3 X
4 X
5
6
7
8 X
9 X
10
Here we see that, in addition to the seed, the following points are members of the lattice: (2,4), (3,7), (4,10), (8,6) and (9,9).
@NULLSET( set_name)
This function deletes all members of the derived set set_name.
Example 1:
In this example, we create a 3x3 derived set, S2, with 9 members. We then delete all of S2's set members with @NULLSET and verify S2 is empty by showing its member count becomes 0:
MODEL:
SETS:
S1;
S2(S1,S1);
ENDSETS
DATA:
S1 = A B C;
ENDDATA
CALC:
! Print out the current size of set S2;
@WRITE( 'Set S2 size = ', @SIZE( S2), @NEWLINE( 1));
! Delete all members from S2;
@NULLSET( S2);
! Verify that S2 now has no members;
@WRITE( 'Set S2 size after @NULLSET() = ', @SIZE( S2), @NEWLINE( 2));
ENDCALC
END
Running this model, we get the following output, showing that all members of S2 were removed by @NULLSET:
Set S2 size = 9
Set S2 size after @NULLSET() = 0
Note that @NULLSET does not currently work on primitive sets. But you can workaround this by creating a derived set directly from a primitive set, and then null out this derived set. For example:
SETS:
MYPRIMITIVESET1 /1..3/;
MYDERIVEDSET( MYSET1);
ENDSETS
CALC:
! The following nulling of a primitive set is not allowed;
@NULLSET( MYPRIMITIVESET);
! The next statement is allowed, given that we are nulling a derived set;
@NULLSET( MYDERIVEDSET);
ENDCALC
Once a set is nulled, you will typically add selected members back using the @INSERT function, described above.
@SIZE( set_name)
This returns the number of elements in the set_name set. Using the @SIZE function is preferred to explicitly listing the size of a set in a model. This serves to make your models more data independent and, therefore, easier to maintain should the size of your sets change.
To view an example of the @SIZE function, refer to the PERT/CPM example in the Sparse Derived Set Example - Explicit List section of Using Sets.
@WRAP( index, limit)
This allows you to "wrap" an index around the end of a set and continue indexing at the other end of the set. That is, when the last (first) member of a set is reached in a set looping function, use of @WRAP will allow you to wrap the set index to the first (last) member of the set. This is a particularly useful function in cyclical, multiperiod planning models.
Formally, J = @WRAP( INDEX, LIMIT) returns J such that J = INDEX - K * LIMIT, where K is an integer such that 1 <= J < LIMIT + 1. Informally, @WRAP will subtract or add LIMIT to INDEX until it falls in the range 1 to LIMIT + 0.99999.
For an example on the use of the @WRAP function in a staff scheduling model, refer to the Primitive Set Example section in Using Sets.