Skip to content

Commit

Permalink
Merge branch 'main' into solver-refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
jsiirola authored Feb 21, 2024
2 parents 05e0b47 + 158933f commit 40ad333
Show file tree
Hide file tree
Showing 42 changed files with 4,824 additions and 2,710 deletions.
51 changes: 50 additions & 1 deletion doc/OnlineDocs/contributed_packages/pyros.rst
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ PyROS Solver Interface

Otherwise, the solution returned is certified to only be robust feasible.


PyROS Uncertainty Sets
-----------------------------
Uncertainty sets are represented by subclasses of
Expand Down Expand Up @@ -518,7 +519,7 @@ correspond to first-stage degrees of freedom.

>>> # === Designate which variables correspond to first-stage
>>> # and second-stage degrees of freedom ===
>>> first_stage_variables =[
>>> first_stage_variables = [
... m.x1, m.x2, m.x3, m.x4, m.x5, m.x6,
... m.x19, m.x20, m.x21, m.x22, m.x23, m.x24, m.x31,
... ]
Expand Down Expand Up @@ -657,6 +658,54 @@ For this example, we notice a ~25% decrease in the final objective
value when switching from a static decision rule (no second-stage recourse)
to an affine decision rule.


Specifying Arguments Indirectly Through ``options``
"""""""""""""""""""""""""""""""""""""""""""""""""""
Like other Pyomo solver interface methods,
:meth:`~pyomo.contrib.pyros.PyROS.solve`
provides support for specifying options indirectly by passing
a keyword argument ``options``, whose value must be a :class:`dict`
mapping names of arguments to :meth:`~pyomo.contrib.pyros.PyROS.solve`
to their desired values.
For example, the ``solve()`` statement in the
:ref:`two-stage problem snippet <example-two-stg>`
could have been equivalently written as:

.. doctest::
:skipif: not (baron.available() and baron.license_is_valid())

>>> results_2 = pyros_solver.solve(
... model=m,
... first_stage_variables=first_stage_variables,
... second_stage_variables=second_stage_variables,
... uncertain_params=uncertain_parameters,
... uncertainty_set=box_uncertainty_set,
... local_solver=local_solver,
... global_solver=global_solver,
... options={
... "objective_focus": pyros.ObjectiveType.worst_case,
... "solve_master_globally": True,
... "decision_rule_order": 1,
... },
... )
==============================================================================
PyROS: The Pyomo Robust Optimization Solver.
...
------------------------------------------------------------------------------
Robust optimal solution identified.
------------------------------------------------------------------------------
...
------------------------------------------------------------------------------
All done. Exiting PyROS.
==============================================================================

In the event an argument is passed directly
by position or keyword, *and* indirectly through ``options``,
an appropriate warning is issued,
and the value passed directly takes precedence over the value
passed through ``options``.


The Price of Robustness
""""""""""""""""""""""""
In conjunction with standard Python control flow tools,
Expand Down
2 changes: 1 addition & 1 deletion pyomo/common/autoslots.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def _deepcopy_tuple(obj, memo, _id):
unchanged = False
if unchanged:
# Python does not duplicate "unchanged" tuples (i.e. allows the
# original objecct to be returned from deepcopy()). We will
# original object to be returned from deepcopy()). We will
# preserve that behavior here.
#
# It also appears to be faster *not* to cache the fact that this
Expand Down
2 changes: 1 addition & 1 deletion pyomo/common/collections/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@
from collections import UserDict

from .orderedset import OrderedDict, OrderedSet
from .component_map import ComponentMap
from .component_map import ComponentMap, DefaultComponentMap
from .component_set import ComponentSet
from .bunch import Bunch
89 changes: 74 additions & 15 deletions pyomo/common/collections/component_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,49 @@
# This software is distributed under the 3-clause BSD License.
# ___________________________________________________________________________

from collections.abc import MutableMapping as collections_MutableMapping
import collections
from collections.abc import Mapping as collections_Mapping
from pyomo.common.autoslots import AutoSlots


def _rebuild_ids(encode, val):
def _rehash_keys(encode, val):
if encode:
return val
else:
# object id() may have changed after unpickling,
# so we rebuild the dictionary keys
return {id(obj): (obj, v) for obj, v in val.values()}
return {_hasher[obj.__class__](obj): (obj, v) for obj, v in val.values()}


class ComponentMap(AutoSlots.Mixin, collections_MutableMapping):
class _Hasher(collections.defaultdict):
def __init__(self, *args, **kwargs):
super().__init__(lambda: self._missing_impl, *args, **kwargs)
self[tuple] = self._tuple

def _missing_impl(self, val):
try:
hash(val)
self[val.__class__] = self._hashable
except:
self[val.__class__] = self._unhashable
return self[val.__class__](val)

@staticmethod
def _hashable(val):
return val

@staticmethod
def _unhashable(val):
return id(val)

def _tuple(self, val):
return tuple(self[i.__class__](i) for i in val)


_hasher = _Hasher()


class ComponentMap(AutoSlots.Mixin, collections.abc.MutableMapping):
"""
This class is a replacement for dict that allows Pyomo
modeling components to be used as entry keys. The
Expand All @@ -49,37 +77,39 @@ class ComponentMap(AutoSlots.Mixin, collections_MutableMapping):
"""

__slots__ = ("_dict",)
__autoslot_mappers__ = {'_dict': _rebuild_ids}
__autoslot_mappers__ = {'_dict': _rehash_keys}

def __init__(self, *args, **kwds):
# maps id(obj) -> (obj,val)
# maps id_hash(obj) -> (obj,val)
self._dict = {}
# handle the dict-style initialization scenarios
self.update(*args, **kwds)

def __str__(self):
"""String representation of the mapping."""
tmp = {str(c) + " (id=" + str(id(c)) + ")": v for c, v in self.items()}
return "ComponentMap(" + str(tmp) + ")"
tmp = {f"{v[0]} (key={k})": v[1] for k, v in self._dict.items()}
return f"ComponentMap({tmp})"

#
# Implement MutableMapping abstract methods
#

def __getitem__(self, obj):
try:
return self._dict[id(obj)][1]
return self._dict[_hasher[obj.__class__](obj)][1]
except KeyError:
raise KeyError("Component with id '%s': %s" % (id(obj), str(obj)))
_id = _hasher[obj.__class__](obj)
raise KeyError(f"{obj} (key={_id})") from None

def __setitem__(self, obj, val):
self._dict[id(obj)] = (obj, val)
self._dict[_hasher[obj.__class__](obj)] = (obj, val)

def __delitem__(self, obj):
try:
del self._dict[id(obj)]
del self._dict[_hasher[obj.__class__](obj)]
except KeyError:
raise KeyError("Component with id '%s': %s" % (id(obj), str(obj)))
_id = _hasher[obj.__class__](obj)
raise KeyError(f"{obj} (key={_id})") from None

def __iter__(self):
return (obj for obj, val in self._dict.values())
Expand Down Expand Up @@ -107,7 +137,7 @@ def __eq__(self, other):
return False
# Note we have already verified the dicts are the same size
for key, val in other.items():
other_id = id(key)
other_id = _hasher[key.__class__](key)
if other_id not in self._dict:
return False
self_val = self._dict[other_id][1]
Expand All @@ -130,7 +160,7 @@ def __ne__(self, other):
#

def __contains__(self, obj):
return id(obj) in self._dict
return _hasher[obj.__class__](obj) in self._dict

def clear(self):
'D.clear() -> None. Remove all items from D.'
Expand All @@ -149,3 +179,32 @@ def setdefault(self, key, default=None):
else:
self[key] = default
return default


class DefaultComponentMap(ComponentMap):
"""A :py:class:`defaultdict` admitting Pyomo Components as keys
This class is a replacement for defaultdict that allows Pyomo
modeling components to be used as entry keys. The base
implementation builds on :py:class:`ComponentMap`.
"""

__slots__ = ('default_factory',)

def __init__(self, default_factory=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.default_factory = default_factory

def __missing__(self, key):
if self.default_factory is None:
raise KeyError(key)
self[key] = ans = self.default_factory()
return ans

def __getitem__(self, obj):
_key = _hasher[obj.__class__](obj)
if _key in self._dict:
return self._dict[_key][1]
else:
return self.__missing__(obj)
87 changes: 47 additions & 40 deletions pyomo/common/collections/component_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,30 @@
from collections.abc import MutableSet as collections_MutableSet
from collections.abc import Set as collections_Set

from pyomo.common.autoslots import AutoSlots
from pyomo.common.collections.component_map import _hasher


def _rehash_keys(encode, val):
if encode:
# TBD [JDS 2/2024]: if we
#
# return list(val.values())
#
# here, then we get a strange failure when deepcopying
# ComponentSets containing an _ImplicitAny domain. We could
# track it down to the implementation of
# autoslots.fast_deepcopy, but couldn't find an obvious bug.
# There is no error if we just return the original dict, or if
# we return a tuple(val.values)
return val
else:
# object id() may have changed after unpickling,
# so we rebuild the dictionary keys
return {_hasher[obj.__class__](obj): obj for obj in val.values()}


class ComponentSet(collections_MutableSet):
class ComponentSet(AutoSlots.Mixin, collections_MutableSet):
"""
This class is a replacement for set that allows Pyomo
modeling components to be used as entries. The
Expand All @@ -38,47 +60,32 @@ class ComponentSet(collections_MutableSet):
"""

__slots__ = ("_data",)
__autoslot_mappers__ = {'_data': _rehash_keys}

def __init__(self, *args):
self._data = dict()
if len(args) > 0:
if len(args) > 1:
raise TypeError(
"%s expected at most 1 arguments, "
"got %s" % (self.__class__.__name__, len(args))
)
self.update(args[0])
def __init__(self, iterable=None):
# maps id_hash(obj) -> obj
self._data = {}
if iterable is not None:
self.update(iterable)

def __str__(self):
"""String representation of the mapping."""
tmp = []
for objid, obj in self._data.items():
tmp.append(str(obj) + " (id=" + str(objid) + ")")
return "ComponentSet(" + str(tmp) + ")"
tmp = [f"{v} (key={k})" for k, v in self._data.items()]
return f"ComponentSet({tmp})"

def update(self, args):
def update(self, iterable):
"""Update a set with the union of itself and others."""
self._data.update((id(obj), obj) for obj in args)

#
# This method must be defined for deepcopy/pickling
# because this class relies on Python ids.
#
def __setstate__(self, state):
# object id() may have changed after unpickling,
# so we rebuild the dictionary keys
assert len(state) == 1
self._data = {id(obj): obj for obj in state['_data']}

def __getstate__(self):
return {'_data': tuple(self._data.values())}
if isinstance(iterable, ComponentSet):
self._data.update(iterable._data)
else:
self._data.update((_hasher[val.__class__](val), val) for val in iterable)

#
# Implement MutableSet abstract methods
#

def __contains__(self, val):
return self._data.__contains__(id(val))
return _hasher[val.__class__](val) in self._data

def __iter__(self):
return iter(self._data.values())
Expand All @@ -88,27 +95,26 @@ def __len__(self):

def add(self, val):
"""Add an element."""
self._data[id(val)] = val
self._data[_hasher[val.__class__](val)] = val

def discard(self, val):
"""Remove an element. Do not raise an exception if absent."""
if id(val) in self._data:
del self._data[id(val)]
_id = _hasher[val.__class__](val)
if _id in self._data:
del self._data[_id]

#
# Overload MutableSet default implementations
#

# We want to avoid generating Pyomo expressions due to
# comparison of values, so we convert both objects to a
# plain dictionary mapping key->(type(val), id(val)) and
# compare that instead.
def __eq__(self, other):
if self is other:
return True
if not isinstance(other, collections_Set):
return False
return len(self) == len(other) and all(id(key) in self._data for key in other)
return len(self) == len(other) and all(
_hasher[val.__class__](val) in self._data for val in other
)

def __ne__(self, other):
return not (self == other)
Expand All @@ -125,6 +131,7 @@ def clear(self):
def remove(self, val):
"""Remove an element. If not a member, raise a KeyError."""
try:
del self._data[id(val)]
del self._data[_hasher[val.__class__](val)]
except KeyError:
raise KeyError("Component with id '%s': %s" % (id(val), str(val)))
_id = _hasher[val.__class__](val)
raise KeyError(f"{val} (key={_id})") from None
Loading

0 comments on commit 40ad333

Please sign in to comment.