Skip to content

Commit e9cfccd

Browse files
authored
Merge pull request #3664 from jsiirola/set-init
Support initialization of indexed Set from scalar functions returning dicts
2 parents 86e7fc6 + 4705fad commit e9cfccd

5 files changed

Lines changed: 123 additions & 29 deletions

File tree

doc/OnlineDocs/explanation/modeling/math_programming/sets.rst

Lines changed: 79 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,30 @@ and postpones creation of its members:
2929

3030
The :class:`Set` class takes optional arguments such as:
3131

32-
- ``dimen`` = Dimension of the members of the set
33-
- ``doc`` = String describing the set
34-
- ``filter`` = A Boolean function used during construction to indicate if a
35-
potential new member should be assigned to the set
36-
- ``initialize`` = An iterable containing the initial members of the Set, or
37-
function that returns an iterable of the initial members the set.
38-
- ``ordered`` = A Boolean indicator that the set is ordered; the default is ``True``
39-
- ``validate`` = A Boolean function that validates new member data
40-
- ``within`` = Set used for validation; it is a super-set of the set being declared.
32+
``dimen``
33+
Dimension of the members of the set; ``None`` for "jagged" sets
34+
(where members do not have a uniform length).
35+
36+
``doc``
37+
String describing the set
38+
39+
``filter``
40+
A Boolean function used during construction to indicate if a
41+
potential new member should be assigned to the set
42+
43+
``initialize``
44+
An iterable containing the initial members of the Set, or
45+
function that returns an iterable of the initial members the set.
46+
47+
``ordered``
48+
A Boolean indicator that the set is ordered; the default is ``True``
49+
(Set is ordered by insertion order)
50+
51+
``validate``
52+
A Boolean function that validates new member data
53+
54+
``within``
55+
Set used for validation; it is a super-set of the set being declared.
4156

4257
In general, Pyomo attempts to infer the "dimensionality" of Set
4358
components (that is, the number of apparent indices) when they are
@@ -451,8 +466,40 @@ for this model, a toy data file (in AMPL "``.dat``" format) would be:
451466

452467
>>> inst = model.create_instance('src/scripting/Isinglecomm.dat')
453468

454-
This can also be done somewhat more efficiently, and perhaps more clearly,
455-
using a :class:`BuildAction` (for more information, see :ref:`BuildAction`):
469+
A similar result can be accomplished more efficiently (because we only
470+
iterate over the Arcs twice) using initialization functions that accept
471+
only a model block and return a ``dict`` with all the information needed
472+
for the indexed set:
473+
474+
.. doctest::
475+
:hide:
476+
477+
>>> model = inst
478+
>>> del model.NodesIn
479+
>>> del model.NodesOut
480+
481+
.. testcode::
482+
483+
def NodesIn_init(m):
484+
# Create a dict to show NodesIn list for every node
485+
d = {i: [] for i in m.Nodes}
486+
# loop over the arcs and record the end points
487+
for i, j in model.Arcs:
488+
d[j].append(i)
489+
return d
490+
model.NodesIn = pyo.Set(model.Nodes, initialize=NodesIn_init)
491+
492+
def NodesOut_init(m):
493+
d = {i: [] for i in m.Nodes}
494+
for i, j in model.Arcs:
495+
d[i].append(j)
496+
return d
497+
model.NodesOut = pyo.Set(model.Nodes, initialize=NodesOut_init)
498+
499+
Alternatively, this can also be done even more efficiently, and perhaps
500+
more clearly, outside the context of Set initialization. For concrete
501+
models, scripts can explicitly add elements to the Sets after
502+
declaration:
456503

457504
.. doctest::
458505
:hide:
@@ -463,8 +510,29 @@ using a :class:`BuildAction` (for more information, see :ref:`BuildAction`):
463510

464511
.. testcode::
465512

513+
model.NodesIn = pyo.Set(model.Nodes, within=model.Nodes)
466514
model.NodesOut = pyo.Set(model.Nodes, within=model.Nodes)
515+
516+
# loop over the arcs and record the end points
517+
for i, j in model.Arcs:
518+
model.NodesIn[j].add(i)
519+
model.NodesOut[i].add(j)
520+
521+
For abstract models, that action must be deferred to instance
522+
construction time using a :class:`BuildAction` (for more information,
523+
see :ref:`BuildAction`):
524+
525+
.. doctest::
526+
:hide:
527+
528+
>>> model = inst
529+
>>> del model.NodesIn
530+
>>> del model.NodesOut
531+
532+
.. testcode::
533+
467534
model.NodesIn = pyo.Set(model.Nodes, within=model.Nodes)
535+
model.NodesOut = pyo.Set(model.Nodes, within=model.Nodes)
468536

469537
def Populate_In_and_Out(model):
470538
# loop over the arcs and record the end points

examples/doc/samples/case_studies/diet/DietProblem.tex

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ \subsection*{Build the model}
6262

6363
At this point we must start defining the rules associated with our parameters and variables. We begin with the most important rule, the cost rule, which will tell the model to try and minimize the overall cost. Logically, the total cost is going to be the sum of how much is spent on each food, and that value in turn is going to be determined by the cost of the food and how much of it is purchased. For example, if three \$5 hamburgers and two \$1 apples are purchased, than the total cost would be $3 \cdot 5 + 2 \cdot 1 = 17$. Note that this process is the same as taking the dot product of the amounts vector and the costs vector.
6464

65-
To input this, we must define the cost rule, which we creatively call costRule as
65+
To input this, we must define the cost rule, which we creatively call costRule as
6666

6767
\begin{verbatim}def costRule(model):
6868
return sum(model.costs[n]*model.amount[n] for n in model.foods)
@@ -75,10 +75,10 @@ \subsection*{Build the model}
7575

7676
This line defines the objective of the model as the costRule, which Pyomo interprets as the value it needs to minimize; in this case it will minimize our costs. Also, as a note, we defined the objective as ``model.cost'' which is not to be confused with the parameter we defined earlier as ``model.costs,'' despite their similar names. These are two different values and accidentally giving them the same name will cause problems when trying to solve the problem.
7777

78-
We must also create a rule for the volume consumed. The construction of this rule is similar to the cost rule as once again we take the dot product, this time between the volume and amount vectors.
78+
We must also create a rule for the volume consumed. The construction of this rule is similar to the cost rule as once again we take the dot product, this time between the volume and amount vectors.
7979

8080
\begin{verbatim}def volumeRule(model):
81-
return sum(model.volumes[n]*model.amount[n] for n in
81+
return sum(model.volumes[n]*model.amount[n] for n in
8282
model.foods) <= model.max_volume
8383
8484
model.volume = pyo.Constraint(rule=volumeRule)
@@ -90,7 +90,7 @@ \subsection*{Build the model}
9090

9191
\begin{verbatim}
9292
def nutrientRule(n, model):
93-
value = sum(model.nutrient_value[n,f]*model.amount[f]
93+
value = sum(model.nutrient_value[n,f]*model.amount[f]
9494
for f in model.foods)
9595
return (model.min_nutrient[n], value, model.max_nutrient[n])
9696
@@ -160,7 +160,7 @@ \subsection*{Data entry}
160160

161161
The amount of spaces between each element is irrelevant (as long as there is at least one) so the matrix should be formatted for ease of reading.
162162

163-
Now that we have finished both the model and the data file save them both. It's convention to give the model file a .py extension and the data file a .dat extension.
163+
Now that we have finished both the model and the data file save them both. It's convention to give the model file a .py extension and the data file a .dat extension.
164164

165165
\subsection*{Solution}
166166

examples/doc/samples/case_studies/diet/README.txt

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ to import the Pyomo package for use in the code. The next step is to create an
1414

1515
{{{
1616
#!python
17-
model = pyo.AbstractModel()
17+
model = pyo.AbstractModel()
1818
}}}
1919
The rest of our work will be contained within this object.
2020

@@ -79,7 +79,7 @@ We restrict our domain to the non-negative reals. If we accepted negative numbe
7979

8080
At this point we must start defining the rules associated with our parameters and variables. We begin with the most important rule, the cost rule, which will tell the model to try and minimize the overall cost. Logically, the total cost is going to be the sum of how much is spent on each food, and that value in turn is going to be determined by the cost of the food and how much of it is purchased. For example, if three !$5 hamburgers and two !$1 apples are purchased, than the total cost would be 3*5 + 2*1 = 17. Note that this process is the same as taking the dot product of the amounts vector and the costs vector.
8181

82-
To input this, we must define the cost rule, which we creatively call costRule as
82+
To input this, we must define the cost rule, which we creatively call costRule as
8383

8484
{{{
8585
#!python
@@ -95,7 +95,7 @@ model.cost=pyo.Objective(rule=costRule
9595

9696
This line defines the objective of the model as the costRule, which Pyomo interprets as the value it needs to minimize; in this case it will minimize our costs. Also, as a note, we defined the objective as "model.cost" which is not to be confused with the parameter we defined earlier as `"model.costs" despite their similar names. These are two different values and accidentally giving them the same name will cause problems when trying to solve the problem.
9797

98-
We must also create a rule for the volume consumed. The construction of this rule is similar to the cost rule as once again we take the dot product, this time between the volume and amount vectors.
98+
We must also create a rule for the volume consumed. The construction of this rule is similar to the cost rule as once again we take the dot product, this time between the volume and amount vectors.
9999

100100
{{{
101101
#!python
@@ -112,7 +112,7 @@ Finally, we need to add the constraint that ensures we obtain proper amounts of
112112
{{{
113113
#!python
114114
def nutrientRule(n, model):
115-
value = sum(model.nutrient_value[n,f]*model.amount[f]
115+
value = sum(model.nutrient_value[n,f]*model.amount[f]
116116
for f in model.foods)
117117
return (model.min_nutrient[n], value, model.max_nutrient[n])
118118

@@ -179,7 +179,7 @@ vc 0 30 0;
179179

180180
The amount of spaces between each element is irrelevant (as long as there is at least one) so the matrix should be formatted for ease of reading.
181181

182-
Now that we have finished both the model and the data file save them both. It's convention to give the model file a .py extension and the data file a .dat extension.
182+
Now that we have finished both the model and the data file save them both. It's convention to give the model file a .py extension and the data file a .dat extension.
183183

184184
== Solution ==
185185

@@ -193,7 +193,7 @@ Using Pyomo we quickly find the solution to our diet problem. Simply run Pyomo
193193
# ----------------------------------------------------------
194194
# Problem Information
195195
# ----------------------------------------------------------
196-
Problem:
196+
Problem:
197197
- Lower bound: 29.44055944
198198
Upper bound: inf
199199
Number of objectives: 1
@@ -205,28 +205,28 @@ Problem:
205205
# ----------------------------------------------------------
206206
# Solver Information
207207
# ----------------------------------------------------------
208-
Solver:
208+
Solver:
209209
- Status: ok
210210
Termination condition: unknown
211211
Error rc: 0
212212

213213
# ----------------------------------------------------------
214214
# Solution Information
215215
# ----------------------------------------------------------
216-
Solution:
216+
Solution:
217217
- number of solutions: 1
218218
number of solutions displayed: 1
219219
- Gap: 0.0
220220
Status: optimal
221-
Objective:
222-
f:
221+
Objective:
222+
f:
223223
Id: 0
224224
Value: 29.44055944
225-
Variable:
226-
amount[rice]:
225+
Variable:
226+
amount[rice]:
227227
Id: 0
228228
Value: 9.44056
229-
amount[apple]:
229+
amount[apple]:
230230
Id: 2
231231
Value: 10
232232

pyomo/core/base/set.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
ParameterizedIndexedCallInitializer,
4848
ParameterizedInitializer,
4949
ParameterizedScalarCallInitializer,
50+
ScalarCallInitializer,
5051
)
5152
from pyomo.core.base.range import (
5253
NumericRange,
@@ -2305,6 +2306,19 @@ def construct(self, data=None):
23052306
for _set in self._anonymous_sets:
23062307
_set.construct()
23072308

2309+
if self.is_indexed():
2310+
# A constant rule could return a dict-like thing or matrix
2311+
# that we would then want to process with Initializer(). If
2312+
# the rule actually returned a constant, then this is just a
2313+
# little overhead.
2314+
if self._init_values is not None and self._init_values._init.constant():
2315+
self._init_values = TuplizeValuesInitializer(
2316+
Initializer(
2317+
self._init_values._init(self.parent_block(), None),
2318+
treat_sequences_as_mappings=False,
2319+
)
2320+
)
2321+
23082322
if data is not None:
23092323
# Data supplied to construct() should override data provided
23102324
# to the constructor

pyomo/core/tests/unit/test_sets.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3339,6 +3339,18 @@ def test_setargs5(self):
33393339
model.Y = RangeSet(model.C)
33403340
model.X = Param(model.C, default=0.0)
33413341

3342+
def test_setargs6(self):
3343+
# Test that we can create an indexed set from a function that returns
3344+
# a dict to define the set
3345+
model = ConcreteModel()
3346+
model.A = Set(initialize=[1, 2])
3347+
model.B = Set(model.A, initialize={1: [2, 3], 2: [3, 4]})
3348+
model.C = Set(model.A, initialize=lambda m: {x: [x + 1, x + 2] for x in m.A})
3349+
# convert to native data types for easier comparison
3350+
B = {k: v.ordered_data() for (k, v) in model.B.items()}
3351+
C = {k: v.ordered_data() for (k, v) in model.C.items()}
3352+
self.assertEqual(B, C)
3353+
33423354
@unittest.skip("_verify was removed during the set rewrite")
33433355
def test_verify(self):
33443356
a = Set(initialize=[1, 2, 3])

0 commit comments

Comments
 (0)