Skip to content

Commit 5206eee

Browse files
authored
Merge pull request Pyomo#3348 from jsiirola/suffix-finder-context
Add `context` option to `SuffixFinder`
2 parents 123c465 + c810bc3 commit 5206eee

File tree

5 files changed

+111
-42
lines changed

5 files changed

+111
-42
lines changed

pyomo/core/base/suffix.py

+29-3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from pyomo.common.modeling import NOTSET
2020
from pyomo.common.pyomo_typing import overload
2121
from pyomo.common.timing import ConstructionTimer
22+
from pyomo.core.base.block import BlockData
2223
from pyomo.core.base.component import ActiveComponent, ModelComponentFactory
2324
from pyomo.core.base.disable_methods import disable_methods
2425
from pyomo.core.base.initializer import Initializer
@@ -409,7 +410,7 @@ class AbstractSuffix(Suffix):
409410

410411

411412
class SuffixFinder(object):
412-
def __init__(self, name, default=None):
413+
def __init__(self, name, default=None, context=None):
413414
"""This provides an efficient utility for finding suffix values on a
414415
(hierarchical) Pyomo model.
415416
@@ -424,11 +425,26 @@ def __init__(self, name, default=None):
424425
Default value to return from `.find()` if no matching Suffix
425426
is found.
426427
428+
context: BlockData
429+
430+
The root of the Block hierarchy to use when searching for
431+
Suffix components. Suffixes outside this hierarchy will not
432+
be interrogated and components that are queried (with
433+
:py:meth:`find(component_data)` will return the default
434+
value.
435+
427436
"""
428437
self.name = name
429438
self.default = default
430439
self.all_suffixes = []
431-
self._suffixes_by_block = {None: []}
440+
self._context = context
441+
self._suffixes_by_block = ComponentMap()
442+
self._suffixes_by_block[self._context] = []
443+
if context is not None:
444+
s = context.component(name)
445+
if s is not None and s.ctype is Suffix and s.active:
446+
self._suffixes_by_block[context].append(s)
447+
self.all_suffixes.append(s)
432448

433449
def find(self, component_data):
434450
"""Find suffix value for a given component data object in model tree
@@ -458,7 +474,17 @@ def find(self, component_data):
458474
459475
"""
460476
# Walk parent tree and search for suffixes
461-
suffixes = self._get_suffix_list(component_data.parent_block())
477+
if isinstance(component_data, BlockData):
478+
_block = component_data
479+
else:
480+
_block = component_data.parent_block()
481+
try:
482+
suffixes = self._get_suffix_list(_block)
483+
except AttributeError:
484+
# Component was outside the context (eventually parent
485+
# becomes None and parent.parent_block() raises an
486+
# AttributeError): we will return the default value
487+
return self.default
462488
# Pass 1: look for the component_data, working root to leaf
463489
for s in suffixes:
464490
if component_data in s:

pyomo/core/plugins/transform/scaling.py

+1-6
Original file line numberDiff line numberDiff line change
@@ -82,15 +82,10 @@ def _create_using(self, original_model, **kwds):
8282
self._apply_to(scaled_model, **kwds)
8383
return scaled_model
8484

85-
def _get_float_scaling_factor(self, component):
86-
if self._suffix_finder is None:
87-
self._suffix_finder = SuffixFinder('scaling_factor', 1.0)
88-
return self._suffix_finder.find(component)
89-
9085
def _apply_to(self, model, rename=True):
9186
# create a map of component to scaling factor
9287
component_scaling_factor_map = ComponentMap()
93-
self._suffix_finder = SuffixFinder('scaling_factor', 1.0)
88+
self._suffix_finder = SuffixFinder('scaling_factor', 1.0, model)
9489

9590
# if the scaling_method is 'user', get the scaling parameters from the suffixes
9691
if self._scaling_method == 'user':

pyomo/core/tests/transform/test_scaling.py

+37-19
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import pyomo.common.unittest as unittest
1414
import pyomo.environ as pyo
1515
from pyomo.opt.base.solvers import UnknownSolver
16-
from pyomo.core.plugins.transform.scaling import ScaleModel
16+
from pyomo.core.plugins.transform.scaling import ScaleModel, SuffixFinder
1717

1818

1919
class TestScaleModelTransformation(unittest.TestCase):
@@ -600,6 +600,13 @@ def con_rule(m, i):
600600
self.assertAlmostEqual(pyo.value(model.zcon), -8, 4)
601601

602602
def test_get_float_scaling_factor_top_level(self):
603+
# Note: the transformation used to have a private method for
604+
# finding suffix values (which this method tested). The
605+
# transformation now leverages the SuffixFinder. To ensure that
606+
# the SuffixFinder behaves in the same way as the original local
607+
# method, we preserve these tests, but directly test the
608+
# SuffixFinder
609+
603610
m = pyo.ConcreteModel()
604611
m.scaling_factor = pyo.Suffix(direction=pyo.Suffix.EXPORT)
605612

@@ -616,17 +623,23 @@ def test_get_float_scaling_factor_top_level(self):
616623
m.scaling_factor[m.v1] = 0.1
617624
m.scaling_factor[m.b1.v2] = 0.2
618625

626+
_finder = SuffixFinder('scaling_factor', 1.0, m)
627+
619628
# SF should be 0.1 from top level
620-
sf = ScaleModel()._get_float_scaling_factor(m.v1)
621-
assert sf == float(0.1)
629+
self.assertEqual(_finder.find(m.v1), 0.1)
622630
# SF should be 0.1 from top level, lower level ignored
623-
sf = ScaleModel()._get_float_scaling_factor(m.b1.v2)
624-
assert sf == float(0.2)
631+
self.assertEqual(_finder.find(m.b1.v2), 0.2)
625632
# No SF, should return 1
626-
sf = ScaleModel()._get_float_scaling_factor(m.b1.b2.v3)
627-
assert sf == 1.0
633+
self.assertEqual(_finder.find(m.b1.b2.v3), 1.0)
628634

629635
def test_get_float_scaling_factor_local_level(self):
636+
# Note: the transformation used to have a private method for
637+
# finding suffix values (which this method tested). The
638+
# transformation now leverages the SuffixFinder. To ensure that
639+
# the SuffixFinder behaves in the same way as the original local
640+
# method, we preserve these tests, but directly test the
641+
# SuffixFinder
642+
630643
m = pyo.ConcreteModel()
631644
m.scaling_factor = pyo.Suffix(direction=pyo.Suffix.EXPORT)
632645

@@ -647,15 +660,21 @@ def test_get_float_scaling_factor_local_level(self):
647660
# Add an intermediate scaling factor - this should take priority
648661
m.b1.scaling_factor[m.b1.b2.v3] = 0.4
649662

663+
_finder = SuffixFinder('scaling_factor', 1.0, m)
664+
650665
# Should get SF from local levels
651-
sf = ScaleModel()._get_float_scaling_factor(m.v1)
652-
assert sf == float(0.1)
653-
sf = ScaleModel()._get_float_scaling_factor(m.b1.v2)
654-
assert sf == float(0.2)
655-
sf = ScaleModel()._get_float_scaling_factor(m.b1.b2.v3)
656-
assert sf == float(0.4)
666+
self.assertEqual(_finder.find(m.v1), 0.1)
667+
self.assertEqual(_finder.find(m.b1.v2), 0.2)
668+
self.assertEqual(_finder.find(m.b1.b2.v3), 0.4)
657669

658670
def test_get_float_scaling_factor_intermediate_level(self):
671+
# Note: the transformation used to have a private method for
672+
# finding suffix values (which this method tested). The
673+
# transformation now leverages the SuffixFinder. To ensure that
674+
# the SuffixFinder behaves in the same way as the original local
675+
# method, we preserve these tests, but directly test the
676+
# SuffixFinder
677+
659678
m = pyo.ConcreteModel()
660679
m.scaling_factor = pyo.Suffix(direction=pyo.Suffix.EXPORT)
661680

@@ -680,15 +699,14 @@ def test_get_float_scaling_factor_intermediate_level(self):
680699

681700
m.b1.b2.b3.scaling_factor[m.b1.b2.b3.v3] = 0.4
682701

702+
_finder = SuffixFinder('scaling_factor', 1.0, m)
703+
683704
# v1 should be unscaled as SF set below variable level
684-
sf = ScaleModel()._get_float_scaling_factor(m.v1)
685-
assert sf == 1.0
705+
self.assertEqual(_finder.find(m.v1), 1.0)
686706
# v2 should get SF from b1 level
687-
sf = ScaleModel()._get_float_scaling_factor(m.b1.b2.b3.v2)
688-
assert sf == float(0.2)
707+
self.assertEqual(_finder.find(m.b1.b2.b3.v2), 0.2)
689708
# v2 should get SF from highest level, ignoring b3 level
690-
sf = ScaleModel()._get_float_scaling_factor(m.b1.b2.b3.v3)
691-
assert sf == float(0.3)
709+
self.assertEqual(_finder.find(m.b1.b2.b3.v3), 0.3)
692710

693711

694712
if __name__ == "__main__":

pyomo/core/tests/unit/test_suffix.py

+41-11
Original file line numberDiff line numberDiff line change
@@ -1795,47 +1795,77 @@ def test_suffix_finder(self):
17951795
m.b1.b2 = Block()
17961796
m.b1.b2.v3 = Var([0])
17971797

1798-
_suffix_finder = SuffixFinder('suffix')
1799-
18001798
# Add Suffixes
18011799
m.suffix = Suffix(direction=Suffix.EXPORT)
18021800
# No suffix on b1 - make sure we can handle missing suffixes
18031801
m.b1.b2.suffix = Suffix(direction=Suffix.EXPORT)
18041802

1803+
_suffix_finder = SuffixFinder('suffix')
1804+
_suffix_b1_finder = SuffixFinder('suffix', context=m.b1)
1805+
_suffix_b2_finder = SuffixFinder('suffix', context=m.b1.b2)
1806+
18051807
# Check for no suffix value
1806-
assert _suffix_finder.find(m.b1.b2.v3[0]) == None
1808+
self.assertEqual(_suffix_finder.find(m.b1.b2.v3[0]), None)
1809+
self.assertEqual(_suffix_b1_finder.find(m.b1.b2.v3[0]), None)
1810+
self.assertEqual(_suffix_b2_finder.find(m.b1.b2.v3[0]), None)
18071811

18081812
# Check finding default values
18091813
# Add a default at the top level
18101814
m.suffix[None] = 1
1811-
assert _suffix_finder.find(m.b1.b2.v3[0]) == 1
1815+
self.assertEqual(_suffix_finder.find(m.b1.b2.v3[0]), 1)
1816+
self.assertEqual(_suffix_b1_finder.find(m.b1.b2.v3[0]), None)
1817+
self.assertEqual(_suffix_b2_finder.find(m.b1.b2.v3[0]), None)
18121818

18131819
# Add a default suffix at a lower level
18141820
m.b1.b2.suffix[None] = 2
1815-
assert _suffix_finder.find(m.b1.b2.v3[0]) == 2
1821+
self.assertEqual(_suffix_finder.find(m.b1.b2.v3[0]), 2)
1822+
self.assertEqual(_suffix_b1_finder.find(m.b1.b2.v3[0]), 2)
1823+
self.assertEqual(_suffix_b2_finder.find(m.b1.b2.v3[0]), 2)
18161824

18171825
# Check for container at lowest level
18181826
m.b1.b2.suffix[m.b1.b2.v3] = 3
1819-
assert _suffix_finder.find(m.b1.b2.v3[0]) == 3
1827+
self.assertEqual(_suffix_finder.find(m.b1.b2.v3[0]), 3)
1828+
self.assertEqual(_suffix_b1_finder.find(m.b1.b2.v3[0]), 3)
1829+
self.assertEqual(_suffix_b2_finder.find(m.b1.b2.v3[0]), 3)
18201830

18211831
# Check for container at top level
18221832
m.suffix[m.b1.b2.v3] = 4
1823-
assert _suffix_finder.find(m.b1.b2.v3[0]) == 4
1833+
self.assertEqual(_suffix_finder.find(m.b1.b2.v3[0]), 4)
1834+
self.assertEqual(_suffix_b1_finder.find(m.b1.b2.v3[0]), 3)
1835+
self.assertEqual(_suffix_b2_finder.find(m.b1.b2.v3[0]), 3)
18241836

18251837
# Check for specific values at lowest level
18261838
m.b1.b2.suffix[m.b1.b2.v3[0]] = 5
1827-
assert _suffix_finder.find(m.b1.b2.v3[0]) == 5
1839+
self.assertEqual(_suffix_finder.find(m.b1.b2.v3[0]), 5)
1840+
self.assertEqual(_suffix_b1_finder.find(m.b1.b2.v3[0]), 5)
1841+
self.assertEqual(_suffix_b2_finder.find(m.b1.b2.v3[0]), 5)
18281842

18291843
# Check for specific values at top level
18301844
m.suffix[m.b1.b2.v3[0]] = 6
1831-
assert _suffix_finder.find(m.b1.b2.v3[0]) == 6
1845+
self.assertEqual(_suffix_finder.find(m.b1.b2.v3[0]), 6)
1846+
self.assertEqual(_suffix_b1_finder.find(m.b1.b2.v3[0]), 5)
1847+
self.assertEqual(_suffix_b2_finder.find(m.b1.b2.v3[0]), 5)
18321848

18331849
# Make sure we don't find default suffixes at lower levels
1834-
assert _suffix_finder.find(m.b1.v2) == 1
1850+
self.assertEqual(_suffix_finder.find(m.b1.v2), 1)
1851+
self.assertEqual(_suffix_b1_finder.find(m.b1.v2), None)
1852+
self.assertEqual(_suffix_b2_finder.find(m.b1.v2), None)
18351853

18361854
# Make sure we don't find specific suffixes at lower levels
18371855
m.b1.b2.suffix[m.v1] = 5
1838-
assert _suffix_finder.find(m.v1) == 1
1856+
self.assertEqual(_suffix_finder.find(m.v1), 1)
1857+
self.assertEqual(_suffix_b1_finder.find(m.v1), None)
1858+
self.assertEqual(_suffix_b2_finder.find(m.v1), None)
1859+
1860+
# Make sure we can look up Blocks and that they will match
1861+
# suffixes that they hold
1862+
self.assertEqual(_suffix_finder.find(m.b1.b2), 2)
1863+
self.assertEqual(_suffix_b1_finder.find(m.b1.b2), 2)
1864+
self.assertEqual(_suffix_b2_finder.find(m.b1.b2), 2)
1865+
1866+
self.assertEqual(_suffix_finder.find(m.b1), 1)
1867+
self.assertEqual(_suffix_b1_finder.find(m.b1), None)
1868+
self.assertEqual(_suffix_b2_finder.find(m.b1), None)
18391869

18401870

18411871
if __name__ == "__main__":

pyomo/repn/plugins/nl_writer.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -510,8 +510,8 @@ def compile(self, column_order, row_order, obj_order, model_id):
510510
class CachingNumericSuffixFinder(SuffixFinder):
511511
scale = True
512512

513-
def __init__(self, name, default=None):
514-
super().__init__(name, default)
513+
def __init__(self, name, default=None, context=None):
514+
super().__init__(name, default, context)
515515
self.suffix_cache = {}
516516

517517
def __call__(self, obj):
@@ -646,7 +646,7 @@ def write(self, model):
646646
# Data structures to support variable/constraint scaling
647647
#
648648
if self.config.scale_model and 'scaling_factor' in suffix_data:
649-
scaling_factor = CachingNumericSuffixFinder('scaling_factor', 1)
649+
scaling_factor = CachingNumericSuffixFinder('scaling_factor', 1, model)
650650
scaling_cache = scaling_factor.suffix_cache
651651
del suffix_data['scaling_factor']
652652
else:

0 commit comments

Comments
 (0)