Set#

Simple Sets#

Introduction#

Sets are the basic building blocks of a GAMSPy model, corresponding exactly to the indices in the algebraic representations of models. A simple set consists of a set name and the elements of the set. Example:

from gamspy import Container, Set
m = Container()

i = Set(m, name = "i", records = ["seattle", "san-diego"], description = "plants")
j = Set(m, name = "j", records = ["new-york", "chicago", "topeka"],
        description = "markets")

Note

In case the name is not provided, it will be autogenerated.

The effect of these statements is probably self-evident. We declared two sets using the gamspy.Set() class and gave them the names i and j. it is recommended to give We also assigned members to the sets as follows:

  • \(i = \{Seattle, San Diego\}\)

  • \(j = \{New York, Chicago, Topeka\}\)

They are labels, but are often referred to as elements or members. The optional description may be used to describe the set for future reference and to ease readability.

Besides using the Set() class directly, one can also facilitate the addSet() method of the gamspy.Container() class:

from gamspy import Container
m = Container()

i = m.addSet(name="i", records = ["seattle", "san-diego"], description="plants")

Set declaration and data assignment can also be done separately:

from gamspy import Container, Set
m = Container()

i = Set(m, "i", description="plants")
i.setRecords(["seattle", "san-diego"])

Not only sets themselves, but also the individual elements can have a description, which is called element text:

from gamspy import Container, Set
m = Container()

i = Set(m, "i", records=[
                       ("seattle", "home of sub pop records"),
                       ("san-diego",),
                       ("washington_dc", "former gams hq"),
                   ],
)
In [1]: i.records
Out[1]:
             uni             element_text
0        seattle  home of sub pop records
1      san-diego
2  washington_dc           former gams hq

The order in which the set members are listed is usually not important. However, if the members represent, for example, time periods, then it may be useful to refer to the next or previous member. There are special operations to do this, and they are discussed in chapter Sets as Sequences: Ordered Sets. For now, it is enough to remember that the order in which set elements are specified is not relevant, unless and until some operation implying order is used. At that time, the rules change, and the set becomes what we will later call an ordered set.

Subsets#

It is often necessary to define sets whose members must all be members of some larger set. For instance, we may wish to define the sectors in an economic model:

from gamspy import Container, Set
m = Container()

i =  Set(m,
         name = "i",
         description = "all sectors",
         records = ["light-ind","food+agr","heavy-ind","services"])
t =  Set(m,
         name = "t",
         domain = i,
         description = "traded sectors",
         records = ["light-ind","food+agr","heavy-ind"])
nt = Set(m,
         name = "nt",
         description = "non-traded sectors",
         records = ["services"])

i

t

nt

0

light-ind

light-ind

1

food+agr

food+agr

2

heavy-ind

heavy-ind

3

services

services

Some types of economic activity, for example exporting and importing, may be logically restricted to a subset of all sectors. In order to model the trade balance we need to know which sectors are traded, and one obvious way is to list them explicitly, as in the definition of the set t above. The domain specification for Set t means that each member of the set t must also be a member of the set i. GAMS will enforce this relationship, which is called domain checking. Obviously, the order of declaration and definition is important: the membership of i must be known before t is defined, otherwise checking cannot be done.

Note

All elements of the subset must also be elements of the superset.

It is legal but unwise to define a subset without reference to the larger set, as is done above for the set nt. In this case domain checking cannot be performed: if services were misspelled no error would be marked, but the model may give incorrect results. Hence, it is recommended to use domain checking whenever possible. It catches errors and allows to write models that are conceptually cleaner because logical relationships are made explicit.

An alternative way to define elements of a subset is with assignments:

from gamspy import Container, Set
m = Container()

i =  Set(m,
         name = "i",
         description = "all sectors",
         records = ["light-ind","food+agr","heavy-ind","services"])
t =  Set(m,
         name = "t",
         domain = i,
         description = "traded sectors",
         records = ["light-ind","heavy-ind"])
t["food+agr"] = True

In the last line the element food+agr of the set i is assigned to the subset t. Assignments may also be used to remove an element from a subset:

t["light-ind"] = False

Note

  • Note that if a subset is assigned to, it then becomes a dynamic set.

  • A subset can be used as a domain in the declaration of other sets, variables, parameters and in equations as long as it is no dynamic set.

Multi-Dimensional Sets#

It is often necessary to provide mappings between elements of different sets. For this purpose, GAMSPy allows the use of multi-dimensional sets. For the current maximum number of permitted dimensions, see Dimensions in the GAMS documentation. The next two subsections explain how to express one-to-one and many-to-many mappings between sets.

One-to-one Mapping#

Consider a set whose elements are pairs: \(A = \{(b,d),(a,c),(c,e)\}\). In this set there are three elements and each element consists of a pair of letters. This kind of set is useful in many types of modeling. In the following example a port has to be associated with a nearby mining region:

from gamspy import Container, Set
import pandas as pd

m = Container()

i = Set(m,
        name = "i",
        description = "mining regions",
        records = ["china","ghana","russia","s-leone"])
n = Set(m,
        name = "n",
        description = "ports",
        records = ["accra","freetown","leningrad","shanghai"])

s = pd.Series(
    index=pd.MultiIndex.from_tuples([("china", "shanghai"),
                                    ("ghana", "accra"),
                                    ("russia", "leningrad"),
                                    ("s-leone", "freetown")])
)

# Alternative:
#
# s = pd.DataFrame([("china", "shanghai"),
#                   ("ghana", "accra"),
#                   ("russia", "leningrad"),
#                   ("s-leone", "freetown")],
#                  columns=["i","n"])
#
# Note that uels_on_axes needs to be set to False in multi_in in this case.

multi_in = Set(m,
               name = "in",
               domain = [i, n],
               description = "mines to ports map",
               uels_on_axes=True,
               records=s)
In [1]: multi_in.records
Out[1]:
          i         n       element_text
0     china  shanghai
1     ghana     accra
2    russia leningrad
3   s-leone  freetown

Here i is the set of mining regions, n is the set of ports and multi_in is a two dimensional set that associates each port with a mining region. The pairs are created using tuples in a pandas MultiIndex object. The set multi_in has four elements, and each element consists of a region-port pair. The domain = [i,n] indicates that the first member of each pair must be a member of the set i of mining regions, and that the second must be in the set n of ports. GAMS will domain check the set elements to ensure that all members belong to the appropriate sets.

Many-to-Many Mapping#

A many-to-many mapping is needed in certain cases. Consider the following sets:

from gamspy import Container, Set
import pandas as pd

m = Container()

i = Set(m, name = "i", records = ["a","b"])
j = Set(m, name = "j", records = ["c","d","e"])

ij1_data = pd.Series(
   index=pd.MultiIndex.from_tuples([("a", "c"),
                                    ("a", "d")])
)

ij2_data = pd.Series(
   index=pd.MultiIndex.from_tuples([("a", "c"),
                                    ("b", "c")])
)

ij3_data = pd.Series(
   index=pd.MultiIndex.from_tuples([("a", "c"),
                                    ("b", "c"),
                                    ("a", "d"),
                                    ("b", "d")])
)

ij1 = Set(m, name = "ij1", domain = [i, j], uels_on_axes=True, records=ij1_data)
ij2 = Set(m, name = "ij2", domain = [i, j], uels_on_axes=True, records=ij2_data)
ij3 = Set(m, name = "ij3", domain = [i, j], uels_on_axes=True, records=ij3_data)

Here the set ij1 presents a one-to-many mapping where one element of the set i maps onto many elements of the set j. The set ij2 represents a many-to-one mapping where many elements of the set i map onto one element of the set j. The set ij3 is the most general case: a many-to-many mapping where many elements of the set i map to many elements of the set j:

In [1]: ij3.records
Out[1]:
    i       j       element_text
0   a       c
1   b       c
2   a       d
3   b       d

Projection and Aggregation of Sets#

In GAMSPy aggregation operations on sets may be performed with an assignment and the gamspy.Sum() operator. Assignments and the sum operator are introduced and discussed in detail in chapter Indexed Operations. Here we only show how they may be used in the context of sets to perform projections and aggregations. The following example serves as illustration.

from gamspy import Container, Set, Parameter, Sum
import pandas as pd

m = Container()

i = Set(m, "i", records = [("i" + str(i), i) for i in range(1,4)])
j = Set(m, "j", records = [("j" + str(j), j) for j in range(1,3)])
k = Set(m, "k", records = [("k" + str(k), k) for k in range(1,5)])

s = pd.Series(
   index=pd.MultiIndex.from_tuples([("i1","j1","k1"),("i1","j1","k2"),("i1","j1","k3"),
                                    ("i1","j1","k4"),("i1","j2","k1"),("i1","j2","k2"),
                                    ("i1","j2","k3"),("i1","j2","k4"),("i2","j1","k1"),
                                    ("i2","j1","k2"),("i2","j1","k3"),("i2","j1","k4"),
                                    ("i2","j2","k1"),("i2","j2","k2"),("i2","j2","k3"),
                                    ("i2","j2","k4"),("i3","j1","k1"),("i3","j1","k2"),
                                    ("i3","j1","k3"),("i3","j1","k4"),("i3","j2","k1"),
                                    ("i3","j2","k2"),("i3","j2","k3"),("i3","j2","k4"),])
)
ijk = Set(m, name = "ijk", domain = [i,j,k], uels_on_axes=True, records=s)
ij1a = Set(m, name = "ij1a", domain = [i,j])

Count_1a = Parameter(m, "Count_1a")
Count_1b = Parameter(m, "Count_1b")
Count_2a = Parameter(m, "Count_2a")
Count_2b = Parameter(m, "Count_2b")

# Method 1: Using an assignment and the sum operator for a projection
ij1a[i,j] = Sum(k,ijk[i,j,k])

# Method 2: Using an assignment and the sum operator for aggregations
Count_2a[...] = Sum(ijk[i,j,k],1)
Count_1a[...] = Sum(ij1a[i,j],1)

Note that the set ijk is a three-dimensional set, its elements are 3-tuples and all permutations of the elements of the three sets i, j and k are in its domain. Thus the number of elements of the set ijk is 3 x 2 x 4 = 24. The set ij1a is a two-dimensional set that is declared in the set statement but not defined. The first assignment statement defines the members of the set ij1a. This is a projection from the set ijk to the set ij1a where the three-tuples of the first set are mapped onto the pairs of the second set, such that the dimension k is eliminated. This means that the four elements "i1.j1.k1", "i1.j1.k2", "i1.j1.k3" and "i1.j1.k4" of the set ijk are all mapped to the element "i1.j1" of the set ij1a. Note that in this context, the result of the gamspy.Sum() operation is not a number but a set. The second and third assignments are aggregations, where the number of elements of the two sets are computed. As already mentioned, the result of the first aggregation is 24 and the result of the second aggregation is 6 = 24 / 4.

Singleton Sets#

A singleton set in GAMSPy is a special set that has at most one element (zero elements are allowed as well). Like other sets, singleton sets may have a domain with several dimensions. Singleton sets are declared with the boolean is_singleton in the gamspy.Set() class (or the gamspy.Container() class).

from gamspy import Container, Set

m = Container()

i = Set(m, name = "i", records = ["a","b","c"])
j = Set(m, name = "j", is_singleton = True, records = ["d"])
k = Set(m, name = "k", is_singleton = True, domain = i, records = ["b"])
l = Set(m, name = "l", is_singleton = True, uels_on_axes=True, domain = [i,i],
        records = pd.Series(
           index=pd.MultiIndex.from_tuples([("b", "c")])
        ))
In [1]: i.records
Out[1]:
  uni       element_text
0   a
1   b
2   c

In [2]: j.records
Out[2]:
  uni       element_text
0   d

In [3]: k.records
Out[3]:
  uni       element_text
0   b

In [4]: l.records
Out[4]:
  i_0       i_1     element_text
0   b         c

The sets j, k and l are declared as singleton sets, each of them has just one element. The set k is a subset of the set i and the set l is a two-dimensional set.

Note that a data statement for a singleton set with more than one element will create a compilation error:

from gamspy import Container, Set

m = Container()

j = Set(m, name = "j", is_singleton = True, records = range(1,5))
GamspyException: Singleton set records size cannot be more than one.

It also possible to assign an element to a singleton set. In this case the singleton set is automatically cleared of the previous element first. For example, adding the following line to the code above will result in set k containing only element a after execution:

k["a"] = True

Singleton sets can be especially useful in assignment statements since they do not need to be controlled by a controlling index or an indexed operator like other sets. Consider the following example:

from gamspy import Container, Set, Parameter

m = Container()

i = Set(m, name = "i", records = ["a","b","c"])
k = Set(m, name = "k", is_singleton = True, domain = i, records = ["b"])
h = Set(m, name = "h", is_singleton = True, domain = i, records = ["a"])
n = Parameter(m, name = "n", domain = i, records = [["a", 2],["b", 3],["c", 5]])

z1 = Parameter(m, name = "z1")
z2 = Parameter(m, name = "z2")

z1[...] = n[k]
z2[...] = n[k] + 100*n[h]

The singleton sets k and h are both subsets of the set i. The parameter n is defined over the set i. The scalar z1 is assigned a value of the parameter n without naming the respective label explicitly in the assignment. It is already specified in the definition of the singleton set k. The assignment statement for the scalar z2 contains an expression where the singleton sets k and h are referenced without a controlling index or an indexed operation.

Note

Singleton sets cannot be used as domains.

The Universal Set: * as Set Identifier#

GAMSPy provides the universal set denoted by * for cases where the user wishes not to specify an index but have only a placeholder for it. The following examples show two ways how the universal set is introduced in a model. We will discuss the advantages and disadvantages of using the universal set later. First example:

from gamspy import Container, Set, Parameter

m = Container()

r = Set(m, name = "r", description = "raw materials", records = ["scrap","new"])
misc = Parameter(m, name = "misc", domain = ["*",r],
                 records = [["max-stock", "scrap", 400],
                            ["max-stock", "new", 275],
                            ["storage-c", "scrap", 0.5],
                            ["storage-c", "new", 2],
                            ["res-value", "scrap", 15],
                            ["res-value", "new", 25]])

In our example, the first index of parameter misc is the universal set "*" and the second index is the previously defined set r. Since the first index is the universal set any entry whatsoever is allowed in this position. In the second position elements of the set r must appear, they are domain checked, as usual.

The second example illustrates how the universal set is introduced in a model with an gamspy.UniverseAlias() statement:

from gamspy import Container, Set, UniverseAlias

m = Container()

r = UniverseAlias(m, name = "new_universe")
k = Set(m, name = "k", domain = r, records = "Chicago")

The gamspy.UniverseAlias() statement links the universal set with the set name new_universe. Set k is a subset of the universal set and Chicago is declared to be an element of k. Any item may be added freely to k.

Note

It is recommended to not use the universal set for data input, since there is no domain checking and thus typos will not be detected and data that the user intends to be in the model might actually not be part of it.

Observe that in GAMSPy a simple set is always regarded as a subset of the universal set. Thus the set definition

i = Set(m, "i", records = range(1,10))

is the same as

i = Set(m, "i", domain = "*", records = range(1,10))

GAMS follows the concept of a domain tree for domains in GAMS. It is assumed that a set and its subset are connected by an arc where the two sets are nodes. Now consider the following one dimensional subsets:

from gamspy import Container, Set

m = Container()

i   = Set(m, "i")
ii  = Set(m, "ii",  domain = i)
j   = Set(m, "j",   domain = i)
jj  = Set(m, "jj",  domain = j)
jjj = Set(m, "jjj", domain = jj)

These subsets are connected with arcs to the set i and thus form a domain tree that is rooted in the universe node "*". This particular domain tree may be represented as follows:

* - i - ii
      |
      - j - jj - jjj

Observe that the universal set is assumed to be ordered and operators for ordered sets such ord, lag and lead may be applied to any sets aliased with the universal set.

Set and Set Element Referencing#

Sets or set elements are referenced in many contexts, including assignments, calculations, equation definitions and loops. Usually GAMSPy statements refer to the whole set or a single set element. In addition, GAMSPy provides several ways to refer to more than one, but not all elements of a set. In the following subsections we will show by example how this is done.

Referencing the Whole Set#

Most commonly whole sets are referenced as in the following examples:

from gamspy import Container, Set, Parameter, Sum

m = Container()

i = Set(m, "i", records = [("i" + str(i), i) for i in range(1,101)])

k = Parameter(m, "k", domain = i)
k[i] = 4

z = Parameter(m, "z")
z[...] = Sum(i, k[i])

The parameter k is declared over the set i, in the assignment statement in the next line all elements of the set i are assigned the value 4. The scalar z is defined to be the gamspy.Sum() of all values of the parameter k(i).

Referencing a Single Element#

Sometimes it is necessary to refer to specific set elements. This is done by using quotes around the label(s). We may add the following line to the example above:

k["i77"] = 15

Referencing a Part of a Set#

There are multiple ways to restrict the domain to more than one element, e.g. subsets, conditionals and tuples. Suppose we want the parameter k from the example above to be assigned the value 10 for the first 8 elements of the set i. The following two lines of code illustrate how easily this may be accomplished with a subset:

j = Set(m, "j", domain = i, records = i.records[0:8])
k[j] = 10

First we define the set j to be a subset of the set i with exactly the elements we are interested in. Then we assign the new value to the elements of this subset. The other values of the parameter k remain unchanged. For examples using conditionals and tuples, see sections Conditionals and Tuples respectively.

Set Attributes#

A GAMSPy set has several attributes attached to it. For a complete list see gamspy.Set(). The attributes may be accessed like in the following example:

data[set_name] = set_name.attribute

Here data is a parameter, set_name is the name of the set and .attribute is one of the attributes listed in gamspy.Set(). The following example serves as illustration:

from gamspy import Container, Set, Parameter

m = Container()

id = Set(m, "id", records = [("Madison","Wisconsin"),
                             ("tea-time","5"),
                             ("-inf",""),
                             ("-7",""),
                             ("13.14","")])

attr = Parameter(m, "attr", domain = [id, "*"], description = "Set attribute values")

attr[id,"position"]    = id.pos
attr[id,"reverse"]     = id.rev
attr[id,"offset"]      = id.off
attr[id,"length"]      = id.len
attr[id,"textLength"]  = id.tlen
attr[id,"first"]       = id.first
attr[id,"last"]        = id.last

The parameter attr is declared to have two dimensions with the set id in the first position and the universal set in the second position. In the following seven statements the values of attr are defined for seven entries of the universal set.

position

reverse

offset

length

textLength

first

last

Madison

1

4

7

9

1

tea-time

2

3

1

8

1

-inf

3

2

2

4

-7

4

1

3

2

13.14

5

4

5

1

Implicit Set Definition (via Domain Forwarding)#

As seen above, sets can be defined through data statements in the declaration. Alternatively, sets can be defined implicitly through data statements of other symbols which use these sets as domains. This is called domain forwarding which can be achieved by using left angle < in GAMS syntax. This is illustrated in the following example:

from gamspy import Container, Set, Parameter

m = Container()

distances = [
    ["seattle", "new-york", 2.5],
    ["seattle", "chicago", 1.7],
    ["seattle", "topeka", 1.8],
    ["san-diego", "new-york", 2.5],
    ["san-diego", "chicago", 1.8],
    ["san-diego", "topeka", 1.4],
]

i = Set(m, name="i", description="plants")
j = Set(m, name="j", description="markets")

d = Parameter(
    m,
    name="d",
    domain=[i, j],
    description="distance in thousands of miles",
    records=distances,
    domain_forwarding=True,
)
print(i.records)
Set
    i 'canning plants'
    j 'markets';

Table d(i<,j<) 'distance in thousands of miles'
            new-york  chicago  topeka
seattle         2.5      1.7     1.8
san-diego       2.5      1.8     1.4;

Display i;

The domain_forwarding = True in the declaration of gamspy.Parameter() d forces set elements to be recursively included in all parent sets. Here set i will therefore contain all elements which define the first dimension of symbol d and set j will contain all elements which define the second dimension of symbol d.

In [1]: i.records
Out[1]:
          uni       element_text
0     seattle
1   san-diego

In [2]: j.records
Out[2]:
         uni        element_text
0   new-york
1    chicago
2     topeka

Note, that domain_forwarding can also pass as a list of bool to control which domains to forward. Also domain_forwarding is not limited to one symbol. One domain set can be defined through multiple symbols using the same domain.

Dynamic Sets#

Introduction#

In this section we introduce a special type of sets: dynamic sets. The sets that we discuss in detail above have their elements stated and the membership is never changed. Therefore they are called static static sets. In contrast, the elements of dynamic sets are not fixed, but may be added and removed. Dynamic sets are most often used as controlling indices in assignments or equation definitions and as the conditional set in an indexed operation. We will first show how assignments are used to change set membership in dynamic sets. Then we will introduce set operations and the last part of this chapter covers dynamic sets in the context of conditions.

Assigning Membership to Dynamic Sets#

The Syntax#

Like any other set, a dynamic set has to be declared before it may be used in the model. Often, a dynamic set is declared as subset of a static set. Dynamic sets in GAMSPy may also be multi-dimensional like static sets. For the current maximum number of permitted dimensions, see Dimensions in the GAMS documentation. For multi-dimensional dynamic sets the index sets can also be specified explicitly at declaration. That way dynamic sets are domain checked. Of course it is also possible to use dynamic sets that are not domain checked. This provides additional power and flexibility but also a lack of intelligibility and danger. Any label is legal as long as such a set’s dimension, once established, is preserved.

In general, the syntax for assigning membership to dynamic sets in GAMSPy is:

set_name[index_list | label] = True | False

Set_name is the internal name of the set in GAMSPy, index_list refers to the domain of the dynamic set and label is one specific element of the domain. An assignment statement may assign membership to the dynamic set either to the whole domain or to a subset of the domain or to one specific element. Note that, as usual, a label must appear in quotes.

Illustrative Example#

We start with assignments of membership to dynamic sets

from gamspy import Container, Set

m = Container()

item     = Set(m, name="item", records = ["dish", "ink", "lipstick", "pen", "pencil", "perfume"])
subitem1 = Set(m, name="subitem1", records = ["pen", "pencil"], domain = item)
subitem2 = Set(m, name="subitem2", domain = item)

subitem1["ink"]      = True
subitem1["lipstick"] = True
subitem2[item]       = True
subitem2["perfume"]  = False

Note that the sets subitem1 and subitem2 are declared like any other set. The two sets become dynamic as soon as they are assigned to. They are also domain checked: the only members they will ever be able to have must also be members of the set item. The first assignment not only makes the set subitem1 dynamic, it also has the effect that its superset item becomes a static set and from then on its membership is frozen. The first two assignments each add one new element to subitem1. Note that both are also elements of item, as required. The third assignment is an example of the familiar indexed assignment: subitem2 is assigned all the members of item. The last assignment removes the label "perfume" from the dynamic set subitem2.

In [1]: print(*subitem1.records["item"], sep=", ")
Out[1]: ink, lipstick, pen, pencil

In [2]: print(*subitem2.records["item"], sep=", ")
Out[2]: dish, ink, lipstick, pen, pencil

Note that even though the labels "pen" and "pencil" were declared to be members of the set subitem1 before the assignment statements that added the labels "ink" and "lipstick" to the set, they appear in the listing above at the end. The reason is that elements are displayed in the internal order, which in this case is the order specified in the declaration of the set item.

Dynamic Sets with Multiple Indices#

Dynamic sets may be multi-dimensional. The following lines continue the example above and illustrate assignments for multi-dimensional sets.

sold = Set(m, "sold", records = ["pencil", "pen"], domain = item)
sup  = Set(m, "sup", records = ["bic", "parker", "waterman"])
supply = Set(m, "supply", domain = [sold, sup])

supply["pencil", "bic"] = True
supply["pen", sup] = True
In [1]: supply.records
Out[1]:
      sold       sup        element_text
0      pen       bic
1      pen    parker
2      pen  waterman
3   pencil       bic

Equations Defined over the Domain of Dynamic Sets#

Generally, dynamic sets are not permitted as domains in declarations of sets, variables, parameters and equations. However, they may be referenced and sometimes it is necessary to define an equation over a dynamic set.

Note

The trick is to declare the equation over the entire domain but define it over the dynamic set.

For example, defining an equation over a dynamic set can be necessary in models that will be solved for arbitrary groupings of regions simultaneously. We assume there are no explicit links between regions, but that we have a number of independent models with a common data definition and common logic. We illustrate with an artificial example, leaving out lots of details.

from gamspy import Container, Set, Parameter, Variable, Equation

m = Container()

allr = Set(m, "allr", records = ["N", "S", "W", "E", "N-E", "S-W"], description = "all regions")
r    = Set(m, "r", domain = allr, description = "region subset for particular solution")
type = Set(m, "type", description = "set for various types of data")

price = Parameter(m, "price", records = 10)
data = Parameter(m, "data", domain = [allr, type], description = "all other data ...")

activity1 = Variable(m, "activity1", domain = allr, description = "first activity")
activity2 = Variable(m, "activity2", domain = allr, description = "second activity")
revenue = Variable(m, "revenue", domain = allr, description = "revenue")

resource1 = Equation(m, "resource1", domain = allr, description = "first resource constraint ...")
prodbal1 = Equation(m, "prodbal1", domain = allr, description = "first production balance ...")

resource1[r] =  activity1[r]       <=  data[r,"resource-1"]
prodbal1[r] =   activity2[r]*price == revenue[r]

To repeat the important point: the equation is declared over the set allr, but defined over r, a subset. Note that the variables and data are declared over allr but referenced over r. Then the set r may be assigned arbitrary combinations of elements of the set allr, and the model may be solved any number of times for the chosen groupings of regions.

Assigning Membership to Singleton Sets#

Singleton sets have only one element. Hence any assignment to a singleton set first clears or empties the set, no explicit action to clear the set is necessary. This is illustrated with the following example:

from gamspy import Container, Set
m = Container()

i  = Set(m, "i", records = ["a", "b", "c"], description = "Static Set")
ii = Set(m, "ii", domain = i, records = "b", description = "Dynamic Set")
si = Set(m, "si", domain = i, records = "b", is_singleton = True, description = "Dynamic Singleton Set")

ii["c"] = True
si["c"] = True

Note that both ii and si are subsets of the set i, but only si is declared as a singleton set. The assignment statements assign to both sets the element "c". While "c" is added to the set ii, it replaces the original element in the singleton set si:

In [1]: print(*ii.records["i"], sep=", ")
Out[1]: b, c

In [2]: print(*si.records["i"], sep=", ")
Out[2]: c

Set Operations#

GAMSPy provides symbols for arithmetic set operations that may be used with dynamic sets. An overview of the set operations in GAMSPy is given below. Examples and alternative formulations for each operation follow. Note that in the table below the set i is the static superset and the sets j and k are dynamic sets.

Set Operation

Operator

Description

Set Union

j[i] + k[i]

Returns a subset of i that contains all the elements of the sets j and k.

Set Intersection

j[i] & k[i]

Returns a subset of i that contains the elements of the set j that are also elements of the set k.

Set Complement

~ j[i]

Returns a subset of i that contains all the elements of the set i that are not elements of the set j.

Set Difference

j[i] - k[i]

Returns a subset of i that contains all the elements of the set j that are not elements of the set k.

Example: The set item is the superset of the dynamic sets subitem1 and subitem2. We add new dynamic sets for the results of the respective set operations.

from gamspy import Container, Set, Number
m = Container()

item     = Set(m, name="item", records = ["dish", "ink", "lipstick", "pen", "pencil", "perfume"])
subitem1 = Set(m, name="subitem1", records = ["pen", "pencil"], domain = item)
subitem2 = Set(m, name="subitem2", domain = item)

subitem1["ink"]      = True
subitem1["lipstick"] = True
subitem2[item]       = True
subitem2["perfume"]  = False

union1        = Set(m, "union1", domain = item)
union2        = Set(m, "union2", domain = item)
intersection1 = Set(m, "intersection1", domain = item)
intersection2 = Set(m, "intersection2", domain = item)
complement1   = Set(m, "complement1", domain = item)
complement2   = Set(m, "complement2", domain = item)
difference1   = Set(m, "difference1", domain = item)
difference2   = Set(m, "difference2", domain = item)

union1[item]     = subitem2[item] + subitem1[item]
union2[subitem1] = True
union2[subitem2] = True

intersection1[item] = subitem2[item] * subitem1[item]
intersection2[item] = Number(1).where[subitem1[item] & subitem2[item]]

complement1[item]     = ~subitem1[item]
complement2[item]     = True
complement2[subitem1] = False

difference1[item]     = subitem2[item] - subitem1[item]
difference2[item]     = Number(1).where[subitem2[item]]
difference2[subitem1] = False
In [1]: print(*intersection1.records["item"], sep=", ")
Out[1]: ink, lipstick, pen, pencil

Looking at the results of each operation will show that the above assignment statements for each operation result in the same dynamic set like using the set operator. Observe that the alternative formulations for the set intersection and set difference involve conditional assignments. Conditional assignments in the context of dynamic sets are discussed in depth in the next section.

Note

The indexed operation gamspy.Sum() may be used for set unions. Similarly, the indexed operation gamspy.Product() may be used for set intersections. For examples see section Conditional Indexed Operations with Dynamic Sets below.

Controlling Dynamic Sets#

Recall that set membership of subsets and dynamic sets may be used as a logical condition. Set membership may also be a building block in complex logical conditions that are constructed using the logical python operators ~ (not), & (and), | (or), ^ (xor), not(x) or y (logical implication) and == (logical equivalence). Moreover, the set operations introduced in the previous section may also be used in logical conditions. Dynamic sets can be controlled in the context of assignments, indexed operations and equations. We will discuss in detail each of these in the following subsections.

Apart from being part of logical conditions, dynamic sets may be assigned members with conditional assignments. Examples are given in the next subsection.

Dynamic Sets in Conditional Assignments#

Dynamic sets may be used in two ways in conditional assignments: they may be the item on the left-hand side that is assigned to and they may be part of the logical condition. Below we present examples for both.

from gamspy import Container, Set

m = Container()

item     = Set(m, name="item",
               records = ["dish", "ink", "lipstick", "pen", "pencil", "perfume"])
subitem1 = Set(m, name="subitem1",
               records = ["ink", "lipstick", "pen", "pencil"],
               domain = item)
subitem2 = Set(m, name="subitem2", domain = item)

subitem2[item].where[subitem1[item]] = True

The conditional assignment adds the members of dynamic set subitem1 to the dynamic set subitem2. Thus subitem2 will have the following elements:

In [1]: print(*subitem2.records["item"], sep=", ")
Out[1]: ink, lipstick, pen, pencil

Note that instead of using subitem1 in where[] we could also write:

subitem2[subitem1] = True

In the next example of a conditional assignment, a dynamic set features in the logical condition on the right-hand side. The first statement clears the set subitem2 of any previously assigned members and the second statement assigns all members of subitem1 to subitem2 using gamspy.Number(). The following conditional assignment will have the same result:

subitem2[item] = False
subitem2[item] = Number(1).where[subitem1[item]]

The logical condition in this assignment is subitem1[item]. It is satisfied for all members of the set subitem1. Hence the statement assigns all elements of the domain item that are members of the set subitem1 to the dynamic set subitem2. Note that in this assignment the where[] is on the right. Conditional assignments with where[] on the right-hand side imply an if-then-else structure where the else case is automatically zero. Unlike parameters, dynamic sets cannot be assigned the value of zero, they are assigned False instead. Therefore a more explicit formulation of the conditional assignment above would be:

subitem2[item] = False
subitem2[item] = Number(1).where[subitem1[item]] + Number(0).where[~ subitem1[item]]

Conditional Indexed Operations with Dynamic Sets#

Indexed operations in GAMSPy may be controlled by where[] conditions. The domain of conditional indexed operations is often restricted by a set, called the conditional set. Dynamic sets may be used as conditional sets or they may be assigned to with a statement that features a conditional indexed operation on the right-hand side. We will illustrate both cases with examples.

Suppose we have a set of origins, a set of destinations and a parameter specifying the flight distance between them:

from gamspy import Container, Set, Parameter, Smax, Domain
import pandas as pd

m = Container()

distances = pd.DataFrame(
    [
        ["Chicago", "Vancouver", 1777],
        ["Chicago", "Bogota", 2691],
        ["Chicago", "Dublin", 3709],
        ["Chicago", "Rio", 5202],
        ["Chicago", "Marrakech", 4352],
        ["Philadelphia", "Vancouver", 2438],
        ["Philadelphia", "Bogota", 2419],
        ["Philadelphia", "Dublin", 3306],
        ["Philadelphia", "Rio", 4695],
        ["Philadelphia", "Marrakech", 3757],
    ],
    columns=["from", "to", "distance"],
).set_index(["from", "to"])

i = Set(m, name="i", records = ["Chicago", "Philadelphia"], description = "origins")
j = Set(m, name="j", records = ["Vancouver", "Bogota", "Dublin", "Rio", "Marrakech"],
        description = "destinations")

d = Parameter(m, "d", domain = [i, j], records = distances.reset_index(),
              description = "distance (miles)")

We wish to find the longest distance that we can travel given that we have a limit of 3500 miles.

can_do = Set(m, name="can_do", domain = [i, j],
             description = "connections with less than 3500 miles")

can_do[i,j].where[d[i,j] < 3500] = True

maxd = Parameter(m, "maxd", description = "longest distance possible")
maxd[...] = Smax(Domain(i,j).where[can_do[i,j]], d[i,j])

The dynamic set can_do contains all connections that are less than 3500 miles. The scalar maxd is defined by a conditional assignment where the indexed operation gamspy.Smax() scans all entries of the parameter d whose label combinations are members of the set can_do and chooses the largest value.

In [1]: can_do.pivot(index = "i", columns = "j")
Out[1]:
               Vancouver    Bogota  Dublin
Chicago             True      True   False
Philadelphia        True      True    True

In [2]: maxd.records
Out[2]:
     value
0   3306.0

There is a shorter alternative formulation for this assignment; see subsection Filtering through Dynamic Sets below for details.

Finally, we also wish to know which flight connection is linked to the longest possible distance. Consider the following two lines:

maxc = Set(m, name="maxc", domain = [i, j], is_singleton = True, description = "maximum distance connection")
maxc[i,j] = Number(1).where[can_do[i,j] & (d[i,j] == maxd)]

Which gives

In [1]: maxc.records
Out[1]:
               i           j        element_text
0   Philadelphia      Dublin

The dynamic singleton set is assigned the member of the dynamic set can_do whose distance equals the maximum distance.

The full power of indexed operators becomes apparent with multi-dimensional dynamic sets

from gamspy import Container, Set, Sum, Product

m = Container()

dep = Set(m, "dep", description="departments",
          records=["cosmetics", "hardware", "household", "stationary", "toy"])
sup = Set(m, "sup", description="suppliers",
          records=["bic", "dupont", "parker", "revlon"])
item = Set(m, "item", description="items_sold",
           records=["dish", "ink", "lipstick", "pen", "pencil", "perfume"])

sales_data = {
    ("cosmetics", "lipstick"),
    ("cosmetics", "perfume"),
    ("hardware", "ink"),
    ("household", "dish"),
    ("household", "pen"),
    ("stationary", "dish"),
    ("stationary", "ink"),
    ("stationary", "pen"),
    ("stationary", "pencil"),
    ("toy", "ink"),
    ("toy", "pen"),
    ("toy", "pencil")
}

sales = Set(m, name="sales", domain=[dep, item],
            description="departments and items sold", uels_on_axes=True,
            records=sales_data)

# Note the alternative, more compact notation of the supply data.
# GAMSPy still needs flat data in the end
supply_data = {
    "dish": ("bic", "dupont"),
    "ink": ("bic", "parker"),
    "lipstick": "revlon",
    "pen": ("parker", "revlon"),
    "pencil": ("bic", "parker"),
    "perfume": "revlon"
}

supply = Set(m, name="supply", domain=[item, sup],
             description="items and suppliers", uels_on_axes=True,
             records=[(item, sup) for item, sups in supply_data.items()
                      for sup in (sups if isinstance(sups, (list, tuple))
                                  else [sups])])

g03 = Set(m, name = "g03", domain = dep,
            description = "departments selling items supplied by Parker")

g03[dep] = Sum(item.where[supply[item,"parker"]], sales[dep,item])

The assignment above is used to create the set of departments that sell items supplied by "parker". Note that the set g03 is a subset of the set dep. Its members are specified by assignment, hence it is a dynamic set. Note that the assignment is made to a set, therefore the indexed operator gamspy.Sum() refers to a set union (and not to an addition as would be the case if the assignment were made to a parameter). The indexed operation is controlled by the two-dimensional set supply with the label "parker" in the second index position. This logical condition is True for all members of the set supply where the second index is "parker". Hence the summation is over all items sold, provided that the supplier is "parker". Given the declaration of the set supply, this means "ink", "pen" and "pencil". The associated departments are thus all departments except for "cosmetics":

In [1]: print(*g03.records["dep"], sep=", ")
Out[1]: hardware, household, stationary, toy

Now suppose we are interested in the departments that are selling only items supplied by "parker". We introduce a new dynamic set g11 and the following assignment adds the desired departments:

g11 = Set(m, name = "g11", domain = dep,
            description = "departments only selling items supplied by parker")

g11[dep] = Product(sales[dep,item], supply[item,"parker"]);

Note that the indexed operation gamspy.Product() refers to set intersections in the context of assignments to dynamic sets. From all departments linked with items only those are included where all items sold are supplied by "parker". This means that departments that additionally sell items that are not supplied by "parker" are excluded. Hence, only "hardware" and "toy" are added to g11.

In [1]: print(*g11.records["dep"], sep=", ")
Out[1]: hardware, toy

Conditional Equations with Dynamic Sets#

where[] conditions in the context of equations may restrict the domain of the equation and they may also feature in the algebraic formulation of the equation. In both instances dynamic sets may be used as part of the logical condition. where[] conditions with dynamic sets in the algebra of equations are similar to conditional assignments with dynamic sets; see section Dynamic Sets in Conditional Assignments above. The example that follows illustrates the use of a dynamic set to restrict the domain of definition of an equation. In section Equations Defined over the Domain of Dynamic Sets above we had the following equation definition:

prodbal1[r] =   activity2[r]*price == revenue[r]

Recall that r is a dynamic set and a subset of the set allr. Hence this equation may be rewritten in the following way:

prodbal1[allr].where[r[allr]] =   activity2[allr]*price == revenue[allr]

Note that both formulations achieve the same result: restricting the domain of definition to those elements that belong to the dynamic set r. While in the second formulation the condition is specified explicitly, in the first formulation the domain is filtered through the dynamic set r. This is the topic of the next subsection.

Filtering through Dynamic Sets#

In certain circumstances the filtering process is an alternative to the where[] condition to restrict the domain of equations, sets, variables, parameters and indexed operations. We already saw an example for restricting the domain of definition of an equation in the previous subsection. The next example refers to restricting the domain in an indexed operation. In section Conditional Indexed Operations with Dynamic Sets we had the following assignment:

maxd[...] = Smax(Domain(i,j).where[can_do[i,j]], d[i,j])

Recall that maxd is a scalar, i and j are sets, can_do is a dynamic set and d is a two-dimensional parameter. Note that the conditional set is the dynamic set can_do. The assignment may be rewritten in the following way:

maxd[...] = Smax(can_do[i,j], d[i,j])

Here the indexed operation is filtered through the dynamic set can_do, a where[] condition is not necessary.

Sets as Sequences: Ordered Sets#

Introduction#

We initially stated that in general, sets in GAMSPy are regarded as an unordered collection of labels. However, in some contexts, say, multi-period planning models, some sets need to be treated as if they were sequences. In this chapter we will establish the notion of ordered sets and we will cover their special features and the associated operations.

Examples where ordered sets are needed include economic models that explicitly represent conditions in different time periods that are linked, location problems where the formulation may require a representation of contiguous areas, as in a grid representation of a city, scheduling problems and programs that model stocks of capital with equations of the form ‘stocks at the end of period \(n\) are equal to stocks at the end of period \(n-1\) plus net gains during period \(n\)’.

Note

Models involving sequences of time periods are often called dynamic models, because they describe how conditions change over time. This use of the word dynamic unfortunately has a different meaning from that used in connection with Dynamic Sets, but this is unavoidable.

Ordered and Unordered Sets#

Certain one-dimensional sets may be treated as if they were a sequence. Those sets need to be ordered and static. A one-dimensional set is ordered if the definition or initialization of the elements in the set corresponds to the order of the labels in the GAMSPy Entry order.

Note

  • The GAMSPy entry order is the order in which the individual labels first appear in the GAMSPy program.

  • For the sake of simplicity, sets that are static and ordered are often just referred to as ordered sets.

GAMS maintains a unique element list where all labels that are used as elements in one or more sets are listed. The order of the elements in any one set is the same as the order of those elements in the unique element list. This means that the order of a set may not be what it appears to be if some of the labels were used in an earlier definition. The internal GAMS order of the labels can be made visible with the getUELs() method of the gamspy.Container() class. A good rule of thumb is that if the user wants a set to be ordered and the labels in the set have not been used already, then they will be ordered.

In the example below we show ordered and unordered sets and the map showing the order. The input is:

from gamspy import Container, Set

m  = Container()

t1 = Set(m, name = "t1", records = ["1987","1988","1989","1990","1991"])
t2 = Set(m, name = "t2", records = ["1983","1984","1985","1986","1987"])
t3 = Set(m, name = "t3", records = ["1987","1989","1991","1983","1985"])

Note that the label "1987" is the first label seen by GAMS. It appears again as the last label in the initialization list for the set t2. This means that the set t2 is not ordered and any attempt to use t2 in a context implying order will cause error messages. Observe that the set t3 is ordered, as all the members of t3 have appeared in the GAMSPy program before, and in the same order that they are listed in the definition of t3.

In [1]: m.getUELs()
Out[1]: ['1987', '1988', '1989', '1990', '1991', '1983', '1984', '1985', '1986']

Note

A set can always be made ordered by moving its declaration closer to the beginning of the program.

Sorting a Set#

reorderUELs is a method of all GAMSPy symbol classes. This method allows the user to reorder UELs of a specific symbol dimension. Example:

from gamspy import Container, Set, Parameter

m = Container()

i = Set(m, "i", records=["i1", "i2", "i3"])
j = Set(m, "j", i, records=["j1", "j2", "j3"])
a = Parameter(m, "a", [i, j], records=[(f"i{i}", f"j{i}", i) for i in range(1,4)])
In [1]: i.getUELs()
Out[1]: ['i1', 'i2', 'i3']

In [2]: m.getUELs()
Out[2]: ['i1', 'i2', 'i3', 'j1', 'j2', 'j3']

But perhaps we want to reorder the UELs i1, i2, i3 to i3, i2, i1.

In [1]: i.reorderUELs(['i3', 'i2', 'i1'])
In [2]: i.getUELs()
Out[2]: ['i3', 'i2', 'i1']

In [3]: i.records
Out[3]:
    uni   element_text
0    i1
1    i2
2    i3

Note that this example does not change the indexing scheme of the Pandas DataFrame at all, it only changes the underlying integer numbering scheme for the categories. We can see this by looking at the Pandas codes:

In [1]: i.records["uni"].cat.codes
Out[1]:
0    2
1    1
2    0
dtype: int8