# Conditional Expressions, Assignments and Equations#

## Introduction#

This chapter deals with the way in which conditional assignments, expressions and
equations are made in GAMS. The index operations already described are very
powerful, but it is necessary to allow for exceptions of one sort or another.
For example, heavy trucks may not be able to use a particular route because of a
weak bridge, or some sectors in an economy may not produce exportable products.
Exceptions such as these may easily be modeled with a logical condition combined
with the `where`

operator, a very powerful feature of GAMSPy introduced in
this chapter.

## The ‘where’ Statement#

The `where`

operator is a very powerful feature in GAMSPy. The general syntax
for a conditional expression is:

```
term.where[logical_condition]
```

Here, `term`

can be a number, a (indexed) symbol, and also a complex expression.
The `where`

operator may be read as under the condition that the following
`logical_condition`

evaluates to TRUE.

Consider the following simple condition, where `u[i]`

, `s[i]`

and `v[i]`

are
parameters with index `i`

:

```
u[i].where[~ s[i]] = v[i]
```

Note that the `term`

is the parameter `u[i]`

and the `logical condition`

is
the expression `~ s[i]`

. If the condition is not satisfied, no assignment is made.
To make it clear, this conditional assignment may be read as: ‘*given that s[i] is
not existing, u[i] equals v[i]*’.

Logical conditions may take various forms, they are introduced in the next section. Conditional expressions may be used in the context of assignments, indexed operations and equations. These topics are covered in later sections of this chapter.

Note

Logical conditions used with the `where`

operator cannot include variables.
However, variable attributes are allowed.

Note

`where`

operator is equivalent to the dollar condition $ in GAMS.

## Logical Conditions#

Logical conditions are special expressions that evaluate to a value of either TRUE or
FALSE. Logical conditions may be numerical expressions and numerical relations and
they may refer to set membership. In the following subsections this is shown in the
context of simple conditional assignments with the `where`

operator on the
left-hand side.

### Numerical Expressions#

Numerical expressions may serve as logical conditions: a result of zero is treated as the logical value FALSE and a non-zero result is treated as the logical value TRUE. The following example illustrates this point.

```
from gamspy import Container, Set, Parameter, Number
m = Container()
i = Set(m, name="i", records=["i1","i2","i3","i4","i5"])
s = Parameter(m, "s", domain = i,
records = [["i1", 3],
["i2", 5],
["i3", 6]])
u = Parameter(m, "v", domain = i)
u[i].where[2*s[i]-6] = 7
```

```
In [1]: u.records
Out[1]:
i value
0 i2 7.0
1 i3 7.0
```

Here the numerical expression is the logical condition. The numerical expression is
zero if `a[i]`

equals 3, and non-zero otherwise. Hence the logical value of the
expression is FALSE for `a[i] = 3`

and TRUE for all other values of a[i]. The
assignment is only made if the numerical expression evaluates to TRUE, otherwise
no assignment is made.

Note

Values of the extended range arithmetic such as

`float("inf")`

are also allowed in logical conditions. If the result of a numerical expression used as a logical condition takes any of these values, the logical value is TRUE.Observe that

`gamspy.math()`

functions are also allowed in logical conditions. If they evaluate to zero, the logical condition is FALSE, otherwise it is TRUE.

### Numerical Relational Operators#

Numerical relational operators compare two numerical expressions and return a logical value. Consider the following examples.

```
from gamspy import Container, Set, Parameter, Number
m = Container()
i = Set(m, name="i", records=["i1","i2","i3","i4","i5"])
s = Parameter(m, "s", domain = i,
records = [["i1", 3],
["i2", 5],
["i3", 6]])
u = Parameter(m, "v", domain = i)
u[i].where[s[i] >= 5] = u[i] + 10
```

```
In [1]: u.records
Out[1]:
i value
0 i2 11.0
1 i4 10.0
```

The assignment `u[i].where[s[i] >= 5] = u[i] + 10`

depends on whether `s[i]`

is greater or
equal to 5. If this is the case, an assignment is made, otherwise not.

### Bitwise Operators#

Bitwise operators can be used to combine two or more logical conditions to build complex logical
expressions. For example, if several expressions are required to be TRUE simultaneously, they may
be connected with the python bitwise operator `&`

. For all available bitwise operators in python
read more here. Another
way to construct complex logical conditions is by nesting them. For details, see subsection
Nested Conditions below.

The following somewhat artificial examples serve as illustration.

```
from gamspy import Container, Set, Parameter
m = Container()
i = Set(m, name="i", records=["i1","i2","i3","i4","i5"])
s = Parameter(m, "s", domain = i,
records = [["i1", 3],
["i2", 5],
["i4", 8]])
t = Parameter(m, "t", domain = i,
records = [["i1", 13],
["i2", 13],
["i3", 13],
["i4", 13]])
u = Parameter(m, "u", domain = i,
records = [["i2", 1]])
v = Parameter(m, "v", domain = i,
records = [["i1", 7],
["i3", 2]])
u[i].where[~ s[i]] = v[i]
u[i].where[s[i] & u[i] & t[i]] = s[i]
u[i].where[s[i] | v[i] | t[i]] = 4
```

Note that there are three conditional assignments for the parameter `u`

. In the first assignment
the logical condition is `~ s[i]`

. This condition holds for all entries of `s`

that are not
specified and therefore zero by default: `s['i3']`

and `s['i5']`

. Hence `u['i3']`

and
`u['i5']`

are assigned the values of `v['i3']`

and `v['i5']`

respectively. The value of
`v['i3']`

is 2 and the value of `v['i5']`

is zero by default. After the first assignment we
have `u['i2']=1`

and `u['i3']=2`

, all other values of `u`

are zero. Note that the logical
condition failed for `u['i2']`

and therefore its value remained unchanged.

```
In [1]: u.records
Out[1]:
i value
0 i2 1.0
1 i3 2.0
```

The logical condition
in the second assignment is `TRUE`

for those labels of the set `i`

that have non-zero entries
in the parameters `s`

, `u`

and `t`

simultaneously. This condition holds only for `i2`

.
Therefore `u['i2']=s['i2']=5`

and all other values of `u`

remain unchanged, resulting in
non-zero values only for `u['i2']`

and `u['i3']`

.

```
In [2]: u.records
Out[2]:
i value
0 i2 5.0
1 i3 2.0
```

The logical condition in the last assignment
evaluates to `TRUE`

for all labels of the set `i`

that have at least one non-zero entry in the
parameters `s`

, `v`

and `t`

. This holds for all labels except for `i5`

. Therefore
`u['i5']`

stays zero and all other values of `u`

are changed to `4`

.

```
i value
0 i1 4.0
1 i2 4.0
2 i3 4.0
3 i4 4.0
```

These examples demonstrate the power of the `where`

operator combined with bitwise operators.
Even more complex logical conditions are possible; see subsection
Mixed Logical Conditions below for details.

### Set Membership and Set Functions#

Apart from numerical and relational expressions, set membership and functions referencing set elements may be used as a logical condition. Consider the following example as illustration for set membership as logical condition.

```
from gamspy import Container, Set, Parameter
m = Container()
i = Set(m, name="i", records=["i1","i2","i3","i4","i5"])
j = Set(m, name="j", records=["i1","i2","i3"], domain = i)
s = Parameter(m, "s", domain = i,
records = [["i1", 3],
["i2", 5],
["i3", 11],
["i4", 8],
["i5", 1]])
t = Parameter(m, "t", domain = i)
t[i].where[j[i]] = s[i] + 3
```

```
In [1]: t.records
Out[1]:
i value
0 i1 6.0
1 i2 8.0
2 i3 14.0
```

Note that the set `j`

is a subset of the set `i`

and that the parameter `t`

is declared
but not defined. The conditional expression `t[i].where[j[i]]`

in the last line restricts
the assignment to the members of the subset `j`

since only they satisfy the condition
`j[i]`

. The values for `t['i4']`

and `t['i5']`

remain unchanged. In this case this
means that they are zero (by default). Note that there is an alternative formulation for
this type of conditional assignment; for details see subsection
Filtering Sets in Assignments below.

Note

Only the membership of subsets and dynamic sets may be used as logical conditions.

The use of set membership as a logical condition is an extremely powerful feature of GAMSPy, see section Conditional Equations below for more examples.

Logical conditions may contain the method sameas or set operators that return particular values depending on the position of elements in sets, the size of sets or the comparison of set elements to each other or text strings. In the following example we have two sets of cities and we want to know how many of them feature in both sets.

```
from gamspy import Container, Set, Parameter, Sum, Domain
m = Container()
i = Set(m, name="i", records=["Beijing","Calcutta","Mumbai","Sydney","Johannesburg","Cairo "])
j = Set(m, name="j", records=["Rome","Paris","Boston","Cairo","Munich","Calcutta","Barcelona "])
b = Parameter(m, "b")
b[...] = Sum(Domain(i,j).where[i.sameAs(j)],1)
```

In the assignment statement we `Sum`

over both sets and we use `sameAs`

to
restrict the domain of the indexed operation to those label combinations `(i,j)`

where `sameAs`

evaluates to TRUE. Thus only identical elements are counted.

The operators ord and card are frequently used to single out the first or last element of an ordered set. For example, we may want to fix a variable for the first and last elements of a set:

```
from gamspy import Container, Set, Variable, Ord, Card
m = Container()
i = Set(m, name="i", records=["Beijing","Calcutta","Mumbai","Sydney","Johannesburg","Cairo"])
j = Set(m, name="j", records=["Rome","Paris","Boston","Cairo","Munich","Calcutta","Barcelona"])
x = Variable(m, "x", domain=[i])
x.fx[i].where[Ord(i) == 1] = 3
x.fx[i].where[Ord(i) == Card(i)] = 7
```

In the first assignment the variable `x`

is fixed for the first element of the set `i`

and in
the second assignment `x`

is fixed for the final element of `i`

.

### Mixed Logical Conditions#

The building blocks introduced in the subsections above may be combined to generate more complex logical conditions. These may contain standard arithmetic operations, numerical relational operations and logical/bitwise operations. All operations, their symbols and their order of precedence are given below. Note that 1 denotes the highest order of precedence and 7 denotes the lowest order of precedence. As usual, the default order of precedence holds only in the absence of parentheses and operators (symbols) on the same level are evaluated from left to right.

Type of Operation |
Operation |
Operator |
Order of precedence |
---|---|---|---|

Standard arithmetic operation |
Exponentiation, Floor division |
**, // |
1 |

Standard arithmetic operation |
Multiplication, Division |
*, / |
2 |

Standard arithmetic operation |
Unary operators: Plus, Minus |
+, - |
3 |

Standard arithmetic operation |
Binary operators: Addition, Subtraction |
+, - |
3 |

Numerical Relational operation |
All |
<, <=, ==, !=, >=, > |
4 |

Logical operation |
Negation |
~ |
5 |

Logical operation |
Logical Conjunction |
& |
6 |

Logical operation |
All other logical operations |
|, ^, <<, >> |
7 |

Note

We recommend to use parentheses rather than relying on the order of precedence of operators. Parentheses prevent errors and make the intention clear.

Consider the following example:

```
x - 5*y & z - 5
(x - (5*y)) & (z-5)
```

These two complex logical conditions are equivalent. However, the parentheses make the second expression easier to understand.

Some simple examples of complex logical conditions, their numerical values and their logical values are given below.

Logical Condition |
Numerical Value |
Logical Value |
---|---|---|

(1 < 2) + (3 < 4) |
2 |
TRUE |

(2 < 1) & (3 < 4) |
0 |
FALSE |

(4*5 - 3) + (10/8) |
18.25 |
TRUE |

(4*5 - 3) | (10 - 8) |
1 |
TRUE |

(4 & 5) + (2*3 <= 6) |
2 |
TRUE |

(4 & 0) + (2*3 < 6) |
0 |
FALSE |

### Nested Conditions#

An alternative way to model complex logical conditions is by nesting them. The syntax is:

```
term.where[logical_condition1.where[logical_condition2.where[...]]]
```

Note that in nested conditions all succeeding expressions after the `where`

operator must
be enclosed in parentheses. The nested expression is equivalent to the following conditional
expression that uses the logical operator `&`

instead of the nesting:

```
term.where[logical_condition1 & logical_condition2 & ...]
```

Consider the following example.:

```
from gamspy import Container, Set, Parameter
m = Container()
i = Set(m, name="i", records=["i1","i2","i3","i4","i5"])
j = Set(m, name="j", records=["i1","i2","i3"], domain = i)
k = Set(m, name="k", records=["i1","i2"], domain = i)
u = Parameter(m, "u", domain = i)
v = Parameter(m, "v", domain = i,
records = [["i1", 7],
["i3", 2]])
u[i].where[j[i].where[k[i]]] = v[i]
```

```
In [1]: u.records
Out[1]:
i value
0 i1 7.0
```

Note

We recommend to use the logical `&`

operator instead of nesting conditions, because
this formulation is easier to read.

## Conditional Assignments#

A conditional assignment is an assignment statement with a `where`

condition on the
left-hand side or on the right-hand side. Most examples until now were conditional assignments
with the `where`

operator on the left.

Warning

he effect of the `where`

condition is significantly different depending on which side
of the assignment it is located.

The next two subsections describe the use of the `where`

condition on each side of the
assignment. Note that in many cases it may be possible to use either of the two forms of
the `where`

condition to describe an assignment. We recommend to choose the clearer
formulation.

Note that if the logical condition in an assignment statement refers to set membership,
then under certain conditions the restriction may be expressed without the use of the
`where`

operator. For details, see section
Filtering Sets in Assignments below.

### where[] on the Left#

If the `where`

condition is on the left-hand side of an assignment, an assignment is
made only in case the logical condition is satisfied. If the logical condition is not
satisfied then no assignment is made and the previous content of the parameter on the left
will remain unchanged. In case the parameter on the left-hand side of the assignment has
not previously been initialized or assigned any values, zeros will be used for any label
for which the assignment was suppressed.

Consider the following example. Note that the parameter `sig`

has been previously
defined in the model.

```
rho[i].where[sig[i] <> 0] = (1/sig[i]) - 1
```

In this assignment `rho[i]`

is calculated and the `where`

condition on the left
protects against dividing by zero. If any of the values associated with the parameter
`sig`

turns out to be zero, no assignment is made and the previous values of
`rho[i]`

remain. As it happens, `rho[i]`

was not previously initialized, and
therefore all the labels for which `sig[i]`

is zero will result in a value of zero.

Now recall the convention that non-zero implies TRUE and zero implies FALSE. The assignment above could therefore be written as:

```
rho[i].where[sig[i]] = (1/sig[i]) - 1
```

In the following examples `i`

is a set and `s`

and `t`

are parameters.

```
s[i].where[t[i]] = t[i]
s[i].where[(t[i]-1) > 0] = t[i]**0.5
```

Note that the first assignment is suppressed if the value of the parameter `t`

equals
zero. The second assignment is suppressed for values of the parameter `t`

that are
smaller or equal to 1.

### where[] on the Right#

If the `where`

condition is on the right-hand side of an assignment statement, an
assignment will *always* be made. In case the logical condition is not satisfied the value
of zero is assigned. Example:

```
u[i].where[s[i] >= 5] = 7
```

Now we move the `where`

condition to the right-hand side:

```
u[i] = Number(7).where[s[i] >= 5]
```

This is equivalent to:

```
if (s[i] >= 5) then (u[i] = 7), else (u[i] = 0)
```

Note that an `if-then-else`

type of construct is implied, but the `else`

operation is
predefined and never made explicit. The else could be made explicit with the following
formulation:

```
u[i] = Number(7).where[s[i] >= 5] + Number(0).where[s[i] < 5]
```

The use of this feature is more apparent in instances when an `else`

condition needs to
be made explicit. Consider the next example. The set `i`

is the set of `plants`

, and we
are calculating `mur[i]`

, the cost of transporting imported raw materials. In some cases
a barge trip must be followed by a road trip because the plant is not alongside the river
and we must combine the separate costs. The assignment is:

```
mur[i] = (1.0 + 0.0030 * ied[i,'barge']).where[ied[i,'barge']]
+ (0.5 + 0.0144 * ied[i,'road' ]).where[ied[i,'road' ]]
```

This means that if the entry in the distance parameter `ied`

is not zero, then the cost
of shipping using that link is added to the total cost. If there is no distance entry,
there is no contribution to the cost, presumably because that mode is not used.

Consider another example for a conditional assignment with the `where`

operator on
the right:

```
b = Sum(i, t[i]).where[a > 0] + 4
```

Here `a`

and `b`

are scalars, `i`

is a set and `t`

is a parameter. If the scalar
`a`

is positive, the scalar `b`

is assigned the sum of all values of the parameter
`t`

plus 4. If `a`

is zero or negative, `b`

becomes just 4. Note that the sum is
only computed if the condition holds, this potentially makes the program faster.

## Conditional Indexed Operations#

We have seen how exceptions in assignments are modeled with `where`

conditions.
`where`

conditions are also used in indexed operations, where they control the
domain of operations. This is conceptually similar to the conditional assignment
with the `where`

on the left.

Consider the following example adapted from a gas trade model for interrelated gas
markets. Here the set `i`

contains supply regions and the parameter `supc`

models
supply capacities. The scalar `tsupc`

is computed with the following statement:

```
tsupc = Sum(i.where[supc[i] != float("inf")], supc[i])
```

This assignment restricts the `Sum`

to the finite values of the
parameter `supc`

.

In indexed operations the logical condition is often a set. This set is called the
*conditional set* and assignments are made only for labels that are elements of the
conditional set. This concept plays an important role in
dynamic sets.

Multi-dimensional sets are introduced in section Multi-Dimensional Sets. In the example used there a two-dimensional set is used to define the mapping between countries and ports. Another typical example for a multi-dimensional set is a set-to-set mapping that defines the relationship between states and regions. This is useful for aggregating data from the state to the regional level. Consider the following example:

```
from gamspy import Container, Set, Parameter, Sum
import pandas as pd
m = Container()
r = Set(m, name = "r", description = "regions")
s = Set(m, name = "s", description = "states")
c = pd.Series(
index=pd.MultiIndex.from_tuples([("north", "vermont"),
("north", "maine"),
("south", "florida"),
("south", "texas")])
)
corr = Set(m, name = "corr",
domain = [r,s],
uels_on_axes=True,
domain_forwarding = True,
records = c)
y = Parameter(m, "y", domain = r, description = "income for each region")
income = Parameter(m, "income", domain = s, description = "income for each state",
records = [["florida", 4.5],
["vermont", 4.2],
["texas", 6.4],
["maine", 4.1]])
```

The set `corr`

links the states to their respective regions, the parameter `income`

is the income of each state. The parameter `y`

is computed with the following assignment
statement:

```
y[r] = Sum(s.where[corr[r,s]], income[s])
```

The conditional set `corr[r,s]`

restricts the domain of the summation: for each region
`r`

the summation over the set `s`

is restricted to the label combinations `(r,s)`

that are elements of the set `corr[r,s]`

. Conceptually, this is analogous to the Boolean
value TRUE or the arithmetic value non-zero. The effect is that only the contributions of
`vermont`

and `maine`

are included in the total for `north`

, and `south`

is the
sum of the incomes from only `texas`

and `florida`

.

```
In [1]: y.records
Out[1]:
r value
0 north 8.3
1 south 10.9
```

Note that the summation above can also be written as:

```
y[r] = Sum(s,income[s].where[corr[r,s]])
```

In this formulation the parameter `income`

is controlled by the conditional set `corr`

instead of the index `s`

. Note that both formulations yield the same result, but the second
alternative is more difficult to read.

Note that if the logical condition in the context of indexed operations refers to set
membership, then under certain conditions the restriction may be expressed without the use of
the `where`

operator. For details, see section
Filtering Controlling Indices in Indexed Operations
below.

## Conditional Equations#

The `where`

operator is also used for exception handling in equations. The next two subsections
discuss the two main uses of `where`

operators in the context of equations: in the body of an
equation and over the domain of definition.

### Dollar Operators within the Algebra of Equations#

A `where`

operator in the algebraic formulation of an equation is analogous to the `where`

on the right of assignments, as presented in section where[] on the Right.
Assuming that “the right” means the right of the `'='`

then the analogy is even closer. As in
the context of assignments, an if-else operation is implied. It is used to exclude parts of the
definition from some of the generated constraints.

```
from gamspy import Container, Set, Variable, Equation, Sum
m = Container()
i = Set(m,
name = "i",
description = "sectors",
records = ["light-ind","food+agr","heavy-ind","services"])
t = Set(m,
name = "t",
domain = i,
description = "tradables",
records = ["light-ind","food+agr","heavy-ind"])
x = Variable(m,"x",domain = i, description = "quantity of output")
y = Variable(m,"y",domain = i, description = "final consumption")
e = Variable(m,"e",domain = i, description = "quantity of exports")
n = Variable(m,"n",domain = i, description = "quantity of imports")
mb = Equation(m, "mb", domain = i, description = "material balance")
mb[i] = x[i] >= y[i] + (e[i] - n[i]).where[t[i]]
```

Note that in the equation definition in the last line, the term `(e[i] - m[i])`

on the
right-hand side of the equation is added only for those elements of the set `i`

that also
belong to the subset `t[i]`

, so that the element services is excluded.

Further, conditional indexed operations may also feature in expressions in equation definitions.
In the following example, note that the set `i`

contains the supply regions, the set `j`

contains the demand regions, and the two-dimensional set `ij`

is the set of feasible links;
the variable `x`

denotes the shipment of natural gas and the variable `s`

denotes the
regional supply.

```
sb[i] = Sum(j.where[ij[i,j]), x[i,j]) <= s[i]
```

Similar to the assignment example seen before, the conditional set `ij[i,j]`

restricts the
domain of the summation: for each supply region `i`

the summation over the demand regions
`j`

is restricted to the label combinations `(i,j)`

that are elements of the set of
feasible links `ij[i,j]`

.

### Control over the Domain of Definition#

In case constraints should only be included in the model if particular conditions are met,
a `where`

condition in the domain of definition of an equation may be used to model this
restriction. Such a `where`

condition is analogous to the
where[] control on the left of assignments. Assuming that “the left”
means the left of the `'='`

then the analogy is even closer.

Note

The `where`

control over the domain of definition of equations restricts the number
of constraints generated to less than the number implied by the domain of the defining sets.

Consider the following example:

```
gple[w,wp,te].where[ple[w,wp]] = yw[w,te] - yw[wp,te] <= dpack
```

Here `w`

, `wp`

and `te`

are sets, `ple`

is a two-dimensional parameter, `yw`

is a
variable and `dpack`

is a scalar. Note that the `where`

condition restricts the first
two indices of the domain of the equation to those label combinations that have non-zero entries
in the two-dimensional parameter `ple`

.

Sometimes the desired restriction of an equation may be achieved either way: through a condition
in the algebra or a condition in the domain of definition. Compare the following two lines, where
`eq1`

and `eq2`

are equations, `i`

and `j`

are sets, `b`

is a `scalar`

, `s`

is a
parameter and `x`

is a two-dimensional variable.

```
eq1[i].where[b] = Sum(j, x[i,j]) >= -s[i]
eq2[i] = Sum(j, x[i,j]).where[b] >= -s[i].where[b]
```

In the first line the `where`

condition is in the domain of definition, in the second line
the `where`

conditions are in the algebraic formulation of the equation. If `b`

is non-zero,
the generated equations `eq1`

and `eq2`

will be identical. However, if `b`

is 0, no equation
`eq1`

will be generated, but for each `i`

we will see a trivial equation `eq2`

of the form
`0 >= 0`

.

Note that if the logical condition in the domain of definition of an equation refers to set
membership, then under certain conditions the restriction may be expressed without the use of
the `where`

operator. For details, see section
Filtering the Domain of Definition below.

## Filtering Sets#

If the logical condition refers to set membership, the restriction modeled with a `where`

condition may sometimes be achieved without the `where`

operator. Consider the following
statement, where `i`

and `j[i]`

are sets, and `u`

and `s`

are parameters:

```
u[i].where[j[i]] = s[i]
```

Note that the assignment is made only for those elements of the set `i`

that are also
elements of the subset `j`

. This conditional assignment may be rewritten in a shorter way:

```
u[j] = s[j]
```

In this statement the assignment has been filtered through the condition without the `where`

operator by using the subset `j`

as the domain for the parameters `u`

and `s`

. This
formulation is cleaner and easier to understand. It is particularly useful in the context of
multi-dimensional sets (tuples), and it may be used in
assignments,
indexed operations and the
domain of definition of equations.

### Filtering Sets in Assignments#

Suppose we want to compute the transportation cost between local collection sites and regional transportation hubs for a fictional parcel delivery service. We define sets for the collection sites and transportation hubs and a two-dimensional set where the collection sites are matched with their respective hubs:

```
from gamspy import Container, Set, Parameter, Variable, Equation, Sum
import pandas as pd
m = Container()
i = Set(m, "i", description = "local collection sites")
j = Set(m, "j", description = "regional transportation hubs")
c = pd.Series(
index=pd.MultiIndex.from_tuples([("boston", "newyork"),
("miami", "atlanta"),
("houston", "atlanta"),
("chicago", "detroit"),
("phoenix", "losangeles")])
)
r = Set(m, "r", domain = [i,j],
uels_on_axes=True,
domain_forwarding = True,
description = "regional transportation hub for each local collection site",
records = c)
dist = pd.DataFrame(
[("miami", "newyork", 1327),
("miami", "detroit", 1387),
("miami", "losangeles", 2737),
("miami", "atlanta", 665),
("boston", "newyork", 216),
("boston", "detroit", 699),
("boston", "losangeles", 3052),
("boston", "atlanta", 1068),
("chicago", "newyork", 843),
("chicago", "detroit", 275),
("chicago", "losangeles", 2095),
("chicago", "atlanta", 695),
("houston", "newyork", 1636),
("houston", "detroit", 1337),
("houston", "losangeles", 1553),
("houston", "atlanta", 814),
("phoenix", "newyork", 2459),
("phoenix", "detroit", 1977),
("phoenix", "losangeles", 398),
("phoenix", "atlanta", 1810)],
columns=["i", "j", "distance in miles"],
)
distance = Parameter(m, "distance", domain = [i,j],
description = "distance in miles",
records = dist)
shipcost = Parameter(m, "shipcost", domain = [i,j],
description = "cost of transporting parcels from a local collection site to a regional hub per unit")
factor = 0.009
shipcost[i,j].where[r[i,j]] = factor*distance[i,j]
```

```
In [1]: shipcost.records
Out[1]:
i j value
0 boston newyork 1.944
1 miami atlanta 5.985
2 houston atlanta 7.326
3 chicago detroit 2.475
4 phoenix losangeles 3.582
```

The distance between collection sites and transportation hubs is given in the parameter `distance`

.
The last line is a conditional assignment for the parameter `shipcost`

. This assignment is only
made if the label combination `(i,j)`

is an element of the set `r`

. Note that in each instance
the indices `i`

and `j`

appear together. Thus the assignment may be simply written as:

```
shipcost[r] = factor*distance[r]
```

Note that the assignment is explicitly restricted to the members of the set `r`

; the `where`

operator is not necessary. Observe that if the indices `i`

or `j`

appear separately in any
assignment, the above simplification cannot be made. For example, consider the case where the
shipping cost depends not only on the `factor`

and the `distance`

between collection sites
and regional hubs, but also on the congestion at the regional hub. We introduce a new parameter
`congestfac`

that models the congestion at each regional hub and is indexed only over the set
`j`

:

```
congestfac = Parameter(m, "congestfac", domain = j,
description = "congestion factor",
records = [["newyork", 1.5],
["detroit", 0.7],
["losangeles", 1.2],
["atlanta", 0.9]])
```

The new cost of shipment is computed as follows:

```
shipcost[i,j].where[r[i,j]] = factor * congestfac[j] * distance[i,j]
```

Note that this conditional assignment *cannot* be reformulated as:

```
shipcost[r] = factor * congestfac[j] * distance[r]
```

In the representation above the index `j`

appears on the right-hand side, but not on the left-hand
side. GAMSPy will flag this assignment as an error. However, the following representation will work:

```
shipcost[r[i,j]] = factor * congestfac[j] * distance[r]
```

In this formulation the set `r`

is explicitly denoted as a tuple of the sets `i`

and `j`

. The
set `j`

may then appear on the right-hand side.

### Filtering Controlling Indices in Indexed Operations#

Similarly, the controlling indices in indexed operations may be filtered through the conditional set
without the use of the `where`

operator. We continue with the shipping cost example from the last
subsection. The total cost of shipment is obtained through the equation that follows. We also include
the variable definitions for clarity.

```
shipped = Variable(m, "shipped", domain = [i,j])
totcost = Variable(m, "totcost")
costequ = Equation(m, "costequ")
costequ = totcost == Sum(Domain(i,j).where[r[i,j]], shipcost[i,j]*shipped[i,j])
```

Here the variable `shipped`

is the number of parcels shipped from the local collection site `i`

to
the regional transportation hub `j`

, and the variable `totcost`

is the total cost of all shipments.
Note that she summation in the equation is restricted to the label combinations that are elements of the
set `r`

. Alternatively, the equation above may be written as:

```
costequ = totcost == Sum(r, shipcost[r]*shipped[r])
```

In this formulation the summation is performed explicitly only over the elements of the set `r`

, no
`where`

condition is necessary. However, if the expression in the equation included a term dependent
only on index `j`

, then we would have to reformulate differently. Suppose the equation included also
the congestion factor `congestfac`

that is indexed only over `j`

:

```
costequ = totcost == Sum(Domain(i,j).where[r[i,j]], factor*congestfac[j]*distance[i,j]*shipped[i,j])
```

In this case the equation needs to be simplified in the following way:

```
costequ = totcost == Sum(r[i,j], factor*congestfac[j]*distance[r]*shipped[r])
```

Like before, the domain of the indexed operation `Sum`

is the set `r`

. But this time the domain of
`r`

has to be named explicitly, so that the parameter `congestfac`

which is indexed only over the
set `j`

is permitted in the scope of the indexed operation. Note that this reasoning is analogous
to the reasoning for filtering sets in assignments in the subsection above.

### Filtering the Domain of Definition#

The rules for filtering sets that we have introduced in subsections
Filtering Sets in Assignments and
Filtering Controlling Indices in Indexed Operations
also apply in the context of equation domains. We continue with the parcel transport example introduced
above and add a `binary variable`

`bin`

, the parameter `bigM`

and the
equation `connect`

to the model. Recall that `shipped[i,j]`

is a variable and `r[i,j]`

is a set.

```
bigM = Parameter(m, "bigM",domain = [i,j])
bin = Variable(m, "bin", domain = [i,j], type = "binary")
connect = Equation(m, "connect", domain = [i,j])
connect[i,j].where[r[i,j]] = shipped[i,j] <= bigM[i,j]*bin[i,j]
```

The `where`

condition restricts the domain of definition of the equation `connect`

to those label
combinations of the sets `i`

and `j`

that are elements of the set `r`

. The equation relates the
continuous variable `shipped[i,j]`

to the binary variable `bin[i,j]`

. Note that each domain in the
quation is the index pair `(i,j)`

. So the equation may be simplified as follows:

```
connect[r] = shipped[r] <= bigM[r]*bin[r]
```

In this formulation the domain of the equation is explicitly restricted to the members of the set `r`

,
without the use of a `where`

condition. Note that if the right-hand side of the equation contained
any term that was indexed over `i`

or `j`

separately, then the domain of definition of the equation
would have to be simplified as:

```
connect[r[i,j]]
```

The reasoning is the same as in the case of assignments and indexed operations.