Sets
Declaration
Sets can be declared using instances of the Set
and RangeSet
classes or by
assigning set expressions. The simplest set declaration creates a set
and postpones creation of its members:
model.A = pyo.Set()
The Set
class takes optional arguments such as:
dimen
= Dimension of the members of the setdoc
= String describing the setfilter
= A Boolean function used during construction to indicate if a potential new member should be assigned to the setinitialize
= An iterable containing the initial members of the Set, or function that returns an iterable of the initial members the set.ordered
= A Boolean indicator that the set is ordered; the default isTrue
validate
= A Boolean function that validates new member datawithin
= Set used for validation; it is a super-set of the set being declared.
In general, Pyomo attempts to infer the “dimensionality” of Set
components (that is, the number of apparent indices) when they are
constructed. However, there are situations where Pyomo either cannot
detect a dimensionality (e.g., a Set
that was not initialized with any
members), or you the user may want to assert the dimensionality of the
set. This can be accomplished through the dimen
keyword. For
example, to create a set whose members will be tuples with two items, one
could write:
model.B = pyo.Set(dimen=2)
To create a set of all the numbers in set model.A
doubled, one could
use
def DoubleA_init(model):
return (i*2 for i in model.A)
model.C = pyo.Set(initialize=DoubleA_init)
As an aside we note that as always in Python, there are lot of ways to
accomplish the same thing. Also, note that this will generate an error
if model.A
contains elements for which multiplication times two is
not defined.
The initialize
option can accept any Python iterable, including a
set
, list
, or tuple
. This data may be returned from a
function or specified directly as in
model.D = pyo.Set(initialize=['red', 'green', 'blue'])
The initialize
option can also specify either a generator or a
function to specify the Set members. In the case of a generator, all
data yielded by the generator will become the initial set members:
def X_init(m):
for i in range(10):
yield 2*i+1
model.X = pyo.Set(initialize=X_init)
For initialization functions, Pyomo supports two signatures. In the
first, the function returns an iterable (set
, list
, or
tuple
) containing the data with which to initialize the Set:
def Y_init(m):
return [2*i+1 for i in range(10)]
model.Y = pyo.Set(initialize=Y_init)
In the second signature, the function is called for each element,
passing the element number in as an extra argument. This is repeated
until the function returns the special value Set.End
:
def Z_init(model, i):
if i > 10:
return pyo.Set.End
return 2*i+1
model.Z = pyo.Set(initialize=Z_init)
Note that the element number starts with 1 and not 0:
>>> model.X.pprint()
X : Size=1, Index=None, Ordered=Insertion
Key : Dimen : Domain : Size : Members
None : 1 : Any : 10 : {1, 3, 5, 7, 9, 11, 13, 15, 17, 19}
>>> model.Y.pprint()
Y : Size=1, Index=None, Ordered=Insertion
Key : Dimen : Domain : Size : Members
None : 1 : Any : 10 : {1, 3, 5, 7, 9, 11, 13, 15, 17, 19}
>>> model.Z.pprint()
Z : Size=1, Index=None, Ordered=Insertion
Key : Dimen : Domain : Size : Members
None : 1 : Any : 10 : {3, 5, 7, 9, 11, 13, 15, 17, 19, 21}
Additional information about iterators for set initialization is in the [PyomoBookIII] book.
Note
For Abstract models, data specified in an input file or through the
data
argument to AbstractModel.create_instance()
will
override the data
specified by the initialize options.
If sets are given as arguments to Set
without keywords, they are
interpreted as indexes for an array of sets. For example, to create an
array of sets that is indexed by the members of the set model.A
, use:
model.E = pyo.Set(model.A)
Arguments can be combined. For example, to create an array of sets,
indexed by set model.A
where each set contains three dimensional
members, use:
model.F = pyo.Set(model.A, dimen=3)
The initialize
option can be used to create a set that contains a
sequence of numbers, but the RangeSet
class provides a concise
mechanism for simple sequences. This class takes as its arguments a
start value, a final value, and a step size. If the RangeSet
has
only a single argument, then that value defines the final value in the
sequence; the first value and step size default to one. If two values
given, they are the first and last value in the sequence and the step
size defaults to one. For example, the following declaration creates a
set with the numbers 1.5, 5 and 8.5:
model.G = pyo.RangeSet(1.5, 10, 3.5)
Operations
Sets may also be created by storing the result of set operations using other Pyomo sets. Pyomo supports set operations including union, intersection, difference, and symmetric difference:
model.I = model.A | model.D # union
model.J = model.A & model.D # intersection
model.K = model.A - model.D # difference
model.L = model.A ^ model.D # exclusive-or
For example, the cross-product operator is the asterisk (*). To define
a new set M
that is the cross product of sets B
and C
, one
could use
model.M = model.B * model.C
This creates a virtual set that holds references to the original sets,
so any updates to the original sets (B
and C
) will be reflected
in the new set (M
). In contrast, you can also create a concrete
set, which directly stores the values of the cross product at the time
of creation and will not reflect subsequent changes in the original
sets with:
model.M_concrete = pyo.Set(initialize=model.B * model.C)
Finally, you can indicate that the members of a set are restricted to be in the
cross product of two other sets, one can use the within
keyword:
model.N = pyo.Set(within=model.B * model.C)
Predefined Virtual Sets
For use in specifying domains for sets, parameters and variables, Pyomo provides the following pre-defined virtual sets:
Any
= all possible valuesReals
= floating point valuesPositiveReals
= strictly positive floating point valuesNonPositiveReals
= non-positive floating point valuesNegativeReals
= strictly negative floating point valuesNonNegativeReals
= non-negative floating point valuesPercentFraction
= floating point values in the interval [0,1]UnitInterval
= alias for PercentFractionIntegers
= integer valuesPositiveIntegers
= positive integer valuesNonPositiveIntegers
= non-positive integer valuesNegativeIntegers
= negative integer valuesNonNegativeIntegers
= non-negative integer valuesBoolean
= Boolean values, which can be represented as False/True, 0/1, ’False’/’True’ and ’F’/’T’Binary
= the integers {0, 1}
For example, if the set model.O
is declared to be within the virtual
set NegativeIntegers
then an attempt to add anything other than a
negative integer will result in an error. Here is the declaration:
model.O = pyo.Set(within=pyo.NegativeIntegers)
Sparse Index Sets
Sets provide indexes for parameters, variables and other sets. Index set issues are important for modelers in part because of efficiency considerations, but primarily because the right choice of index sets can result in very natural formulations that are conducive to understanding and maintenance. Pyomo leverages Python to provide a rich collection of options for index set creation and use.
The choice of how to represent indexes often depends on the application and the nature of the instance data that are expected. To illustrate some of the options and issues, we will consider problems involving networks. In many network applications, it is useful to declare a set of nodes, such as
model.Nodes = pyo.Set()
and then a set of arcs can be created with reference to the nodes.
Consider the following simple version of minimum cost flow problem:
where
Set: Nodes \(\equiv \mathcal{N}\)
Set: Arcs \(\equiv \mathcal{A} \subseteq \mathcal{N} \times \mathcal{N}\)
Var: Flow on arc \((i,j)\) \(\equiv x_{i,j},\; (i,j) \in \mathcal{A}\)
Param: Flow Cost on arc \((i,j)\) \(\equiv c_{i,j},\; (i,j) \in \mathcal{A}\)
Param: Demand at node latexmath:i \(\equiv D_{i},\; i \in \mathcal{N}\)
Param: Supply at node latexmath:i \(\equiv S_{i},\; i \in \mathcal{N}\)
In the simplest case, the arcs can just be the cross product of the nodes, which is accomplished by the definition
model.Arcs = model.Nodes*model.Nodes
that creates a set with two dimensional members. For applications where all nodes are always connected to all other nodes this may suffice. However, issues can arise when the network is not fully dense. For example, the burden of avoiding flow on arcs that do not exist falls on the data file where high-enough costs must be provided for those arcs. Such a scheme is not very elegant or robust.
For many network flow applications, it might be better to declare the arcs using
model.Arcs = pyo.Set(dimen=2)
or
model.Arcs = pyo.Set(within=model.Nodes*model.Nodes)
where the difference is that the first version will provide error
checking as data is assigned to the set elements. This would enable
specification of a sparse network in a natural way. But this results in
a need to change the FlowBalance
constraint because as it was
written in the simple example, it sums over the entire set of nodes for
each node. One way to remedy this is to sum only over the members of the
set model.arcs
as in
def FlowBalance_rule(m, node):
return m.Supply[node] \
+ sum(m.Flow[i, node] for i in m.Nodes if (i,node) in m.Arcs) \
- m.Demand[node] \
- sum(m.Flow[node, j] for j in m.Nodes if (j,node) in m.Arcs) \
== 0
This will be OK unless the number of nodes becomes very large for a sparse network, then the time to generate this constraint might become an issue (admittely, only for very large networks, but such networks do exist).
Another method, which comes in handy in many network applications, is to
have a set for each node that contain the nodes at the other end of arcs
going to the node at hand and another set giving the nodes on out-going
arcs. If these sets are called model.NodesIn
and model.NodesOut
respectively, then the flow balance rule can be re-written as
def FlowBalance_rule(m, node):
return m.Supply[node] \
+ sum(m.Flow[i, node] for i in m.NodesIn[node]) \
- m.Demand[node] \
- sum(m.Flow[node, j] for j in m.NodesOut[node]) \
== 0
The data for NodesIn
and NodesOut
could be added to the input
file, and this may be the most efficient option.
For all but the largest networks, rather than reading Arcs
,
NodesIn
and NodesOut
from a data file, it might be more elegant
to read only Arcs
from a data file and declare model.NodesIn
with an initialize
option specifying the creation as follows:
def NodesIn_init(m, node):
for i, j in m.Arcs:
if j == node:
yield i
model.NodesIn = pyo.Set(model.Nodes, initialize=NodesIn_init)
with a similar definition for model.NodesOut
. This code creates a
list of sets for NodesIn
, one set of nodes for each node. The full
model is:
import pyomo.environ as pyo
model = pyo.AbstractModel()
model.Nodes = pyo.Set()
model.Arcs = pyo.Set(dimen=2)
def NodesOut_init(m, node):
for i, j in m.Arcs:
if i == node:
yield j
model.NodesOut = pyo.Set(model.Nodes, initialize=NodesOut_init)
def NodesIn_init(m, node):
for i, j in m.Arcs:
if j == node:
yield i
model.NodesIn = pyo.Set(model.Nodes, initialize=NodesIn_init)
model.Flow = pyo.Var(model.Arcs, domain=pyo.NonNegativeReals)
model.FlowCost = pyo.Param(model.Arcs)
model.Demand = pyo.Param(model.Nodes)
model.Supply = pyo.Param(model.Nodes)
def Obj_rule(m):
return pyo.summation(m.FlowCost, m.Flow)
model.Obj = pyo.Objective(rule=Obj_rule, sense=pyo.minimize)
def FlowBalance_rule(m, node):
return m.Supply[node] \
+ sum(m.Flow[i, node] for i in m.NodesIn[node]) \
- m.Demand[node] \
- sum(m.Flow[node, j] for j in m.NodesOut[node]) \
== 0
model.FlowBalance = pyo.Constraint(model.Nodes, rule=FlowBalance_rule)
for this model, a toy data file (in AMPL “.dat
” format) would be:
set Nodes := CityA CityB CityC ;
set Arcs :=
CityA CityB
CityA CityC
CityC CityB
;
param : FlowCost :=
CityA CityB 1.4
CityA CityC 2.7
CityC CityB 1.6
;
param Demand :=
CityA 0
CityB 1
CityC 1
;
param Supply :=
CityA 2
CityB 0
CityC 0
;
This can also be done somewhat more efficiently, and perhaps more clearly,
using a BuildAction
(for more information, see BuildAction and BuildCheck):
model.NodesOut = pyo.Set(model.Nodes, within=model.Nodes)
model.NodesIn = pyo.Set(model.Nodes, within=model.Nodes)
def Populate_In_and_Out(model):
# loop over the arcs and record the end points
for i, j in model.Arcs:
model.NodesIn[j].add(i)
model.NodesOut[i].add(j)
model.In_n_Out = pyo.BuildAction(rule=Populate_In_and_Out)
Sparse Index Sets Example
One may want to have a constraint that holds
There are many ways to accomplish this, but one good way is to create a
set of tuples composed of all model.k, model.V[k]
pairs. This
can be done as follows:
def kv_init(m):
return ((k,v) for k in m.K for v in m.V[k])
model.KV = pyo.Set(dimen=2, initialize=kv_init)
We can now create the constraint \(x_{i,k,v} \leq a_{i,k}y_i \;\forall\; i \in I, k \in K, v \in V_k\) with:
model.a = pyo.Param(model.I, model.K, default=1)
model.y = pyo.Var(model.I)
model.x = pyo.Var(model.I, model.KV)
def c1_rule(m, i, k, v):
return m.x[i,k,v] <= m.a[i,k]*m.y[i]
model.c1 = pyo.Constraint(model.I, model.KV, rule=c1_rule)