diff --git a/doc/OnlineDocs/contributed_packages/pyros.rst b/doc/OnlineDocs/contributed_packages/pyros.rst index 3062bdf1ee8..aad37a9685a 100644 --- a/doc/OnlineDocs/contributed_packages/pyros.rst +++ b/doc/OnlineDocs/contributed_packages/pyros.rst @@ -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 @@ -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, ... ] @@ -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 ` +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, diff --git a/pyomo/common/autoslots.py b/pyomo/common/autoslots.py index cb79d4a0338..89fefaf4f21 100644 --- a/pyomo/common/autoslots.py +++ b/pyomo/common/autoslots.py @@ -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 diff --git a/pyomo/common/collections/__init__.py b/pyomo/common/collections/__init__.py index 93785124e3c..717caf87b2c 100644 --- a/pyomo/common/collections/__init__.py +++ b/pyomo/common/collections/__init__.py @@ -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 diff --git a/pyomo/common/collections/component_map.py b/pyomo/common/collections/component_map.py index 80ba5fe0d1c..8dcfdb6c837 100644 --- a/pyomo/common/collections/component_map.py +++ b/pyomo/common/collections/component_map.py @@ -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 @@ -49,18 +77,18 @@ 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 @@ -68,18 +96,20 @@ def __str__(self): 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()) @@ -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] @@ -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.' @@ -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) diff --git a/pyomo/common/collections/component_set.py b/pyomo/common/collections/component_set.py index dfeac5cbfa5..6e12bad7277 100644 --- a/pyomo/common/collections/component_set.py +++ b/pyomo/common/collections/component_set.py @@ -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 @@ -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()) @@ -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) @@ -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 diff --git a/pyomo/common/collections/orderedset.py b/pyomo/common/collections/orderedset.py index f29245b75fe..834101e3896 100644 --- a/pyomo/common/collections/orderedset.py +++ b/pyomo/common/collections/orderedset.py @@ -9,42 +9,30 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from collections.abc import MutableSet from collections import OrderedDict +from collections.abc import MutableSet +from pyomo.common.autoslots import AutoSlots -class OrderedSet(MutableSet): +class OrderedSet(AutoSlots.Mixin, MutableSet): __slots__ = ('_dict',) def __init__(self, iterable=None): - # TODO: Starting in Python 3.7, dict is ordered (and is faster - # than OrderedDict). dict began supporting reversed() in 3.8. - # We should consider changing the underlying data type here from - # OrderedDict to dict. - self._dict = OrderedDict() + # Starting in Python 3.7, dict is ordered (and is faster than + # OrderedDict). dict began supporting reversed() in 3.8. + self._dict = {} if iterable is not None: - if iterable.__class__ is OrderedSet: - self._dict.update(iterable._dict) - else: - self.update(iterable) + self.update(iterable) def __str__(self): """String representation of the mapping.""" return "OrderedSet(%s)" % (', '.join(repr(x) for x in self)) def update(self, iterable): - for val in iterable: - self.add(val) - - # - # This method must be defined for deepcopy/pickling - # because this class is slotized. - # - def __setstate__(self, state): - self._dict = state - - def __getstate__(self): - return self._dict + if isinstance(iterable, OrderedSet): + self._dict.update(iterable._dict) + else: + self._dict.update((val, None) for val in iterable) # # Implement MutableSet abstract methods diff --git a/pyomo/common/tests/test_component_map.py b/pyomo/common/tests/test_component_map.py new file mode 100644 index 00000000000..7cd4ec2c458 --- /dev/null +++ b/pyomo/common/tests/test_component_map.py @@ -0,0 +1,90 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest + +from pyomo.common.collections import ComponentMap, ComponentSet, DefaultComponentMap +from pyomo.environ import ConcreteModel, Block, Var, Constraint + + +class TestComponentMap(unittest.TestCase): + def test_tuple(self): + m = ConcreteModel() + m.v = Var() + m.c = Constraint(expr=m.v >= 0) + m.cm = cm = ComponentMap() + + cm[(1, 2)] = 5 + self.assertEqual(len(cm), 1) + self.assertIn((1, 2), cm) + self.assertEqual(cm[1, 2], 5) + + cm[(1, 2)] = 50 + self.assertEqual(len(cm), 1) + self.assertIn((1, 2), cm) + self.assertEqual(cm[1, 2], 50) + + cm[(1, (2, m.v))] = 10 + self.assertEqual(len(cm), 2) + self.assertIn((1, (2, m.v)), cm) + self.assertEqual(cm[1, (2, m.v)], 10) + + cm[(1, (2, m.v))] = 100 + self.assertEqual(len(cm), 2) + self.assertIn((1, (2, m.v)), cm) + self.assertEqual(cm[1, (2, m.v)], 100) + + i = m.clone() + self.assertIn((1, 2), i.cm) + self.assertIn((1, (2, i.v)), i.cm) + self.assertNotIn((1, (2, i.v)), m.cm) + self.assertIn((1, (2, m.v)), m.cm) + self.assertNotIn((1, (2, m.v)), i.cm) + + +class TestDefaultComponentMap(unittest.TestCase): + def test_default_component_map(self): + dcm = DefaultComponentMap(ComponentSet) + + m = ConcreteModel() + m.x = Var() + m.b = Block() + m.b.y = Var() + + self.assertEqual(len(dcm), 0) + + dcm[m.x].add(m) + self.assertEqual(len(dcm), 1) + self.assertIn(m.x, dcm) + self.assertIn(m, dcm[m.x]) + + dcm[m.b.y].add(m.b) + self.assertEqual(len(dcm), 2) + self.assertIn(m.b.y, dcm) + self.assertNotIn(m, dcm[m.b.y]) + self.assertIn(m.b, dcm[m.b.y]) + + dcm[m.b.y].add(m) + self.assertEqual(len(dcm), 2) + self.assertIn(m.b.y, dcm) + self.assertIn(m, dcm[m.b.y]) + self.assertIn(m.b, dcm[m.b.y]) + + def test_no_default_factory(self): + dcm = DefaultComponentMap() + + dcm['found'] = 5 + self.assertEqual(len(dcm), 1) + self.assertIn('found', dcm) + self.assertEqual(dcm['found'], 5) + + with self.assertRaisesRegex(KeyError, "'missing'"): + dcm["missing"] diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 2c522af864d..7f04ffbfce7 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -411,9 +411,11 @@ def _check_and_escape_options(): if cp.returncode != 0: if self.config.load_solution: raise RuntimeError( - 'A feasible solution was not found, so no solution can be loaded.' - 'Please set opt.config.load_solution=False and check ' - 'results.termination_condition and ' + 'A feasible solution was not found, so no solution can be loaded. ' + 'If using the appsi.solvers.Cbc interface, you can ' + 'set opt.config.load_solution=False. If using the environ.SolverFactory ' + 'interface, you can set opt.solve(model, load_solutions = False). ' + 'Then you can check results.termination_condition and ' 'results.best_feasible_objective before loading a solution.' ) results = Results() diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 1d7147f16e8..1b7ab5000d2 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -341,9 +341,11 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): if config.load_solution: if cpxprob.solution.get_solution_type() == cpxprob.solution.type.none: raise RuntimeError( - 'A feasible solution was not found, so no solution can be loades. ' - 'Please set opt.config.load_solution=False and check ' - 'results.termination_condition and ' + 'A feasible solution was not found, so no solution can be loaded. ' + 'If using the appsi.solvers.Cplex interface, you can ' + 'set opt.config.load_solution=False. If using the environ.SolverFactory ' + 'interface, you can set opt.solve(model, load_solutions = False). ' + 'Then you can check results.termination_condition and ' 'results.best_feasible_objective before loading a solution.' ) else: diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index aa233ef77d6..1e18862e3bd 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -946,9 +946,11 @@ def _postsolve(self, timer: HierarchicalTimer): self.load_vars() else: raise RuntimeError( - 'A feasible solution was not found, so no solution can be loaded.' - 'Please set opt.config.load_solution=False and check ' - 'results.termination_condition and ' + 'A feasible solution was not found, so no solution can be loaded. ' + 'If using the appsi.solvers.Gurobi interface, you can ' + 'set opt.config.load_solution=False. If using the environ.SolverFactory ' + 'interface, you can set opt.solve(model, load_solutions = False). ' + 'Then you can check results.termination_condition and ' 'results.best_feasible_objective before loading a solution.' ) timer.stop('load solution') diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index a9a23682355..3612b9d5014 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -680,9 +680,11 @@ def _postsolve(self, timer: HierarchicalTimer): self.load_vars() else: raise RuntimeError( - 'A feasible solution was not found, so no solution can be loaded.' - 'Please set opt.config.load_solution=False and check ' - 'results.termination_condition and ' + 'A feasible solution was not found, so no solution can be loaded. ' + 'If using the appsi.solvers.Highs interface, you can ' + 'set opt.config.load_solution=False. If using the environ.SolverFactory ' + 'interface, you can set opt.solve(model, load_solutions = False). ' + 'Then you can check results.termination_condition and ' 'results.best_feasible_objective before loading a solution.' ) timer.stop('load solution') diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index d7a786e6c2c..29e74f81c98 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -421,9 +421,11 @@ def _parse_sol(self): results.best_feasible_objective = value(obj_expr_evaluated) elif self.config.load_solution: raise RuntimeError( - 'A feasible solution was not found, so no solution can be loaded.' - 'Please set opt.config.load_solution=False and check ' - 'results.termination_condition and ' + 'A feasible solution was not found, so no solution can be loaded. ' + 'If using the appsi.solvers.Ipopt interface, you can ' + 'set opt.config.load_solution=False. If using the environ.SolverFactory ' + 'interface, you can set opt.solve(model, load_solutions = False). ' + 'Then you can check results.termination_condition and ' 'results.best_feasible_objective before loading a solution.' ) diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index e1835b810b0..00c0598c687 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -169,9 +169,11 @@ def _solve(self, timer: HierarchicalTimer): timer.stop('load solution') else: raise RuntimeError( - 'A feasible solution was not found, so no solution can be loaded.' - 'Please set opt.config.load_solution=False and check ' - 'results.termination_condition and ' + 'A feasible solution was not found, so no solution can be loaded. ' + 'If using the appsi.solvers.Wntr interface, you can ' + 'set opt.config.load_solution=False. If using the environ.SolverFactory ' + 'interface, you can set opt.solve(model, load_solutions = False). ' + 'Then you can check results.termination_condition and ' 'results.best_feasible_objective before loading a solution.' ) return results diff --git a/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py b/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py index 3c01acab531..dc721488f74 100644 --- a/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py +++ b/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py @@ -435,7 +435,7 @@ def check_hull_projected_constraints(self, m, constraints, indices): self.assertIs(body.linear_vars[2], m.startup.binary_indicator_var) self.assertEqual(body.linear_coefs[2], 2) - # 1 <= time1_disjuncts[0].ind_var + time_1.disjuncts[1].ind_var + # 1 <= time1_disjuncts[0].ind_var + time1_disjuncts[1].ind_var cons = constraints[indices[7]] self.assertEqual(cons.lower, 1) self.assertIsNone(cons.upper) @@ -548,12 +548,12 @@ def test_project_disaggregated_vars(self): # we of course get tremendous amounts of garbage, but we make sure that # what should be here is: self.check_hull_projected_constraints( - m, constraints, [23, 19, 8, 10, 54, 67, 35, 3, 4, 1, 2] + m, constraints, [16, 12, 69, 71, 47, 60, 28, 1, 2, 3, 4] ) # and when we filter, it's still there. constraints = filtered._pyomo_contrib_fme_transformation.projected_constraints self.check_hull_projected_constraints( - filtered, constraints, [10, 8, 5, 6, 15, 19, 11, 3, 4, 1, 2] + filtered, constraints, [8, 6, 20, 21, 13, 17, 9, 1, 2, 3, 4] ) @unittest.skipIf(not 'glpk' in solvers, 'glpk not available') @@ -570,7 +570,7 @@ def test_post_processing(self): # They should be the same as the above, but now these are *all* the # constraints self.check_hull_projected_constraints( - m, constraints, [10, 8, 5, 6, 15, 19, 11, 3, 4, 1, 2] + m, constraints, [8, 6, 20, 21, 13, 17, 9, 1, 2, 3, 4] ) # and check that we didn't change the model diff --git a/pyomo/contrib/gdpopt/branch_and_bound.py b/pyomo/contrib/gdpopt/branch_and_bound.py index 918f3d459a0..36b81c881be 100644 --- a/pyomo/contrib/gdpopt/branch_and_bound.py +++ b/pyomo/contrib/gdpopt/branch_and_bound.py @@ -230,12 +230,12 @@ def _solve_gdp(self, model, config): no_feasible_soln = float('inf') self.LB = ( node_data.obj_lb - if solve_data.objective_sense == minimize + if self.objective_sense == minimize else -no_feasible_soln ) self.UB = ( no_feasible_soln - if solve_data.objective_sense == minimize + if self.objective_sense == minimize else -node_data.obj_lb ) config.logger.info( diff --git a/pyomo/contrib/pyros/CHANGELOG.txt b/pyomo/contrib/pyros/CHANGELOG.txt index 7d4678f0ba3..94f4848edb2 100644 --- a/pyomo/contrib/pyros/CHANGELOG.txt +++ b/pyomo/contrib/pyros/CHANGELOG.txt @@ -2,6 +2,13 @@ PyROS CHANGELOG =============== +------------------------------------------------------------------------------- +PyROS 1.2.10 07 Feb 2024 +------------------------------------------------------------------------------- +- Update argument resolution and validation routines of `PyROS.solve()` +- Use methods of `common.config` for docstring of `PyROS.solve()` + + ------------------------------------------------------------------------------- PyROS 1.2.9 15 Dec 2023 ------------------------------------------------------------------------------- @@ -14,6 +21,7 @@ PyROS 1.2.9 15 Dec 2023 - Refactor DR polishing routine; initialize auxiliary variables to values they are meant to represent + ------------------------------------------------------------------------------- PyROS 1.2.8 12 Oct 2023 ------------------------------------------------------------------------------- diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py new file mode 100644 index 00000000000..a7ca41d095f --- /dev/null +++ b/pyomo/contrib/pyros/config.py @@ -0,0 +1,901 @@ +""" +Interfaces for managing PyROS solver options. +""" + +from collections.abc import Iterable +import logging + +from pyomo.common.collections import ComponentSet +from pyomo.common.config import ( + ConfigDict, + ConfigValue, + In, + IsInstance, + NonNegativeFloat, + InEnum, + Path, +) +from pyomo.common.errors import ApplicationError, PyomoException +from pyomo.core.base import Var, _VarData +from pyomo.core.base.param import Param, _ParamData +from pyomo.opt import SolverFactory +from pyomo.contrib.pyros.util import ObjectiveType, setup_pyros_logger +from pyomo.contrib.pyros.uncertainty_sets import UncertaintySet + + +default_pyros_solver_logger = setup_pyros_logger() + + +class LoggerType: + """ + Domain validator for objects castable to logging.Logger. + """ + + def __call__(self, obj): + """ + Cast object to logger. + + Parameters + ---------- + obj : object + Object to be cast. + + Returns + ------- + logging.Logger + If `str_or_logger` is of type `logging.Logger`,then + `str_or_logger` is returned. + Otherwise, ``logging.getLogger(str_or_logger)`` + is returned. + """ + if isinstance(obj, logging.Logger): + return obj + else: + return logging.getLogger(obj) + + def domain_name(self): + """Return str briefly describing domain encompassed by self.""" + return "None, str or logging.Logger" + + +class PositiveIntOrMinusOne: + """ + Domain validator for objects castable to a + strictly positive int or -1. + """ + + def __call__(self, obj): + """ + Cast object to positive int or -1. + + Parameters + ---------- + obj : object + Object of interest. + + Returns + ------- + int + Positive int, or -1. + + Raises + ------ + ValueError + If object not castable to positive int, or -1. + """ + ans = int(obj) + if ans != float(obj) or (ans <= 0 and ans != -1): + raise ValueError(f"Expected positive int or -1, but received value {obj!r}") + return ans + + def domain_name(self): + """Return str briefly describing domain encompassed by self.""" + return "positive int or -1" + + +def mutable_param_validator(param_obj): + """ + Check that Param-like object has attribute `mutable=True`. + + Parameters + ---------- + param_obj : Param or _ParamData + Param-like object of interest. + + Raises + ------ + ValueError + If lengths of the param object and the accompanying + index set do not match. This may occur if some entry + of the Param is not initialized. + ValueError + If attribute `mutable` is of value False. + """ + if len(param_obj) != len(param_obj.index_set()): + raise ValueError( + f"Length of Param component object with " + f"name {param_obj.name!r} is {len(param_obj)}, " + "and does not match that of its index set, " + f"which is of length {len(param_obj.index_set())}. " + "Check that all entries of the component object " + "have been initialized." + ) + if not param_obj.mutable: + raise ValueError(f"Param object with name {param_obj.name!r} is immutable.") + + +class InputDataStandardizer(object): + """ + Standardizer for objects castable to a list of Pyomo + component types. + + Parameters + ---------- + ctype : type + Pyomo component type, such as Component, Var or Param. + cdatatype : type + Corresponding Pyomo component data type, such as + _ComponentData, _VarData, or _ParamData. + ctype_validator : callable, optional + Validator function for objects of type `ctype`. + cdatatype_validator : callable, optional + Validator function for objects of type `cdatatype`. + allow_repeats : bool, optional + True to allow duplicate component data entries in final + list to which argument is cast, False otherwise. + + Attributes + ---------- + ctype + cdatatype + ctype_validator + cdatatype_validator + allow_repeats + """ + + def __init__( + self, + ctype, + cdatatype, + ctype_validator=None, + cdatatype_validator=None, + allow_repeats=False, + ): + """Initialize self (see class docstring).""" + self.ctype = ctype + self.cdatatype = cdatatype + self.ctype_validator = ctype_validator + self.cdatatype_validator = cdatatype_validator + self.allow_repeats = allow_repeats + + def standardize_ctype_obj(self, obj): + """ + Standardize object of type ``self.ctype`` to list + of objects of type ``self.cdatatype``. + """ + if self.ctype_validator is not None: + self.ctype_validator(obj) + return list(obj.values()) + + def standardize_cdatatype_obj(self, obj): + """ + Standardize object of type ``self.cdatatype`` to + ``[obj]``. + """ + if self.cdatatype_validator is not None: + self.cdatatype_validator(obj) + return [obj] + + def __call__(self, obj, from_iterable=None, allow_repeats=None): + """ + Cast object to a flat list of Pyomo component data type + entries. + + Parameters + ---------- + obj : object + Object to be cast. + from_iterable : Iterable or None, optional + Iterable from which `obj` obtained, if any. + allow_repeats : bool or None, optional + True if list can contain repeated entries, + False otherwise. + + Raises + ------ + TypeError + If all entries in the resulting list + are not of type ``self.cdatatype``. + ValueError + If the resulting list contains duplicate entries. + """ + if allow_repeats is None: + allow_repeats = self.allow_repeats + + if isinstance(obj, self.ctype): + ans = self.standardize_ctype_obj(obj) + elif isinstance(obj, self.cdatatype): + ans = self.standardize_cdatatype_obj(obj) + elif isinstance(obj, Iterable) and not isinstance(obj, str): + ans = [] + for item in obj: + ans.extend(self.__call__(item, from_iterable=obj)) + else: + from_iterable_qual = ( + f" (entry of iterable {from_iterable})" + if from_iterable is not None + else "" + ) + raise TypeError( + f"Input object {obj!r}{from_iterable_qual} " + "is not of valid component type " + f"{self.ctype.__name__} or component data type " + f"{self.cdatatype.__name__}." + ) + + # check for duplicates if desired + if not allow_repeats and len(ans) != len(ComponentSet(ans)): + comp_name_list = [comp.name for comp in ans] + raise ValueError( + f"Standardized component list {comp_name_list} " + f"derived from input {obj} " + "contains duplicate entries." + ) + + return ans + + def domain_name(self): + """Return str briefly describing domain encompassed by self.""" + return ( + f"{self.cdatatype.__name__}, {self.ctype.__name__}, " + f"or Iterable of {self.cdatatype.__name__}/{self.ctype.__name__}" + ) + + +class SolverNotResolvable(PyomoException): + """ + Exception type for failure to cast an object to a Pyomo solver. + """ + + +class SolverResolvable(object): + """ + Callable for casting an object (such as a str) + to a Pyomo solver. + + Parameters + ---------- + require_available : bool, optional + True if `available()` method of a standardized solver + object obtained through `self` must return `True`, + False otherwise. + solver_desc : str, optional + Descriptor for the solver obtained through `self`, + such as 'local solver' + or 'global solver'. This argument is used + for constructing error/exception messages. + + Attributes + ---------- + require_available + solver_desc + """ + + def __init__(self, require_available=True, solver_desc="solver"): + """Initialize self (see class docstring).""" + self.require_available = require_available + self.solver_desc = solver_desc + + @staticmethod + def is_solver_type(obj): + """ + Return True if object is considered a Pyomo solver, + False otherwise. + + An object is considered a Pyomo solver provided that + it has callable attributes named 'solve' and + 'available'. + """ + return callable(getattr(obj, "solve", None)) and callable( + getattr(obj, "available", None) + ) + + def __call__(self, obj, require_available=None, solver_desc=None): + """ + Cast object to a Pyomo solver. + + If `obj` is a string, then ``SolverFactory(obj.lower())`` + is returned. If `obj` is a Pyomo solver type, then + `obj` is returned. + + Parameters + ---------- + obj : object + Object to be cast to Pyomo solver type. + require_available : bool or None, optional + True if `available()` method of the resolved solver + object must return True, False otherwise. + If `None` is passed, then ``self.require_available`` + is used. + solver_desc : str or None, optional + Brief description of the solver, such as 'local solver' + or 'backup global solver'. This argument is used + for constructing error/exception messages. + If `None` is passed, then ``self.solver_desc`` + is used. + + Returns + ------- + Solver + Pyomo solver. + + Raises + ------ + SolverNotResolvable + If `obj` cannot be cast to a Pyomo solver because + it is neither a str nor a Pyomo solver type. + ApplicationError + In event that solver is not available, the + method `available(exception_flag=True)` of the + solver to which `obj` is cast should raise an + exception of this type. The present method + will also emit a more detailed error message + through the default PyROS logger. + """ + # resort to defaults if necessary + if require_available is None: + require_available = self.require_available + if solver_desc is None: + solver_desc = self.solver_desc + + # perform casting + if isinstance(obj, str): + solver = SolverFactory(obj.lower()) + elif self.is_solver_type(obj): + solver = obj + else: + raise SolverNotResolvable( + f"Cannot cast object `{obj!r}` to a Pyomo optimizer for use as " + f"{solver_desc}, as the object is neither a str nor a " + f"Pyomo Solver type (got type {type(obj).__name__})." + ) + + # availability check, if so desired + if require_available: + try: + solver.available(exception_flag=True) + except ApplicationError: + default_pyros_solver_logger.exception( + f"Output of `available()` method for {solver_desc} " + f"with repr {solver!r} resolved from object {obj} " + "is not `True`. " + "Check solver and any required dependencies " + "have been set up properly." + ) + raise + + return solver + + def domain_name(self): + """Return str briefly describing domain encompassed by self.""" + return "str or Solver" + + +class SolverIterable(object): + """ + Callable for casting an iterable (such as a list of strs) + to a list of Pyomo solvers. + + Parameters + ---------- + require_available : bool, optional + True if `available()` method of a standardized solver + object obtained through `self` must return `True`, + False otherwise. + filter_by_availability : bool, optional + True to remove standardized solvers for which `available()` + does not return True, False otherwise. + solver_desc : str, optional + Descriptor for the solver obtained through `self`, + such as 'backup local solver' + or 'backup global solver'. + """ + + def __init__( + self, require_available=True, filter_by_availability=True, solver_desc="solver" + ): + """Initialize self (see class docstring).""" + self.require_available = require_available + self.filter_by_availability = filter_by_availability + self.solver_desc = solver_desc + + def __call__( + self, obj, require_available=None, filter_by_availability=None, solver_desc=None + ): + """ + Cast iterable object to a list of Pyomo solver objects. + + Parameters + ---------- + obj : str, Solver, or Iterable of str/Solver + Object of interest. + require_available : bool or None, optional + True if `available()` method of each solver + object must return True, False otherwise. + If `None` is passed, then ``self.require_available`` + is used. + solver_desc : str or None, optional + Descriptor for the solver, such as 'backup local solver' + or 'backup global solver'. This argument is used + for constructing error/exception messages. + If `None` is passed, then ``self.solver_desc`` + is used. + + Returns + ------- + solvers : list of solver type + List of solver objects to which obj is cast. + + Raises + ------ + TypeError + If `obj` is a str. + """ + if require_available is None: + require_available = self.require_available + if filter_by_availability is None: + filter_by_availability = self.filter_by_availability + if solver_desc is None: + solver_desc = self.solver_desc + + solver_resolve_func = SolverResolvable() + + if isinstance(obj, str) or solver_resolve_func.is_solver_type(obj): + # single solver resolvable is cast to singleton list. + # perform explicit check for str, otherwise this method + # would attempt to resolve each character. + obj_as_list = [obj] + else: + obj_as_list = list(obj) + + solvers = [] + for idx, val in enumerate(obj_as_list): + solver_desc_str = f"{solver_desc} " f"(index {idx})" + opt = solver_resolve_func( + obj=val, + require_available=require_available, + solver_desc=solver_desc_str, + ) + if filter_by_availability and not opt.available(exception_flag=False): + default_pyros_solver_logger.warning( + f"Output of `available()` method for solver object {opt} " + f"resolved from object {val} of sequence {obj_as_list} " + f"to be used as {self.solver_desc} " + "is not `True`. " + "Removing from list of standardized solvers." + ) + else: + solvers.append(opt) + + return solvers + + def domain_name(self): + """Return str briefly describing domain encompassed by self.""" + return "str, solver type, or Iterable of str/solver type" + + +def pyros_config(): + CONFIG = ConfigDict('PyROS') + + # ================================================ + # === Options common to all solvers + # ================================================ + CONFIG.declare( + 'time_limit', + ConfigValue( + default=None, + domain=NonNegativeFloat, + doc=( + """ + Wall time limit for the execution of the PyROS solver + in seconds (including time spent by subsolvers). + If `None` is provided, then no time limit is enforced. + """ + ), + ), + ) + CONFIG.declare( + 'keepfiles', + ConfigValue( + default=False, + domain=bool, + description=( + """ + Export subproblems with a non-acceptable termination status + for debugging purposes. + If True is provided, then the argument + `subproblem_file_directory` must also be specified. + """ + ), + ), + ) + CONFIG.declare( + 'tee', + ConfigValue( + default=False, + domain=bool, + description="Output subordinate solver logs for all subproblems.", + ), + ) + CONFIG.declare( + 'load_solution', + ConfigValue( + default=True, + domain=bool, + description=( + """ + Load final solution(s) found by PyROS to the deterministic + model provided. + """ + ), + ), + ) + + # ================================================ + # === Required User Inputs + # ================================================ + CONFIG.declare( + "first_stage_variables", + ConfigValue( + default=[], + domain=InputDataStandardizer(Var, _VarData, allow_repeats=False), + description="First-stage (or design) variables.", + visibility=1, + ), + ) + CONFIG.declare( + "second_stage_variables", + ConfigValue( + default=[], + domain=InputDataStandardizer(Var, _VarData, allow_repeats=False), + description="Second-stage (or control) variables.", + visibility=1, + ), + ) + CONFIG.declare( + "uncertain_params", + ConfigValue( + default=[], + domain=InputDataStandardizer( + ctype=Param, + cdatatype=_ParamData, + ctype_validator=mutable_param_validator, + allow_repeats=False, + ), + description=( + """ + Uncertain model parameters. + The `mutable` attribute for all uncertain parameter + objects should be set to True. + """ + ), + visibility=1, + ), + ) + CONFIG.declare( + "uncertainty_set", + ConfigValue( + default=None, + domain=IsInstance(UncertaintySet), + description=( + """ + Uncertainty set against which the + final solution(s) returned by PyROS should be certified + to be robust. + """ + ), + visibility=1, + ), + ) + CONFIG.declare( + "local_solver", + ConfigValue( + default=None, + domain=SolverResolvable(solver_desc="local solver", require_available=True), + description="Subordinate local NLP solver.", + visibility=1, + ), + ) + CONFIG.declare( + "global_solver", + ConfigValue( + default=None, + domain=SolverResolvable( + solver_desc="global solver", require_available=True + ), + description="Subordinate global NLP solver.", + visibility=1, + ), + ) + # ================================================ + # === Optional User Inputs + # ================================================ + CONFIG.declare( + "objective_focus", + ConfigValue( + default=ObjectiveType.nominal, + domain=InEnum(ObjectiveType), + description=( + """ + Choice of objective focus to optimize in the master problems. + Choices are: `ObjectiveType.worst_case`, + `ObjectiveType.nominal`. + """ + ), + doc=( + """ + Objective focus for the master problems: + + - `ObjectiveType.nominal`: + Optimize the objective function subject to the nominal + uncertain parameter realization. + - `ObjectiveType.worst_case`: + Optimize the objective function subject to the worst-case + uncertain parameter realization. + + By default, `ObjectiveType.nominal` is chosen. + + A worst-case objective focus is required for certification + of robust optimality of the final solution(s) returned + by PyROS. + If a nominal objective focus is chosen, then only robust + feasibility is guaranteed. + """ + ), + ), + ) + CONFIG.declare( + "nominal_uncertain_param_vals", + ConfigValue( + default=[], + domain=list, + doc=( + """ + Nominal uncertain parameter realization. + Entries should be provided in an order consistent with the + entries of the argument `uncertain_params`. + If an empty list is provided, then the values of the `Param` + objects specified through `uncertain_params` are chosen. + """ + ), + ), + ) + CONFIG.declare( + "decision_rule_order", + ConfigValue( + default=0, + domain=In([0, 1, 2]), + description=( + """ + Order (or degree) of the polynomial decision rule functions + used for approximating the adjustability of the second stage + variables with respect to the uncertain parameters. + """ + ), + doc=( + """ + Order (or degree) of the polynomial decision rule functions + for approximating the adjustability of the second stage + variables with respect to the uncertain parameters. + + Choices are: + + - 0: static recourse + - 1: affine recourse + - 2: quadratic recourse + """ + ), + ), + ) + CONFIG.declare( + "solve_master_globally", + ConfigValue( + default=False, + domain=bool, + doc=( + """ + True to solve all master problems with the subordinate + global solver, False to solve all master problems with + the subordinate local solver. + Along with a worst-case objective focus + (see argument `objective_focus`), + solving the master problems to global optimality is required + for certification + of robust optimality of the final solution(s) returned + by PyROS. Otherwise, only robust feasibility is guaranteed. + """ + ), + ), + ) + CONFIG.declare( + "max_iter", + ConfigValue( + default=-1, + domain=PositiveIntOrMinusOne(), + description=( + """ + Iteration limit. If -1 is provided, then no iteration + limit is enforced. + """ + ), + ), + ) + CONFIG.declare( + "robust_feasibility_tolerance", + ConfigValue( + default=1e-4, + domain=NonNegativeFloat, + description=( + """ + Relative tolerance for assessing maximal inequality + constraint violations during the GRCS separation step. + """ + ), + ), + ) + CONFIG.declare( + "separation_priority_order", + ConfigValue( + default={}, + domain=dict, + doc=( + """ + Mapping from model inequality constraint names + to positive integers specifying the priorities + of their corresponding separation subproblems. + A higher integer value indicates a higher priority. + Constraints not referenced in the `dict` assume + a priority of 0. + Separation subproblems are solved in order of decreasing + priority. + """ + ), + ), + ) + CONFIG.declare( + "progress_logger", + ConfigValue( + default=default_pyros_solver_logger, + domain=LoggerType(), + doc=( + """ + Logger (or name thereof) used for reporting PyROS solver + progress. If `None` or a `str` is provided, then + ``progress_logger`` + is cast to ``logging.getLogger(progress_logger)``. + In the default case, `progress_logger` is set to + a :class:`pyomo.contrib.pyros.util.PreformattedLogger` + object of level ``logging.INFO``. + """ + ), + ), + ) + CONFIG.declare( + "backup_local_solvers", + ConfigValue( + default=[], + domain=SolverIterable( + solver_desc="backup local solver", + require_available=False, + filter_by_availability=True, + ), + doc=( + """ + Additional subordinate local NLP optimizers to invoke + in the event the primary local NLP optimizer fails + to solve a subproblem to an acceptable termination condition. + """ + ), + ), + ) + CONFIG.declare( + "backup_global_solvers", + ConfigValue( + default=[], + domain=SolverIterable( + solver_desc="backup global solver", + require_available=False, + filter_by_availability=True, + ), + doc=( + """ + Additional subordinate global NLP optimizers to invoke + in the event the primary global NLP optimizer fails + to solve a subproblem to an acceptable termination condition. + """ + ), + ), + ) + CONFIG.declare( + "subproblem_file_directory", + ConfigValue( + default=None, + domain=Path(), + description=( + """ + Directory to which to export subproblems not successfully + solved to an acceptable termination condition. + In the event ``keepfiles=True`` is specified, a str or + path-like referring to an existing directory must be + provided. + """ + ), + ), + ) + + # ================================================ + # === Advanced Options + # ================================================ + CONFIG.declare( + "bypass_local_separation", + ConfigValue( + default=False, + domain=bool, + description=( + """ + This is an advanced option. + Solve all separation subproblems with the subordinate global + solver(s) only. + This option is useful for expediting PyROS + in the event that the subordinate global optimizer(s) provided + can quickly solve separation subproblems to global optimality. + """ + ), + ), + ) + CONFIG.declare( + "bypass_global_separation", + ConfigValue( + default=False, + domain=bool, + doc=( + """ + This is an advanced option. + Solve all separation subproblems with the subordinate local + solver(s) only. + If `True` is chosen, then robustness of the final solution(s) + returned by PyROS is not guaranteed, and a warning will + be issued at termination. + This option is useful for expediting PyROS + in the event that the subordinate global optimizer provided + cannot tractably solve separation subproblems to global + optimality. + """ + ), + ), + ) + CONFIG.declare( + "p_robustness", + ConfigValue( + default={}, + domain=dict, + doc=( + """ + This is an advanced option. + Add p-robustness constraints to all master subproblems. + If an empty dict is provided, then p-robustness constraints + are not added. + Otherwise, the dict must map a `str` of value ``'rho'`` + to a non-negative `float`. PyROS automatically + specifies ``1 + p_robustness['rho']`` + as an upper bound for the ratio of the + objective function value under any PyROS-sampled uncertain + parameter realization to the objective function under + the nominal parameter realization. + """ + ), + visibility=1, + ), + ) + + return CONFIG diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 475eb424c0b..6de42d7299e 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -11,29 +11,25 @@ # pyros.py: Generalized Robust Cutting-Set Algorithm for Pyomo import logging -from textwrap import indent, dedent, wrap -from pyomo.common.collections import Bunch, ComponentSet -from pyomo.common.config import ConfigDict, ConfigValue, In, NonNegativeFloat +from pyomo.common.config import document_kwargs_from_configdict +from pyomo.common.collections import Bunch from pyomo.core.base.block import Block from pyomo.core.expr import value -from pyomo.core.base.var import Var, _VarData -from pyomo.core.base.param import Param, _ParamData -from pyomo.core.base.objective import Objective, maximize -from pyomo.contrib.pyros.util import a_logger, time_code, get_main_elapsed_time +from pyomo.core.base.var import Var +from pyomo.core.base.objective import Objective +from pyomo.contrib.pyros.util import time_code from pyomo.common.modeling import unique_component_name from pyomo.opt import SolverFactory +from pyomo.contrib.pyros.config import pyros_config from pyomo.contrib.pyros.util import ( - model_is_valid, recast_to_min_obj, add_decision_rule_constraints, add_decision_rule_variables, load_final_solution, pyrosTerminationCondition, - ValidEnum, ObjectiveType, - validate_uncertainty_set, identify_objective_functions, - validate_kwarg_inputs, + validate_pyros_inputs, transform_to_standard_form, turn_bounds_to_constraints, replace_uncertain_bounds_with_constraints, @@ -43,13 +39,12 @@ ) from pyomo.contrib.pyros.solve_data import ROSolveResults from pyomo.contrib.pyros.pyros_algorithm_methods import ROSolver_iterative_solve -from pyomo.contrib.pyros.uncertainty_sets import uncertainty_sets from pyomo.core.base import Constraint from datetime import datetime -__version__ = "1.2.9" +__version__ = "1.2.10" default_pyros_solver_logger = setup_pyros_logger() @@ -85,590 +80,6 @@ def _get_pyomo_version_info(): return {"Pyomo version": pyomo_version, "Commit hash": commit_hash} -def NonNegIntOrMinusOne(obj): - ''' - if obj is a non-negative int, return the non-negative int - if obj is -1, return -1 - else, error - ''' - ans = int(obj) - if ans != float(obj) or (ans < 0 and ans != -1): - raise ValueError("Expected non-negative int, but received %s" % (obj,)) - return ans - - -def PositiveIntOrMinusOne(obj): - ''' - if obj is a positive int, return the int - if obj is -1, return -1 - else, error - ''' - ans = int(obj) - if ans != float(obj) or (ans <= 0 and ans != -1): - raise ValueError("Expected positive int, but received %s" % (obj,)) - return ans - - -class SolverResolvable(object): - def __call__(self, obj): - ''' - if obj is a string, return the Solver object for that solver name - if obj is a Solver object, return a copy of the Solver - if obj is a list, and each element of list is solver resolvable, return list of solvers - ''' - if isinstance(obj, str): - return SolverFactory(obj.lower()) - elif callable(getattr(obj, "solve", None)): - return obj - elif isinstance(obj, list): - return [self(o) for o in obj] - else: - raise ValueError( - "Expected a Pyomo solver or string object, " - "instead received {1}".format(obj.__class__.__name__) - ) - - -class InputDataStandardizer(object): - def __init__(self, ctype, cdatatype): - self.ctype = ctype - self.cdatatype = cdatatype - - def __call__(self, obj): - if isinstance(obj, self.ctype): - return list(obj.values()) - if isinstance(obj, self.cdatatype): - return [obj] - ans = [] - for item in obj: - ans.extend(self.__call__(item)) - for _ in ans: - assert isinstance(_, self.cdatatype) - return ans - - -class PyROSConfigValue(ConfigValue): - """ - Subclass of ``common.collections.ConfigValue``, - with a few attributes added to facilitate documentation - of the PyROS solver. - An instance of this class is used for storing and - documenting an argument to the PyROS solver. - - Attributes - ---------- - is_optional : bool - Argument is optional. - document_default : bool, optional - Document the default value of the argument - in any docstring generated from this instance, - or a `ConfigDict` object containing this instance. - dtype_spec_str : None or str, optional - String documenting valid types for this argument. - If `None` is provided, then this string is automatically - determined based on the `domain` argument to the - constructor. - - NOTES - ----- - Cleaner way to access protected attributes - (particularly _doc, _description) inherited from ConfigValue? - - """ - - def __init__( - self, - default=None, - domain=None, - description=None, - doc=None, - visibility=0, - is_optional=True, - document_default=True, - dtype_spec_str=None, - ): - """Initialize self (see class docstring).""" - - # initialize base class attributes - super(self.__class__, self).__init__( - default=default, - domain=domain, - description=description, - doc=doc, - visibility=visibility, - ) - - self.is_optional = is_optional - self.document_default = document_default - - if dtype_spec_str is None: - self.dtype_spec_str = self.domain_name() - # except AttributeError: - # self.dtype_spec_str = repr(self._domain) - else: - self.dtype_spec_str = dtype_spec_str - - -def pyros_config(): - CONFIG = ConfigDict('PyROS') - - # ================================================ - # === Options common to all solvers - # ================================================ - CONFIG.declare( - 'time_limit', - PyROSConfigValue( - default=None, - domain=NonNegativeFloat, - doc=( - """ - Wall time limit for the execution of the PyROS solver - in seconds (including time spent by subsolvers). - If `None` is provided, then no time limit is enforced. - """ - ), - is_optional=True, - document_default=False, - dtype_spec_str="None or NonNegativeFloat", - ), - ) - CONFIG.declare( - 'keepfiles', - PyROSConfigValue( - default=False, - domain=bool, - description=( - """ - Export subproblems with a non-acceptable termination status - for debugging purposes. - If True is provided, then the argument `subproblem_file_directory` - must also be specified. - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str=None, - ), - ) - CONFIG.declare( - 'tee', - PyROSConfigValue( - default=False, - domain=bool, - description="Output subordinate solver logs for all subproblems.", - is_optional=True, - document_default=True, - dtype_spec_str=None, - ), - ) - CONFIG.declare( - 'load_solution', - PyROSConfigValue( - default=True, - domain=bool, - description=( - """ - Load final solution(s) found by PyROS to the deterministic model - provided. - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str=None, - ), - ) - - # ================================================ - # === Required User Inputs - # ================================================ - CONFIG.declare( - "first_stage_variables", - PyROSConfigValue( - default=[], - domain=InputDataStandardizer(Var, _VarData), - description="First-stage (or design) variables.", - is_optional=False, - dtype_spec_str="list of Var", - ), - ) - CONFIG.declare( - "second_stage_variables", - PyROSConfigValue( - default=[], - domain=InputDataStandardizer(Var, _VarData), - description="Second-stage (or control) variables.", - is_optional=False, - dtype_spec_str="list of Var", - ), - ) - CONFIG.declare( - "uncertain_params", - PyROSConfigValue( - default=[], - domain=InputDataStandardizer(Param, _ParamData), - description=( - """ - Uncertain model parameters. - The `mutable` attribute for all uncertain parameter - objects should be set to True. - """ - ), - is_optional=False, - dtype_spec_str="list of Param", - ), - ) - CONFIG.declare( - "uncertainty_set", - PyROSConfigValue( - default=None, - domain=uncertainty_sets, - description=( - """ - Uncertainty set against which the - final solution(s) returned by PyROS should be certified - to be robust. - """ - ), - is_optional=False, - dtype_spec_str="UncertaintySet", - ), - ) - CONFIG.declare( - "local_solver", - PyROSConfigValue( - default=None, - domain=SolverResolvable(), - description="Subordinate local NLP solver.", - is_optional=False, - dtype_spec_str="Solver", - ), - ) - CONFIG.declare( - "global_solver", - PyROSConfigValue( - default=None, - domain=SolverResolvable(), - description="Subordinate global NLP solver.", - is_optional=False, - dtype_spec_str="Solver", - ), - ) - # ================================================ - # === Optional User Inputs - # ================================================ - CONFIG.declare( - "objective_focus", - PyROSConfigValue( - default=ObjectiveType.nominal, - domain=ValidEnum(ObjectiveType), - description=( - """ - Choice of objective focus to optimize in the master problems. - Choices are: `ObjectiveType.worst_case`, - `ObjectiveType.nominal`. - """ - ), - doc=( - """ - Objective focus for the master problems: - - - `ObjectiveType.nominal`: - Optimize the objective function subject to the nominal - uncertain parameter realization. - - `ObjectiveType.worst_case`: - Optimize the objective function subject to the worst-case - uncertain parameter realization. - - By default, `ObjectiveType.nominal` is chosen. - - A worst-case objective focus is required for certification - of robust optimality of the final solution(s) returned - by PyROS. - If a nominal objective focus is chosen, then only robust - feasibility is guaranteed. - """ - ), - is_optional=True, - document_default=False, - dtype_spec_str="ObjectiveType", - ), - ) - CONFIG.declare( - "nominal_uncertain_param_vals", - PyROSConfigValue( - default=[], - domain=list, - doc=( - """ - Nominal uncertain parameter realization. - Entries should be provided in an order consistent with the - entries of the argument `uncertain_params`. - If an empty list is provided, then the values of the `Param` - objects specified through `uncertain_params` are chosen. - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str="list of float", - ), - ) - CONFIG.declare( - "decision_rule_order", - PyROSConfigValue( - default=0, - domain=In([0, 1, 2]), - description=( - """ - Order (or degree) of the polynomial decision rule functions used - for approximating the adjustability of the second stage - variables with respect to the uncertain parameters. - """ - ), - doc=( - """ - Order (or degree) of the polynomial decision rule functions used - for approximating the adjustability of the second stage - variables with respect to the uncertain parameters. - - Choices are: - - - 0: static recourse - - 1: affine recourse - - 2: quadratic recourse - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str=None, - ), - ) - CONFIG.declare( - "solve_master_globally", - PyROSConfigValue( - default=False, - domain=bool, - doc=( - """ - True to solve all master problems with the subordinate - global solver, False to solve all master problems with - the subordinate local solver. - Along with a worst-case objective focus - (see argument `objective_focus`), - solving the master problems to global optimality is required - for certification - of robust optimality of the final solution(s) returned - by PyROS. Otherwise, only robust feasibility is guaranteed. - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str=None, - ), - ) - CONFIG.declare( - "max_iter", - PyROSConfigValue( - default=-1, - domain=PositiveIntOrMinusOne, - description=( - """ - Iteration limit. If -1 is provided, then no iteration - limit is enforced. - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str="int", - ), - ) - CONFIG.declare( - "robust_feasibility_tolerance", - PyROSConfigValue( - default=1e-4, - domain=NonNegativeFloat, - description=( - """ - Relative tolerance for assessing maximal inequality - constraint violations during the GRCS separation step. - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str=None, - ), - ) - CONFIG.declare( - "separation_priority_order", - PyROSConfigValue( - default={}, - domain=dict, - doc=( - """ - Mapping from model inequality constraint names - to positive integers specifying the priorities - of their corresponding separation subproblems. - A higher integer value indicates a higher priority. - Constraints not referenced in the `dict` assume - a priority of 0. - Separation subproblems are solved in order of decreasing - priority. - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str=None, - ), - ) - CONFIG.declare( - "progress_logger", - PyROSConfigValue( - default=default_pyros_solver_logger, - domain=a_logger, - doc=( - """ - Logger (or name thereof) used for reporting PyROS solver - progress. If a `str` is specified, then ``progress_logger`` - is cast to ``logging.getLogger(progress_logger)``. - In the default case, `progress_logger` is set to - a :class:`pyomo.contrib.pyros.util.PreformattedLogger` - object of level ``logging.INFO``. - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str="str or logging.Logger", - ), - ) - CONFIG.declare( - "backup_local_solvers", - PyROSConfigValue( - default=[], - domain=SolverResolvable(), - doc=( - """ - Additional subordinate local NLP optimizers to invoke - in the event the primary local NLP optimizer fails - to solve a subproblem to an acceptable termination condition. - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str="list of Solver", - ), - ) - CONFIG.declare( - "backup_global_solvers", - PyROSConfigValue( - default=[], - domain=SolverResolvable(), - doc=( - """ - Additional subordinate global NLP optimizers to invoke - in the event the primary global NLP optimizer fails - to solve a subproblem to an acceptable termination condition. - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str="list of Solver", - ), - ) - CONFIG.declare( - "subproblem_file_directory", - PyROSConfigValue( - default=None, - domain=str, - description=( - """ - Directory to which to export subproblems not successfully - solved to an acceptable termination condition. - In the event ``keepfiles=True`` is specified, a str or - path-like referring to an existing directory must be - provided. - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str="None, str, or path-like", - ), - ) - - # ================================================ - # === Advanced Options - # ================================================ - CONFIG.declare( - "bypass_local_separation", - PyROSConfigValue( - default=False, - domain=bool, - description=( - """ - This is an advanced option. - Solve all separation subproblems with the subordinate global - solver(s) only. - This option is useful for expediting PyROS - in the event that the subordinate global optimizer(s) provided - can quickly solve separation subproblems to global optimality. - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str=None, - ), - ) - CONFIG.declare( - "bypass_global_separation", - PyROSConfigValue( - default=False, - domain=bool, - doc=( - """ - This is an advanced option. - Solve all separation subproblems with the subordinate local - solver(s) only. - If `True` is chosen, then robustness of the final solution(s) - returned by PyROS is not guaranteed, and a warning will - be issued at termination. - This option is useful for expediting PyROS - in the event that the subordinate global optimizer provided - cannot tractably solve separation subproblems to global - optimality. - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str=None, - ), - ) - CONFIG.declare( - "p_robustness", - PyROSConfigValue( - default={}, - domain=dict, - doc=( - """ - This is an advanced option. - Add p-robustness constraints to all master subproblems. - If an empty dict is provided, then p-robustness constraints - are not added. - Otherwise, the dict must map a `str` of value ``'rho'`` - to a non-negative `float`. PyROS automatically - specifies ``1 + p_robustness['rho']`` - as an upper bound for the ratio of the - objective function value under any PyROS-sampled uncertain - parameter realization to the objective function under - the nominal parameter realization. - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str=None, - ), - ) - - return CONFIG - - @SolverFactory.register( "pyros", doc="Robust optimization (RO) solver implementing " @@ -836,6 +247,46 @@ def _log_config(self, logger, config, exclude_options=None, **log_kwargs): logger.log(msg=f" {key}={val!r}", **log_kwargs) logger.log(msg="-" * self._LOG_LINE_LENGTH, **log_kwargs) + def _resolve_and_validate_pyros_args(self, model, **kwds): + """ + Resolve and validate arguments to ``self.solve()``. + + Parameters + ---------- + model : ConcreteModel + Deterministic model object passed to ``self.solve()``. + **kwds : dict + All other arguments to ``self.solve()``. + + Returns + ------- + config : ConfigDict + Standardized arguments. + + Note + ---- + This method can be broken down into three steps: + + 1. Cast arguments to ConfigDict. Argument-wise + validation is performed automatically. + Note that arguments specified directly take + precedence over arguments specified indirectly + through direct argument 'options'. + 2. Inter-argument validation. + """ + config = self.CONFIG(kwds.pop("options", {})) + config = config(kwds) + state_vars = validate_pyros_inputs(model, config) + + return config, state_vars + + @document_kwargs_from_configdict( + config=CONFIG, + section="Keyword Arguments", + indent_spacing=4, + width=72, + visibility=0, + ) def solve( self, model, @@ -853,21 +304,25 @@ def solve( ---------- model: ConcreteModel The deterministic model. - first_stage_variables: list of Var + first_stage_variables: VarData, Var, or iterable of VarData/Var First-stage model variables (or design variables). - second_stage_variables: list of Var + second_stage_variables: VarData, Var, or iterable of VarData/Var Second-stage model variables (or control variables). - uncertain_params: list of Param + uncertain_params: ParamData, Param, or iterable of ParamData/Param Uncertain model parameters. - The `mutable` attribute for every uncertain parameter - objects must be set to True. + The `mutable` attribute for all uncertain parameter objects + must be set to True. uncertainty_set: UncertaintySet Uncertainty set against which the solution(s) returned will be confirmed to be robust. - local_solver: Solver + local_solver: str or solver type Subordinate local NLP solver. - global_solver: Solver + If a `str` is passed, then the `str` is cast to + ``SolverFactory(local_solver)``. + global_solver: str or solver type Subordinate global NLP solver. + If a `str` is passed, then the `str` is cast to + ``SolverFactory(global_solver)``. Returns ------- @@ -875,41 +330,17 @@ def solve( Summary of PyROS termination outcome. """ - - # === Add the explicit arguments to the config - config = self.CONFIG(kwds.pop('options', {})) - config.first_stage_variables = first_stage_variables - config.second_stage_variables = second_stage_variables - config.uncertain_params = uncertain_params - config.uncertainty_set = uncertainty_set - config.local_solver = local_solver - config.global_solver = global_solver - - dev_options = kwds.pop('dev_options', {}) - config.set_value(kwds) - config.set_value(dev_options) - - model = model - - # === Validate kwarg inputs - validate_kwarg_inputs(model, config) - - # === Validate ability of grcs RO solver to handle this model - if not model_is_valid(model): - raise AttributeError( - "This model structure is not currently handled by the ROSolver." - ) - - # === Define nominal point if not specified - if len(config.nominal_uncertain_param_vals) == 0: - config.nominal_uncertain_param_vals = list( - p.value for p in config.uncertain_params - ) - elif len(config.nominal_uncertain_param_vals) != len(config.uncertain_params): - raise AttributeError( - "The nominal_uncertain_param_vals list must be the same length" - "as the uncertain_params list" + kwds.update( + dict( + first_stage_variables=first_stage_variables, + second_stage_variables=second_stage_variables, + uncertain_params=uncertain_params, + uncertainty_set=uncertainty_set, + local_solver=local_solver, + global_solver=global_solver, ) + ) + config, state_vars = self._resolve_and_validate_pyros_args(model, **kwds) # === Create data containers model_data = ROSolveResults() @@ -940,15 +371,13 @@ def solve( util = Block(concrete=True) util.first_stage_variables = config.first_stage_variables util.second_stage_variables = config.second_stage_variables + util.state_vars = state_vars util.uncertain_params = config.uncertain_params model_data.util_block = unique_component_name(model, 'util') model.add_component(model_data.util_block, util) # Note: model.component(model_data.util_block) is util - # === Validate uncertainty set happens here, requires util block for Cardinality and FactorModel sets - validate_uncertainty_set(config=config) - # === Leads to a logger warning here for inactive obj when cloning model_data.original_model = model # === For keeping track of variables after cloning @@ -990,22 +419,10 @@ def solve( # === Move bounds on control variables to explicit ineq constraints wm_util = model_data.working_model - # === Every non-fixed variable that is neither first-stage - # nor second-stage is taken to be a state variable - fsv = ComponentSet(model_data.working_model.util.first_stage_variables) - ssv = ComponentSet(model_data.working_model.util.second_stage_variables) - sv = ComponentSet() - model_data.working_model.util.state_vars = [] - for v in model_data.working_model.component_data_objects(Var): - if not v.fixed and v not in fsv | ssv | sv: - model_data.working_model.util.state_vars.append(v) - sv.add(v) - - # Bounds on second stage variables and state variables are separation objectives, - # they are brought in this was as explicit constraints + # cast bounds on second-stage and state variables to + # explicit constraints for separation objectives for c in model_data.working_model.util.second_stage_variables: turn_bounds_to_constraints(c, wm_util, config) - for c in model_data.working_model.util.state_vars: turn_bounds_to_constraints(c, wm_util, config) @@ -1085,131 +502,3 @@ def solve( config.progress_logger.info("=" * self._LOG_LINE_LENGTH) return return_soln - - -def _generate_filtered_docstring(): - """ - Add Numpy-style 'Keyword arguments' section to `PyROS.solve()` - docstring. - """ - cfg = PyROS.CONFIG() - - # mandatory args already documented - exclude_args = [ - "first_stage_variables", - "second_stage_variables", - "uncertain_params", - "uncertainty_set", - "local_solver", - "global_solver", - ] - - indent_by = 8 - width = 72 - before = PyROS.solve.__doc__ - section_name = "Keyword Arguments" - - indent_str = ' ' * indent_by - wrap_width = width - indent_by - cfg = pyros_config() - - arg_docs = [] - - def wrap_doc(doc, indent_by, width): - """ - Wrap a string, accounting for paragraph - breaks ('\n\n') and bullet points (paragraphs - which, when dedented, are such that each line - starts with '- ' or ' '). - """ - paragraphs = doc.split("\n\n") - wrapped_pars = [] - for par in paragraphs: - lines = dedent(par).split("\n") - has_bullets = all( - line.startswith("- ") or line.startswith(" ") - for line in lines - if line != "" - ) - if has_bullets: - # obtain strings of each bullet point - # (dedented, bullet dash and bullet indent removed) - bullet_groups = [] - new_group = False - group = "" - for line in lines: - new_group = line.startswith("- ") - if new_group: - bullet_groups.append(group) - group = "" - new_line = line[2:] - group += f"{new_line}\n" - if group != "": - # ensure last bullet not skipped - bullet_groups.append(group) - - # first entry is just ''; remove - bullet_groups = bullet_groups[1:] - - # wrap each bullet point, then add bullet - # and indents as necessary - wrapped_groups = [] - for group in bullet_groups: - wrapped_groups.append( - "\n".join( - f"{'- ' if idx == 0 else ' '}{line}" - for idx, line in enumerate( - wrap(group, width - 2 - indent_by) - ) - ) - ) - - # now combine bullets into single 'paragraph' - wrapped_pars.append( - indent("\n".join(wrapped_groups), prefix=' ' * indent_by) - ) - else: - wrapped_pars.append( - indent( - "\n".join(wrap(dedent(par), width=width - indent_by)), - prefix=' ' * indent_by, - ) - ) - - return "\n\n".join(wrapped_pars) - - section_header = indent(f"{section_name}\n" + "-" * len(section_name), indent_str) - for key, itm in cfg._data.items(): - if key in exclude_args: - continue - arg_name = key - arg_dtype = itm.dtype_spec_str - - if itm.is_optional: - if itm.document_default: - optional_str = f", default={repr(itm._default)}" - else: - optional_str = ", optional" - else: - optional_str = "" - - arg_header = f"{indent_str}{arg_name} : {arg_dtype}{optional_str}" - - # dedented_doc_str = dedent(itm.doc).replace("\n", ' ').strip() - if itm._doc is not None: - raw_arg_desc = itm._doc - else: - raw_arg_desc = itm._description - - arg_description = wrap_doc( - raw_arg_desc, width=wrap_width, indent_by=indent_by + 4 - ) - - arg_docs.append(f"{arg_header}\n{arg_description}") - - kwargs_section_doc = "\n".join([section_header] + arg_docs) - - return f"{before}\n{kwargs_section_doc}\n" - - -PyROS.solve.__doc__ = _generate_filtered_docstring() diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py new file mode 100644 index 00000000000..76b9114b9e6 --- /dev/null +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -0,0 +1,607 @@ +""" +Test objects for construction of PyROS ConfigDict. +""" + +import logging +import unittest + +from pyomo.core.base import ConcreteModel, Var, _VarData +from pyomo.common.log import LoggingIntercept +from pyomo.common.errors import ApplicationError +from pyomo.core.base.param import Param, _ParamData +from pyomo.contrib.pyros.config import ( + InputDataStandardizer, + mutable_param_validator, + LoggerType, + SolverNotResolvable, + PositiveIntOrMinusOne, + pyros_config, + SolverIterable, + SolverResolvable, +) +from pyomo.contrib.pyros.util import ObjectiveType +from pyomo.opt import SolverFactory, SolverResults +from pyomo.contrib.pyros.uncertainty_sets import BoxSet +from pyomo.common.dependencies import numpy_available + + +class TestInputDataStandardizer(unittest.TestCase): + """ + Test standardizer method for Pyomo component-type inputs. + """ + + def test_single_component_data(self): + """ + Test standardizer works for single component + data-type entry. + """ + mdl = ConcreteModel() + mdl.v = Var([0, 1]) + + standardizer_func = InputDataStandardizer(Var, _VarData) + + standardizer_input = mdl.v[0] + standardizer_output = standardizer_func(standardizer_input) + + self.assertIsInstance( + standardizer_output, + list, + msg=( + "Standardized output should be of type list, " + f"but is of type {standardizer_output.__class__.__name__}." + ), + ) + self.assertEqual( + len(standardizer_output), + 1, + msg="Length of standardizer output is not as expected.", + ) + self.assertIs( + standardizer_output[0], + mdl.v[0], + msg=( + f"Entry {standardizer_output[0]} (id {id(standardizer_output[0])}) " + "is not identical to " + f"input component data object {mdl.v[0]} " + f"(id {id(mdl.v[0])})" + ), + ) + + def test_standardizer_indexed_component(self): + """ + Test component standardizer works on indexed component. + """ + mdl = ConcreteModel() + mdl.v = Var([0, 1]) + + standardizer_func = InputDataStandardizer(Var, _VarData) + + standardizer_input = mdl.v + standardizer_output = standardizer_func(standardizer_input) + + self.assertIsInstance( + standardizer_output, + list, + msg=( + "Standardized output should be of type list, " + f"but is of type {standardizer_output.__class__.__name__}." + ), + ) + self.assertEqual( + len(standardizer_output), + 2, + msg="Length of standardizer output is not as expected.", + ) + enum_zip = enumerate(zip(standardizer_input.values(), standardizer_output)) + for idx, (input, output) in enum_zip: + self.assertIs( + input, + output, + msg=( + f"Entry {input} (id {id(input)}) " + "is not identical to " + f"input component data object {output} " + f"(id {id(output)})" + ), + ) + + def test_standardizer_multiple_components(self): + """ + Test standardizer works on sequence of components. + """ + mdl = ConcreteModel() + mdl.v = Var([0, 1]) + mdl.x = Var(["a", "b"]) + + standardizer_func = InputDataStandardizer(Var, _VarData) + + standardizer_input = [mdl.v[0], mdl.x] + standardizer_output = standardizer_func(standardizer_input) + expected_standardizer_output = [mdl.v[0], mdl.x["a"], mdl.x["b"]] + + self.assertIsInstance( + standardizer_output, + list, + msg=( + "Standardized output should be of type list, " + f"but is of type {standardizer_output.__class__.__name__}." + ), + ) + self.assertEqual( + len(standardizer_output), + len(expected_standardizer_output), + msg="Length of standardizer output is not as expected.", + ) + enum_zip = enumerate(zip(expected_standardizer_output, standardizer_output)) + for idx, (input, output) in enum_zip: + self.assertIs( + input, + output, + msg=( + f"Entry {input} (id {id(input)}) " + "is not identical to " + f"input component data object {output} " + f"(id {id(output)})" + ), + ) + + def test_standardizer_invalid_duplicates(self): + """ + Test standardizer raises exception if input contains duplicates + and duplicates are not allowed. + """ + mdl = ConcreteModel() + mdl.v = Var([0, 1]) + mdl.x = Var(["a", "b"]) + + standardizer_func = InputDataStandardizer(Var, _VarData, allow_repeats=False) + + exc_str = r"Standardized.*list.*contains duplicate entries\." + with self.assertRaisesRegex(ValueError, exc_str): + standardizer_func([mdl.x, mdl.v, mdl.x]) + + def test_standardizer_invalid_type(self): + """ + Test standardizer raises exception as expected + when input is of invalid type. + """ + standardizer_func = InputDataStandardizer(Var, _VarData) + + exc_str = r"Input object .*is not of valid component type.*" + with self.assertRaisesRegex(TypeError, exc_str): + standardizer_func(2) + + def test_standardizer_iterable_with_invalid_type(self): + """ + Test standardizer raises exception as expected + when input is an iterable with entries of invalid type. + """ + mdl = ConcreteModel() + mdl.v = Var([0, 1]) + standardizer_func = InputDataStandardizer(Var, _VarData) + + exc_str = r"Input object .*entry of iterable.*is not of valid component type.*" + with self.assertRaisesRegex(TypeError, exc_str): + standardizer_func([mdl.v, 2]) + + def test_standardizer_invalid_str_passed(self): + """ + Test standardizer raises exception as expected + when input is of invalid type str. + """ + standardizer_func = InputDataStandardizer(Var, _VarData) + + exc_str = r"Input object .*is not of valid component type.*" + with self.assertRaisesRegex(TypeError, exc_str): + standardizer_func("abcd") + + def test_standardizer_invalid_uninitialized_params(self): + """ + Test standardizer raises exception when Param with + uninitialized entries passed. + """ + standardizer_func = InputDataStandardizer( + ctype=Param, cdatatype=_ParamData, ctype_validator=mutable_param_validator + ) + + mdl = ConcreteModel() + mdl.p = Param([0, 1]) + + exc_str = r"Length of .*does not match that of.*index set" + with self.assertRaisesRegex(ValueError, exc_str): + standardizer_func(mdl.p) + + def test_standardizer_invalid_immutable_params(self): + """ + Test standardizer raises exception when immutable + Param object(s) passed. + """ + standardizer_func = InputDataStandardizer( + ctype=Param, cdatatype=_ParamData, ctype_validator=mutable_param_validator + ) + + mdl = ConcreteModel() + mdl.p = Param([0, 1], initialize=1) + + exc_str = r"Param object with name .*immutable" + with self.assertRaisesRegex(ValueError, exc_str): + standardizer_func(mdl.p) + + def test_standardizer_valid_mutable_params(self): + """ + Test Param-like standardizer works as expected for sequence + of valid mutable Param objects. + """ + mdl = ConcreteModel() + mdl.p1 = Param([0, 1], initialize=0, mutable=True) + mdl.p2 = Param(["a", "b"], initialize=1, mutable=True) + + standardizer_func = InputDataStandardizer( + ctype=Param, cdatatype=_ParamData, ctype_validator=mutable_param_validator + ) + + standardizer_input = [mdl.p1[0], mdl.p2] + standardizer_output = standardizer_func(standardizer_input) + expected_standardizer_output = [mdl.p1[0], mdl.p2["a"], mdl.p2["b"]] + + self.assertIsInstance( + standardizer_output, + list, + msg=( + "Standardized output should be of type list, " + f"but is of type {standardizer_output.__class__.__name__}." + ), + ) + self.assertEqual( + len(standardizer_output), + len(expected_standardizer_output), + msg="Length of standardizer output is not as expected.", + ) + enum_zip = enumerate(zip(expected_standardizer_output, standardizer_output)) + for idx, (input, output) in enum_zip: + self.assertIs( + input, + output, + msg=( + f"Entry {input} (id {id(input)}) " + "is not identical to " + f"input component data object {output} " + f"(id {id(output)})" + ), + ) + + +AVAILABLE_SOLVER_TYPE_NAME = "available_pyros_test_solver" + + +class AvailableSolver: + """ + Perennially available placeholder solver. + """ + + def available(self, exception_flag=False): + """ + Check solver available. + """ + return True + + def solve(self, model, **kwds): + """ + Return SolverResults object with 'unknown' termination + condition. Model remains unchanged. + """ + return SolverResults() + + +class UnavailableSolver: + def available(self, exception_flag=True): + if exception_flag: + raise ApplicationError(f"Solver {self.__class__} not available") + return False + + def solve(self, model, *args, **kwargs): + return SolverResults() + + +class TestSolverResolvable(unittest.TestCase): + """ + Test PyROS standardizer for solver-type objects. + """ + + def setUp(self): + SolverFactory.register(AVAILABLE_SOLVER_TYPE_NAME)(AvailableSolver) + + def tearDown(self): + SolverFactory.unregister(AVAILABLE_SOLVER_TYPE_NAME) + + def test_solver_resolvable_valid_str(self): + """ + Test solver resolvable class is valid for string + type. + """ + solver_str = AVAILABLE_SOLVER_TYPE_NAME + standardizer_func = SolverResolvable() + solver = standardizer_func(solver_str) + expected_solver_type = type(SolverFactory(solver_str)) + + self.assertIsInstance( + solver, + type(SolverFactory(solver_str)), + msg=( + "SolverResolvable object should be of type " + f"{expected_solver_type.__name__}, " + f"but got object of type {solver.__class__.__name__}." + ), + ) + + def test_solver_resolvable_valid_solver_type(self): + """ + Test solver resolvable class is valid for string + type. + """ + solver = SolverFactory(AVAILABLE_SOLVER_TYPE_NAME) + standardizer_func = SolverResolvable() + standardized_solver = standardizer_func(solver) + + self.assertIs( + solver, + standardized_solver, + msg=( + f"Test solver {solver} and standardized solver " + f"{standardized_solver} are not identical." + ), + ) + + def test_solver_resolvable_invalid_type(self): + """ + Test solver resolvable object raises expected + exception when invalid entry is provided. + """ + invalid_object = 2 + standardizer_func = SolverResolvable(solver_desc="local solver") + + exc_str = ( + r"Cannot cast object `2` to a Pyomo optimizer.*" + r"local solver.*got type int.*" + ) + with self.assertRaisesRegex(SolverNotResolvable, exc_str): + standardizer_func(invalid_object) + + def test_solver_resolvable_unavailable_solver(self): + """ + Test solver standardizer fails in event solver is + unavailable. + """ + unavailable_solver = UnavailableSolver() + standardizer_func = SolverResolvable( + solver_desc="local solver", require_available=True + ) + + exc_str = r"Solver.*UnavailableSolver.*not available" + with self.assertRaisesRegex(ApplicationError, exc_str): + with LoggingIntercept(level=logging.ERROR) as LOG: + standardizer_func(unavailable_solver) + + error_msgs = LOG.getvalue()[:-1] + self.assertRegex( + error_msgs, r"Output of `available\(\)` method.*local solver.*" + ) + + +class TestSolverIterable(unittest.TestCase): + """ + Test standardizer method for iterable of solvers, + used to validate `backup_local_solvers` and `backup_global_solvers` + arguments. + """ + + def setUp(self): + SolverFactory.register(AVAILABLE_SOLVER_TYPE_NAME)(AvailableSolver) + + def tearDown(self): + SolverFactory.unregister(AVAILABLE_SOLVER_TYPE_NAME) + + def test_solver_iterable_valid_list(self): + """ + Test solver type standardizer works for list of valid + objects castable to solver. + """ + solver_list = [ + AVAILABLE_SOLVER_TYPE_NAME, + SolverFactory(AVAILABLE_SOLVER_TYPE_NAME), + ] + expected_solver_types = [AvailableSolver] * 2 + standardizer_func = SolverIterable() + + standardized_solver_list = standardizer_func(solver_list) + + # check list of solver types returned + for idx, standardized_solver in enumerate(standardized_solver_list): + self.assertIsInstance( + standardized_solver, + expected_solver_types[idx], + msg=( + f"Standardized solver {standardized_solver} " + f"(index {idx}) expected to be of type " + f"{expected_solver_types[idx].__name__}, " + f"but is of type {standardized_solver.__class__.__name__}" + ), + ) + + # second entry of standardized solver list should be the same + # object as that of input list, since the input solver is a Pyomo + # solver type + self.assertIs( + standardized_solver_list[1], + solver_list[1], + msg=( + f"Test solver {solver_list[1]} and standardized solver " + f"{standardized_solver_list[1]} should be identical." + ), + ) + + def test_solver_iterable_valid_str(self): + """ + Test SolverIterable raises exception when str passed. + """ + solver_str = AVAILABLE_SOLVER_TYPE_NAME + standardizer_func = SolverIterable() + + solver_list = standardizer_func(solver_str) + self.assertEqual( + len(solver_list), 1, "Standardized solver list is not of expected length" + ) + + def test_solver_iterable_unavailable_solver(self): + """ + Test SolverIterable addresses unavailable solvers appropriately. + """ + solvers = (AvailableSolver(), UnavailableSolver()) + + standardizer_func = SolverIterable( + require_available=True, + filter_by_availability=True, + solver_desc="example solver list", + ) + exc_str = r"Solver.*UnavailableSolver.* not available" + with self.assertRaisesRegex(ApplicationError, exc_str): + standardizer_func(solvers) + with self.assertRaisesRegex(ApplicationError, exc_str): + standardizer_func(solvers, filter_by_availability=False) + + standardized_solver_list = standardizer_func( + solvers, filter_by_availability=True, require_available=False + ) + self.assertEqual( + len(standardized_solver_list), + 1, + msg=("Length of filtered standardized solver list not as " "expected."), + ) + self.assertIs( + standardized_solver_list[0], + solvers[0], + msg="Entry of filtered standardized solver list not as expected.", + ) + + standardized_solver_list = standardizer_func( + solvers, filter_by_availability=False, require_available=False + ) + self.assertEqual( + len(standardized_solver_list), + 2, + msg=("Length of filtered standardized solver list not as " "expected."), + ) + self.assertEqual( + standardized_solver_list, + list(solvers), + msg="Entry of filtered standardized solver list not as expected.", + ) + + def test_solver_iterable_invalid_list(self): + """ + Test SolverIterable raises exception if iterable contains + at least one invalid object. + """ + invalid_object = [AVAILABLE_SOLVER_TYPE_NAME, 2] + standardizer_func = SolverIterable(solver_desc="backup solver") + + exc_str = ( + r"Cannot cast object `2` to a Pyomo optimizer.*" + r"backup solver.*index 1.*got type int.*" + ) + with self.assertRaisesRegex(SolverNotResolvable, exc_str): + standardizer_func(invalid_object) + + +class TestPyROSConfig(unittest.TestCase): + """ + Test PyROS ConfigDict behaves as expected. + """ + + CONFIG = pyros_config() + + def test_config_objective_focus(self): + """ + Test config parses objective focus as expected. + """ + config = self.CONFIG() + + for obj_focus_name in ["nominal", "worst_case"]: + config.objective_focus = obj_focus_name + self.assertEqual( + config.objective_focus, + ObjectiveType[obj_focus_name], + msg="Objective focus not set as expected.", + ) + + for obj_focus in ObjectiveType: + config.objective_focus = obj_focus + self.assertEqual( + config.objective_focus, + obj_focus, + msg="Objective focus not set as expected.", + ) + + invalid_focus = "test_example" + exc_str = f".*{invalid_focus!r} is not a valid ObjectiveType" + with self.assertRaisesRegex(ValueError, exc_str): + config.objective_focus = invalid_focus + + +class TestPositiveIntOrMinusOne(unittest.TestCase): + """ + Test validator for -1 or positive int works as expected. + """ + + def test_positive_int_or_minus_one(self): + """ + Test positive int or -1 validator works as expected. + """ + standardizer_func = PositiveIntOrMinusOne() + self.assertIs( + standardizer_func(1.0), + 1, + msg=(f"{PositiveIntOrMinusOne.__name__} does not standardize as expected."), + ) + self.assertEqual( + standardizer_func(-1.00), + -1, + msg=(f"{PositiveIntOrMinusOne.__name__} does not standardize as expected."), + ) + + exc_str = r"Expected positive int or -1, but received value.*" + with self.assertRaisesRegex(ValueError, exc_str): + standardizer_func(1.5) + with self.assertRaisesRegex(ValueError, exc_str): + standardizer_func(0) + + +class TestLoggerType(unittest.TestCase): + """ + Test logger type validator. + """ + + def test_logger_type(self): + """ + Test logger type validator. + """ + standardizer_func = LoggerType() + mylogger = logging.getLogger("example") + self.assertIs( + standardizer_func(mylogger), + mylogger, + msg=f"{LoggerType.__name__} output not as expected", + ) + self.assertIs( + standardizer_func(mylogger.name), + mylogger, + msg=f"{LoggerType.__name__} output not as expected", + ) + + exc_str = r"A logger name must be a string" + with self.assertRaisesRegex(Exception, exc_str): + standardizer_func(2) + + +if __name__ == "__main__": + unittest.main() diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index fc215f86a7c..df3568e42a4 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -29,7 +29,6 @@ selective_clone, add_decision_rule_variables, add_decision_rule_constraints, - model_is_valid, turn_bounds_to_constraints, transform_to_standard_form, ObjectiveType, @@ -131,6 +130,9 @@ scip_license_is_valid = False scip_version = (0, 0, 0) +_ipopt = SolverFactory("ipopt") +ipopt_available = _ipopt.available(exception_flag=False) + # @SolverFactory.register("time_delay_solver") class TimeDelaySolver(object): @@ -148,7 +150,7 @@ def __init__(self, calls_to_sleep, max_time, sub_solver): self.num_calls = 0 self.options = Bunch() - def available(self): + def available(self, exception_flag=True): return True def license_is_valid(self): @@ -621,21 +623,6 @@ def test_dr_eqns_form_correct(self): ) -class testModelIsValid(unittest.TestCase): - def test_model_is_valid_via_possible_inputs(self): - m = ConcreteModel() - m.x = Var() - m.obj1 = Objective(expr=m.x**2) - self.assertTrue(model_is_valid(m)) - m.obj2 = Objective(expr=m.x) - self.assertFalse(model_is_valid(m)) - m.obj2.deactivate() - self.assertTrue(model_is_valid(m)) - m.del_component("obj1") - m.del_component("obj2") - self.assertFalse(model_is_valid(m)) - - class testTurnBoundsToConstraints(unittest.TestCase): def test_bounds_to_constraints(self): m = ConcreteModel() @@ -3560,10 +3547,7 @@ class behaves like a regular Python list. # assigning to slices should work fine all_sets[3:] = [BoxSet([[1, 1.5]]), BoxSet([[1, 3]])] - @unittest.skipUnless( - SolverFactory('ipopt').available(exception_flag=False), - "Local NLP solver is not available.", - ) + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") def test_uncertainty_set_with_correct_params(self): ''' Case in which the UncertaintySet is constructed using the uncertain_param objects from the model to @@ -3602,10 +3586,7 @@ def test_uncertainty_set_with_correct_params(self): " be the same uncertain param Var objects in the original model.", ) - @unittest.skipUnless( - SolverFactory('ipopt').available(exception_flag=False), - "Local NLP solver is not available.", - ) + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") def test_uncertainty_set_with_incorrect_params(self): ''' Case in which the set is constructed using uncertain_param objects which are Params instead of @@ -5417,16 +5398,14 @@ def test_multiple_objs(self): # check validation error raised due to multiple objectives with self.assertRaisesRegex( - AttributeError, - "This model structure is not currently handled by the ROSolver.", + ValueError, r"Expected model with exactly 1 active objective.*has 3" ): pyros_solver.solve(**solve_kwargs) # check validation error raised due to multiple objectives m.b.obj.deactivate() with self.assertRaisesRegex( - AttributeError, - "This model structure is not currently handled by the ROSolver.", + ValueError, r"Expected model with exactly 1 active objective.*has 2" ): pyros_solver.solve(**solve_kwargs) @@ -6313,5 +6292,598 @@ def test_log_disclaimer(self): ) +class UnavailableSolver: + def available(self, exception_flag=True): + if exception_flag: + raise ApplicationError(f"Solver {self.__class__} not available") + return False + + def solve(self, model, *args, **kwargs): + return SolverResults() + + +class TestPyROSUnavailableSubsolvers(unittest.TestCase): + """ + Check that appropriate exceptionsa are raised if + PyROS is invoked with unavailable subsolvers. + """ + + def test_pyros_unavailable_subsolver(self): + """ + Test PyROS raises expected error message when + unavailable subsolver is passed. + """ + m = ConcreteModel() + m.p = Param(range(3), initialize=0, mutable=True) + m.z = Var([0, 1], initialize=0) + m.con = Constraint(expr=m.z[0] + m.z[1] >= m.p[0]) + m.obj = Objective(expr=m.z[0] + m.z[1]) + + pyros_solver = SolverFactory("pyros") + + exc_str = r".*Solver.*UnavailableSolver.*not available" + with self.assertRaisesRegex(ValueError, exc_str): + # note: ConfigDict interface raises ValueError + # once any exception is triggered, + # so we check for that instead of ApplicationError + with LoggingIntercept(level=logging.ERROR) as LOG: + pyros_solver.solve( + model=m, + first_stage_variables=[m.z[0]], + second_stage_variables=[m.z[1]], + uncertain_params=[m.p[0]], + uncertainty_set=BoxSet([[0, 1]]), + local_solver=SimpleTestSolver(), + global_solver=UnavailableSolver(), + ) + + error_msgs = LOG.getvalue()[:-1] + self.assertRegex( + error_msgs, r"Output of `available\(\)` method.*global solver.*" + ) + + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") + def test_pyros_unavailable_backup_subsolver(self): + """ + Test PyROS raises expected error message when + unavailable backup subsolver is passed. + """ + m = ConcreteModel() + m.p = Param(range(3), initialize=0, mutable=True) + m.z = Var([0, 1], initialize=0) + m.con = Constraint(expr=m.z[0] + m.z[1] >= m.p[0]) + m.obj = Objective(expr=m.z[0] + m.z[1]) + + pyros_solver = SolverFactory("pyros") + + # note: ConfigDict interface raises ValueError + # once any exception is triggered, + # so we check for that instead of ApplicationError + with LoggingIntercept(level=logging.WARNING) as LOG: + pyros_solver.solve( + model=m, + first_stage_variables=[m.z[0]], + second_stage_variables=[m.z[1]], + uncertain_params=[m.p[0]], + uncertainty_set=BoxSet([[0, 1]]), + local_solver=SolverFactory("ipopt"), + global_solver=SolverFactory("ipopt"), + backup_global_solvers=[UnavailableSolver()], + bypass_global_separation=True, + ) + + error_msgs = LOG.getvalue()[:-1] + self.assertRegex( + error_msgs, + r"Output of `available\(\)` method.*backup global solver.*" + r"Removing from list.*", + ) + + +class TestPyROSResolveKwargs(unittest.TestCase): + """ + Test PyROS resolves kwargs as expected. + """ + + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") + @unittest.skipUnless( + baron_license_is_valid, "Global NLP solver is not available and licensed." + ) + def test_pyros_kwargs_with_overlap(self): + """ + Test PyROS works as expected when there is overlap between + keyword arguments passed explicitly and implicitly + through `options`. + """ + # define model + m = ConcreteModel() + m.x1 = Var(initialize=0, bounds=(0, None)) + m.x2 = Var(initialize=0, bounds=(0, None)) + m.x3 = Var(initialize=0, bounds=(None, None)) + m.u1 = Param(initialize=1.125, mutable=True) + m.u2 = Param(initialize=1, mutable=True) + + m.con1 = Constraint(expr=m.x1 * m.u1 ** (0.5) - m.x2 * m.u1 <= 2) + m.con2 = Constraint(expr=m.x1**2 - m.x2**2 * m.u1 == m.x3) + + m.obj = Objective(expr=(m.x1 - 4) ** 2 + (m.x2 - m.u2) ** 2) + + # Define the uncertainty set + # we take the parameter `u2` to be 'fixed' + ellipsoid = AxisAlignedEllipsoidalSet(center=[1.125, 1], half_lengths=[1, 0]) + + # Instantiate the PyROS solver + pyros_solver = SolverFactory("pyros") + + # Define subsolvers utilized in the algorithm + local_subsolver = SolverFactory('ipopt') + global_subsolver = SolverFactory("baron") + + # Call the PyROS solver + results = pyros_solver.solve( + model=m, + first_stage_variables=[m.x1, m.x2], + second_stage_variables=[], + uncertain_params=[m.u1, m.u2], + uncertainty_set=ellipsoid, + local_solver=local_subsolver, + global_solver=global_subsolver, + bypass_local_separation=True, + solve_master_globally=True, + options={ + "objective_focus": ObjectiveType.worst_case, + "solve_master_globally": False, + "max_iter": 1, + "time_limit": 1000, + }, + ) + + # check termination status as expected + self.assertEqual( + results.pyros_termination_condition, + pyrosTerminationCondition.max_iter, + msg="Termination condition not as expected", + ) + self.assertEqual( + results.iterations, 1, msg="Number of iterations not as expected" + ) + + # check config resolved as expected + config = results.config + self.assertEqual( + config.bypass_local_separation, + True, + msg="Resolved value of kwarg `bypass_local_separation` not as expected.", + ) + self.assertEqual( + config.solve_master_globally, + True, + msg="Resolved value of kwarg `solve_master_globally` not as expected.", + ) + self.assertEqual( + config.max_iter, + 1, + msg="Resolved value of kwarg `max_iter` not as expected.", + ) + self.assertEqual( + config.objective_focus, + ObjectiveType.worst_case, + msg="Resolved value of kwarg `objective_focus` not as expected.", + ) + self.assertEqual( + config.time_limit, + 1e3, + msg="Resolved value of kwarg `time_limit` not as expected.", + ) + + +class SimpleTestSolver: + """ + Simple test solver class with no actual solve() + functionality. Written to test unrelated aspects + of PyROS functionality. + """ + + def available(self, exception_flag=False): + """ + Check solver available. + """ + return True + + def solve(self, model, **kwds): + """ + Return SolverResults object with 'unknown' termination + condition. Model remains unchanged. + """ + res = SolverResults() + res.solver.termination_condition = TerminationCondition.unknown + + return res + + +class TestPyROSSolverAdvancedValidation(unittest.TestCase): + """ + Test PyROS solver returns expected exception messages + when arguments are invalid. + """ + + def build_simple_test_model(self): + """ + Build simple valid test model. + """ + m = ConcreteModel(name="test_model") + + m.x1 = Var(initialize=0, bounds=(0, None)) + m.x2 = Var(initialize=0, bounds=(0, None)) + m.u = Param(initialize=1.125, mutable=True) + + m.con1 = Constraint(expr=m.x1 * m.u ** (0.5) - m.x2 * m.u <= 2) + + m.obj = Objective(expr=(m.x1 - 4) ** 2 + (m.x2 - 1) ** 2) + + return m + + def test_pyros_invalid_model_type(self): + """ + Test PyROS fails if model is not of correct class. + """ + mdl = self.build_simple_test_model() + + local_solver = SimpleTestSolver() + global_solver = SimpleTestSolver() + + pyros = SolverFactory("pyros") + + exc_str = "Model should be of type.*but is of type.*" + with self.assertRaisesRegex(TypeError, exc_str): + pyros.solve( + model=2, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + ) + + def test_pyros_multiple_objectives(self): + """ + Test PyROS raises exception if input model has multiple + objectives. + """ + mdl = self.build_simple_test_model() + mdl.obj2 = Objective(expr=(mdl.x1 + mdl.x2)) + + local_solver = SimpleTestSolver() + global_solver = SimpleTestSolver() + + pyros = SolverFactory("pyros") + + exc_str = "Expected model with exactly 1 active.*but.*has 2" + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + ) + + def test_pyros_empty_dof_vars(self): + """ + Test PyROS solver raises exception raised if there are no + first-stage variables or second-stage variables. + """ + # build model + mdl = self.build_simple_test_model() + + # prepare solvers + pyros = SolverFactory("pyros") + local_solver = SimpleTestSolver() + global_solver = SimpleTestSolver() + + # perform checks + exc_str = ( + "Arguments `first_stage_variables` and " + "`second_stage_variables` are both empty lists." + ) + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[], + second_stage_variables=[], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + ) + + def test_pyros_overlap_dof_vars(self): + """ + Test PyROS solver raises exception raised if there are Vars + passed as both first-stage and second-stage. + """ + # build model + mdl = self.build_simple_test_model() + + # prepare solvers + pyros = SolverFactory("pyros") + local_solver = SimpleTestSolver() + global_solver = SimpleTestSolver() + + # perform checks + exc_str = ( + "Arguments `first_stage_variables` and `second_stage_variables` " + "contain at least one common Var object." + ) + with LoggingIntercept(level=logging.ERROR) as LOG: + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x1, mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + ) + + # check logger output is as expected + log_msgs = LOG.getvalue().split("\n")[:-1] + self.assertEqual( + len(log_msgs), 3, "Error message does not contain expected number of lines." + ) + self.assertRegex( + text=log_msgs[0], + expected_regex=( + "The following Vars were found in both `first_stage_variables`" + "and `second_stage_variables`.*" + ), + ) + self.assertRegex(text=log_msgs[1], expected_regex=" 'x1'") + self.assertRegex( + text=log_msgs[2], + expected_regex="Ensure no Vars are included in both arguments.", + ) + + def test_pyros_vars_not_in_model(self): + """ + Test PyROS appropriately raises exception if there are + variables not included in active model objective + or constraints which are not descended from model. + """ + # set up model + mdl = self.build_simple_test_model() + mdl.name = "model1" + mdl2 = self.build_simple_test_model() + mdl2.name = "model2" + + # set up solvers + local_solver = SimpleTestSolver() + global_solver = SimpleTestSolver() + pyros = SolverFactory("pyros") + + mdl.bad_con = Constraint(expr=mdl2.x1 + mdl2.x2 >= 1) + + desc_dof_map = [ + ("first-stage", [mdl2.x1], [], 2), + ("second-stage", [], [mdl2.x2], 2), + ("state", [mdl.x1], [], 3), + ] + + # now perform checks + for vardesc, first_stage_vars, second_stage_vars, numlines in desc_dof_map: + with LoggingIntercept(level=logging.ERROR) as LOG: + exc_str = ( + "Found entries of " + f"{vardesc} variables not descended from.*model.*" + ) + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=first_stage_vars, + second_stage_variables=second_stage_vars, + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + ) + + log_msgs = LOG.getvalue().split("\n")[:-1] + + # check detailed log message is as expected + self.assertEqual( + len(log_msgs), + numlines, + "Error-level log message does not contain expected number of lines.", + ) + self.assertRegex( + text=log_msgs[0], + expected_regex=( + f"The following {vardesc} variables" + ".*not descended from.*model with name 'model1'" + ), + ) + + def test_pyros_non_continuous_vars(self): + """ + Test PyROS raises exception if model contains + non-continuous variables. + """ + # build model; make one variable discrete + mdl = self.build_simple_test_model() + mdl.x2.domain = NonNegativeIntegers + + # prepare solvers + pyros = SolverFactory("pyros") + local_solver = SimpleTestSolver() + global_solver = SimpleTestSolver() + + # perform checks + exc_str = "Model with name 'test_model' contains non-continuous Vars." + with LoggingIntercept(level=logging.ERROR) as LOG: + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + ) + + # check logger output is as expected + log_msgs = LOG.getvalue().split("\n")[:-1] + self.assertEqual( + len(log_msgs), 3, "Error message does not contain expected number of lines." + ) + self.assertRegex( + text=log_msgs[0], + expected_regex=( + "The following Vars of model with name 'test_model' " + "are non-continuous:" + ), + ) + self.assertRegex(text=log_msgs[1], expected_regex=" 'x2'") + self.assertRegex( + text=log_msgs[2], + expected_regex=( + "Ensure all model variables passed to " "PyROS solver are continuous." + ), + ) + + def test_pyros_uncertainty_dimension_mismatch(self): + """ + Test PyROS solver raises exception if uncertainty + set dimension does not match the number + of uncertain parameters. + """ + # build model + mdl = self.build_simple_test_model() + + # prepare solvers + pyros = SolverFactory("pyros") + local_solver = SimpleTestSolver() + global_solver = SimpleTestSolver() + + # perform checks + exc_str = ( + r"Length of argument `uncertain_params` does not match dimension " + r"of argument `uncertainty_set` \(1 != 2\)." + ) + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2], [0, 1]]), + local_solver=local_solver, + global_solver=global_solver, + ) + + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") + def test_pyros_nominal_point_not_in_set(self): + """ + Test PyROS raises exception if nominal point is not in the + uncertainty set. + + NOTE: need executable solvers to solve set bounding problems + for validity checks. + """ + # build model + mdl = self.build_simple_test_model() + + # prepare solvers + pyros = SolverFactory("pyros") + local_solver = SolverFactory("ipopt") + global_solver = SolverFactory("ipopt") + + # perform checks + exc_str = ( + r"Nominal uncertain parameter realization \[0\] " + "is not a point in the uncertainty set.*" + ) + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + nominal_uncertain_param_vals=[0], + ) + + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") + def test_pyros_nominal_point_len_mismatch(self): + """ + Test PyROS raises exception if there is mismatch between length + of nominal uncertain parameter specification and number + of uncertain parameters. + """ + # build model + mdl = self.build_simple_test_model() + + # prepare solvers + pyros = SolverFactory("pyros") + local_solver = SolverFactory("ipopt") + global_solver = SolverFactory("ipopt") + + # perform checks + exc_str = ( + r"Lengths of arguments `uncertain_params` " + r"and `nominal_uncertain_param_vals` " + r"do not match \(1 != 2\)." + ) + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + nominal_uncertain_param_vals=[0, 1], + ) + + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") + def test_pyros_invalid_bypass_separation(self): + """ + Test PyROS raises exception if both local and + global separation are set to be bypassed. + """ + # build model + mdl = self.build_simple_test_model() + + # prepare solvers + pyros = SolverFactory("pyros") + local_solver = SolverFactory("ipopt") + global_solver = SolverFactory("ipopt") + + # perform checks + exc_str = ( + r"Arguments `bypass_local_separation` and `bypass_global_separation` " + r"cannot both be True." + ) + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + bypass_local_separation=True, + bypass_global_separation=True, + ) + + if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 17b51be709b..028a9f38da1 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -283,14 +283,6 @@ def generate_shape_str(shape, required_shape): ) -def uncertainty_sets(obj): - if not isinstance(obj, UncertaintySet): - raise ValueError( - "Expected an UncertaintySet object, instead received %s" % (obj,) - ) - return obj - - def column(matrix, i): # Get column i of a given multi-dimensional list return [row[i] for row in matrix] diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 97fa42c32e9..a3ab3464aa8 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -445,51 +445,6 @@ def setup_pyros_logger(name=DEFAULT_LOGGER_NAME): return logger -def a_logger(str_or_logger): - """ - Standardize a string or logger object to a logger object. - - Parameters - ---------- - str_or_logger : str or logging.Logger - String or logger object to normalize. - - Returns - ------- - logging.Logger - If `str_or_logger` is of type `logging.Logger`,then - `str_or_logger` is returned. - Otherwise, ``logging.getLogger(str_or_logger)`` - is returned. In the event `str_or_logger` is - the name of the default PyROS logger, the logger level - is set to `logging.INFO`, and a `PreformattedLogger` - instance is returned in lieu of a standard `Logger` - instance. - """ - if isinstance(str_or_logger, logging.Logger): - return logging.getLogger(str_or_logger.name) - else: - return logging.getLogger(str_or_logger) - - -def ValidEnum(enum_class): - ''' - Python 3 dependent format string - ''' - - def fcn(obj): - if obj not in enum_class: - raise ValueError( - "Expected an {0} object, " - "instead received {1}".format( - enum_class.__name__, obj.__class__.__name__ - ) - ) - return obj - - return fcn - - class pyrosTerminationCondition(Enum): """Enumeration of all possible PyROS termination conditions.""" @@ -568,14 +523,6 @@ def recast_to_min_obj(model, obj): obj.sense = minimize -def model_is_valid(model): - """ - Assess whether model is valid on basis of the number of active - Objectives. A valid model must contain exactly one active Objective. - """ - return len(list(model.component_data_objects(Objective, active=True))) == 1 - - def turn_bounds_to_constraints(variable, model, config=None): ''' Turn the variable in question's "bounds" into direct inequality constraints on the model. @@ -659,41 +606,6 @@ def get_time_from_solver(results): return float("nan") if solve_time is None else solve_time -def validate_uncertainty_set(config): - ''' - Confirm expression output from uncertainty set function references all q in q. - Typecheck the uncertainty_set.q is Params referenced inside of m. - Give warning that the nominal point (default value in the model) is not in the specified uncertainty set. - :param config: solver config - ''' - # === Check that q in UncertaintySet object constraint expression is referencing q in model.uncertain_params - uncertain_params = config.uncertain_params - - # === Non-zero number of uncertain parameters - if len(uncertain_params) == 0: - raise AttributeError( - "Must provide uncertain params, uncertain_params list length is 0." - ) - # === No duplicate parameters - if len(uncertain_params) != len(ComponentSet(uncertain_params)): - raise AttributeError("No duplicates allowed for uncertain param objects.") - # === Ensure nominal point is in the set - if not config.uncertainty_set.point_in_set( - point=config.nominal_uncertain_param_vals - ): - raise AttributeError( - "Nominal point for uncertain parameters must be in the uncertainty set." - ) - # === Check set validity via boundedness and non-emptiness - if not config.uncertainty_set.is_valid(config=config): - raise AttributeError( - "Invalid uncertainty set detected. Check the uncertainty set object to " - "ensure non-emptiness and boundedness." - ) - - return - - def add_bounds_for_uncertain_parameters(model, config): ''' This function solves a set of optimization problems to determine bounds on the uncertain parameters @@ -873,98 +785,345 @@ def replace_uncertain_bounds_with_constraints(model, uncertain_params): v.setlb(None) -def validate_kwarg_inputs(model, config): - ''' - Confirm kwarg inputs satisfy PyROS requirements. - :param model: the deterministic model - :param config: the config for this PyROS instance - :return: - ''' - - # === Check if model is ConcreteModel object - if not isinstance(model, ConcreteModel): - raise ValueError("Model passed to PyROS solver must be a ConcreteModel object.") +def check_components_descended_from_model(model, components, components_name, config): + """ + Check all members in a provided sequence of Pyomo component + objects are descended from a given ConcreteModel object. - first_stage_variables = config.first_stage_variables - second_stage_variables = config.second_stage_variables - uncertain_params = config.uncertain_params + Parameters + ---------- + model : ConcreteModel + Model from which components should all be descended. + components : Iterable of Component + Components of interest. + components_name : str + Brief description or name for the sequence of components. + Used for constructing error messages. + config : ConfigDict + PyROS solver options. - if not config.first_stage_variables and not config.second_stage_variables: - # Must have non-zero DOF + Raises + ------ + ValueError + If at least one entry of `components` is not descended + from `model`. + """ + components_not_in_model = [comp for comp in components if comp.model() is not model] + if components_not_in_model: + comp_names_str = "\n ".join( + f"{comp.name!r}, from model with name {comp.model().name!r}" + for comp in components_not_in_model + ) + config.progress_logger.error( + f"The following {components_name} " + "are not descended from the " + f"input deterministic model with name {model.name!r}:\n " + f"{comp_names_str}" + ) raise ValueError( - "first_stage_variables and " - "second_stage_variables cannot both be empty lists." + f"Found entries of {components_name} " + "not descended from input model. " + "Check logger output messages." ) - if ComponentSet(first_stage_variables) != ComponentSet( - config.first_stage_variables - ): + +def get_state_vars(blk, first_stage_variables, second_stage_variables): + """ + Get state variables of a modeling block. + + The state variables with respect to `blk` are the unfixed + `_VarData` objects participating in the active objective + or constraints descended from `blk` which are not + first-stage variables or second-stage variables. + + Parameters + ---------- + blk : ScalarBlock + Block of interest. + first_stage_variables : Iterable of VarData + First-stage variables. + second_stage_variables : Iterable of VarData + Second-stage variables. + + Yields + ------ + _VarData + State variable. + """ + dof_var_set = ComponentSet(first_stage_variables) | ComponentSet( + second_stage_variables + ) + for var in get_vars_from_component(blk, (Objective, Constraint)): + is_state_var = not var.fixed and var not in dof_var_set + if is_state_var: + yield var + + +def check_variables_continuous(model, vars, config): + """ + Check that all DOF and state variables of the model + are continuous. + + Parameters + ---------- + model : ConcreteModel + Input deterministic model. + config : ConfigDict + PyROS solver options. + + Raises + ------ + ValueError + If at least one variable is found to not be continuous. + + Note + ---- + A variable is considered continuous if the `is_continuous()` + method returns True. + """ + non_continuous_vars = [var for var in vars if not var.is_continuous()] + if non_continuous_vars: + non_continuous_vars_str = "\n ".join( + f"{var.name!r}" for var in non_continuous_vars + ) + config.progress_logger.error( + f"The following Vars of model with name {model.name!r} " + f"are non-continuous:\n {non_continuous_vars_str}\n" + "Ensure all model variables passed to PyROS solver are continuous." + ) raise ValueError( - "All elements in first_stage_variables must be Var members of the model object." + f"Model with name {model.name!r} contains non-continuous Vars." ) - if ComponentSet(second_stage_variables) != ComponentSet( - config.second_stage_variables - ): + +def validate_model(model, config): + """ + Validate deterministic model passed to PyROS solver. + + Parameters + ---------- + model : ConcreteModel + Deterministic model. Should have only one active Objective. + config : ConfigDict + PyROS solver options. + + Returns + ------- + ComponentSet + The variables participating in the active Objective + and Constraint expressions of `model`. + + Raises + ------ + TypeError + If model is not of type ConcreteModel. + ValueError + If model does not have exactly one active Objective + component. + """ + # note: only support ConcreteModel. no support for Blocks + if not isinstance(model, ConcreteModel): + raise TypeError( + f"Model should be of type {ConcreteModel.__name__}, " + f"but is of type {type(model).__name__}." + ) + + # active objectives check + active_objs_list = list( + model.component_data_objects(Objective, active=True, descend_into=True) + ) + if len(active_objs_list) != 1: raise ValueError( - "All elements in second_stage_variables must be Var members of the model object." + "Expected model with exactly 1 active objective, but " + f"model provided has {len(active_objs_list)}." ) - if any( - v in ComponentSet(second_stage_variables) - for v in ComponentSet(first_stage_variables) - ): + +def validate_variable_partitioning(model, config): + """ + Check that partitioning of the first-stage variables, + second-stage variables, and uncertain parameters + is valid. + + Parameters + ---------- + model : ConcreteModel + Input deterministic model. + config : ConfigDict + PyROS solver options. + + Returns + ------- + list of _VarData + State variables of the model. + + Raises + ------ + ValueError + If first-stage variables and second-stage variables + overlap, or there are no first-stage variables + and no second-stage variables. + """ + # at least one DOF required + if not config.first_stage_variables and not config.second_stage_variables: raise ValueError( - "No common elements allowed between first_stage_variables and second_stage_variables." + "Arguments `first_stage_variables` and " + "`second_stage_variables` are both empty lists." ) - if ComponentSet(uncertain_params) != ComponentSet(config.uncertain_params): + # ensure no overlap between DOF var sets + overlapping_vars = ComponentSet(config.first_stage_variables) & ComponentSet( + config.second_stage_variables + ) + if overlapping_vars: + overlapping_var_list = "\n ".join(f"{var.name!r}" for var in overlapping_vars) + config.progress_logger.error( + "The following Vars were found in both `first_stage_variables`" + f"and `second_stage_variables`:\n {overlapping_var_list}" + "\nEnsure no Vars are included in both arguments." + ) raise ValueError( - "uncertain_params must be mutable Param members of the model object." + "Arguments `first_stage_variables` and `second_stage_variables` " + "contain at least one common Var object." + ) + + state_vars = list( + get_state_vars( + model, + first_stage_variables=config.first_stage_variables, + second_stage_variables=config.second_stage_variables, + ) + ) + var_type_list_map = { + "first-stage variables": config.first_stage_variables, + "second-stage variables": config.second_stage_variables, + "state variables": state_vars, + } + for desc, vars in var_type_list_map.items(): + check_components_descended_from_model( + model=model, components=vars, components_name=desc, config=config ) - if not config.uncertainty_set: + all_vars = config.first_stage_variables + config.second_stage_variables + state_vars + check_variables_continuous(model, all_vars, config) + + return state_vars + + +def validate_uncertainty_specification(model, config): + """ + Validate specification of uncertain parameters and uncertainty + set. + + Parameters + ---------- + model : ConcreteModel + Input deterministic model. + config : ConfigDict + PyROS solver options. + + Raises + ------ + ValueError + If at least one of the following holds: + + - dimension of uncertainty set does not equal number of + uncertain parameters + - uncertainty set `is_valid()` method does not return + true. + - nominal parameter realization is not in the uncertainty set. + """ + check_components_descended_from_model( + model=model, + components=config.uncertain_params, + components_name="uncertain parameters", + config=config, + ) + + if len(config.uncertain_params) != config.uncertainty_set.dim: raise ValueError( - "An UncertaintySet object must be provided to the PyROS solver." + "Length of argument `uncertain_params` does not match dimension " + "of argument `uncertainty_set` " + f"({len(config.uncertain_params)} != {config.uncertainty_set.dim})." ) - non_mutable_params = [] - for p in config.uncertain_params: - if not ( - not p.is_constant() and p.is_fixed() and not p.is_potentially_variable() - ): - non_mutable_params.append(p) - if non_mutable_params: - raise ValueError( - "Param objects which are uncertain must have attribute mutable=True. " - "Offending Params: %s" % [p.name for p in non_mutable_params] - ) + # validate uncertainty set + if not config.uncertainty_set.is_valid(config=config): + raise ValueError( + f"Uncertainty set {config.uncertainty_set} is invalid, " + "as it is either empty or unbounded." + ) - # === Solvers provided check - if not config.local_solver or not config.global_solver: + # fill-in nominal point as necessary, if not provided. + # otherwise, check length matches uncertainty dimension + if not config.nominal_uncertain_param_vals: + config.nominal_uncertain_param_vals = [ + value(param, exception=True) for param in config.uncertain_params + ] + elif len(config.nominal_uncertain_param_vals) != len(config.uncertain_params): raise ValueError( - "User must designate both a local and global optimization solver via the local_solver" - " and global_solver options." + "Lengths of arguments `uncertain_params` and " + "`nominal_uncertain_param_vals` " + "do not match " + f"({len(config.uncertain_params)} != " + f"{len(config.nominal_uncertain_param_vals)})." ) - if config.bypass_local_separation and config.bypass_global_separation: + # uncertainty set should contain nominal point + nominal_point_in_set = config.uncertainty_set.point_in_set( + point=config.nominal_uncertain_param_vals + ) + if not nominal_point_in_set: raise ValueError( - "User cannot simultaneously enable options " - "'bypass_local_separation' and " - "'bypass_global_separation'." + "Nominal uncertain parameter realization " + f"{config.nominal_uncertain_param_vals} " + "is not a point in the uncertainty set " + f"{config.uncertainty_set!r}." ) - # === Degrees of freedom provided check - if len(config.first_stage_variables) + len(config.second_stage_variables) == 0: + +def validate_separation_problem_options(model, config): + """ + Validate separation problem arguments to the PyROS solver. + + Parameters + ---------- + model : ConcreteModel + Input deterministic model. + config : ConfigDict + PyROS solver options. + + Raises + ------ + ValueError + If options `bypass_local_separation` and + `bypass_global_separation` are set to False. + """ + if config.bypass_local_separation and config.bypass_global_separation: raise ValueError( - "User must designate at least one first- and/or second-stage variable." + "Arguments `bypass_local_separation` " + "and `bypass_global_separation` " + "cannot both be True." ) - # === Uncertain params provided check - if len(config.uncertain_params) == 0: - raise ValueError("User must designate at least one uncertain parameter.") - return +def validate_pyros_inputs(model, config): + """ + Perform advanced validation of PyROS solver arguments. + + Parameters + ---------- + model : ConcreteModel + Input deterministic model. + config : ConfigDict + PyROS solver options. + """ + validate_model(model, config) + state_vars = validate_variable_partitioning(model, config) + validate_uncertainty_specification(model, config) + validate_separation_problem_options(model, config) + + return state_vars def substitute_ssv_in_dr_constraints(model, constraint): diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 48353078fca..a0948c693d7 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -14,13 +14,13 @@ import sys import weakref import textwrap -from contextlib import contextmanager +from collections import defaultdict +from contextlib import contextmanager from inspect import isclass, currentframe +from io import StringIO from itertools import filterfalse, chain from operator import itemgetter, attrgetter -from io import StringIO -from pyomo.common.pyomo_typing import overload from pyomo.common.autoslots import AutoSlots from pyomo.common.collections import Mapping @@ -28,6 +28,7 @@ from pyomo.common.formatting import StreamIndenter from pyomo.common.gc_manager import PauseGC from pyomo.common.log import is_debug_set +from pyomo.common.pyomo_typing import overload from pyomo.common.timing import ConstructionTimer from pyomo.core.base.component import ( Component, @@ -1986,7 +1987,7 @@ def private_data(self, scope=None): if self._private_data is None: self._private_data = {} if scope not in self._private_data: - self._private_data[scope] = {} + self._private_data[scope] = Block._private_data_initializers[scope]() return self._private_data[scope] @@ -2004,6 +2005,7 @@ class Block(ActiveIndexedComponent): """ _ComponentDataClass = _BlockData + _private_data_initializers = defaultdict(lambda: dict) def __new__(cls, *args, **kwds): if cls != Block: @@ -2207,6 +2209,23 @@ def display(self, filename=None, ostream=None, prefix=""): for key in sorted(self): _BlockData.display(self[key], filename, ostream, prefix) + @staticmethod + def register_private_data_initializer(initializer, scope=None): + mod = currentframe().f_back.f_globals['__name__'] + if scope is None: + scope = mod + elif not mod.startswith(scope): + raise ValueError( + "'private_data' scope must be substrings of the caller's module name. " + f"Received '{scope}' when calling register_private_data_initializer()." + ) + if scope in Block._private_data_initializers: + raise RuntimeError( + "Duplicate initializer registration for 'private_data' dictionary " + f"(scope={scope})" + ) + Block._private_data_initializers[scope] = initializer + class ScalarBlock(_BlockData, Block): def __init__(self, *args, **kwds): diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index c67236656be..8cf3c48ad0a 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -27,6 +27,7 @@ as_numeric, is_fixed, native_numeric_types, + native_logical_types, native_types, ) from pyomo.core.expr import ( @@ -84,14 +85,15 @@ def C_rule(model, i, j): model.c = Constraint(rule=simple_constraint_rule(...)) """ - return rule_wrapper( - rule, - { - None: Constraint.Skip, - True: Constraint.Feasible, - False: Constraint.Infeasible, - }, - ) + map_types = set([type(None)]) | native_logical_types + result_map = {None: Constraint.Skip} + for l_type in native_logical_types: + result_map[l_type(True)] = Constraint.Feasible + result_map[l_type(False)] = Constraint.Infeasible + # Note: some logical types hash the same as bool (e.g., np.bool_), so + # we will pass the set of all logical types in addition to the + # result_map + return rule_wrapper(rule, result_map, map_types=map_types) def simple_constraintlist_rule(rule): @@ -109,14 +111,15 @@ def C_rule(model, i, j): model.c = ConstraintList(expr=simple_constraintlist_rule(...)) """ - return rule_wrapper( - rule, - { - None: ConstraintList.End, - True: Constraint.Feasible, - False: Constraint.Infeasible, - }, - ) + map_types = set([type(None)]) | native_logical_types + result_map = {None: ConstraintList.End} + for l_type in native_logical_types: + result_map[l_type(True)] = Constraint.Feasible + result_map[l_type(False)] = Constraint.Infeasible + # Note: some logical types hash the same as bool (e.g., np.bool_), so + # we will pass the set of all logical types in addition to the + # result_map + return rule_wrapper(rule, result_map, map_types=map_types) # diff --git a/pyomo/core/base/indexed_component.py b/pyomo/core/base/indexed_component.py index abb29580960..0d498da091d 100644 --- a/pyomo/core/base/indexed_component.py +++ b/pyomo/core/base/indexed_component.py @@ -160,9 +160,12 @@ def _get_indexed_component_data_name(component, index): """ -def rule_result_substituter(result_map): +def rule_result_substituter(result_map, map_types): _map = result_map - _map_types = set(type(key) for key in result_map) + if map_types is None: + _map_types = set(type(key) for key in result_map) + else: + _map_types = map_types def rule_result_substituter_impl(rule, *args, **kwargs): if rule.__class__ in _map_types: @@ -203,7 +206,7 @@ def rule_result_substituter_impl(rule, *args, **kwargs): """ -def rule_wrapper(rule, wrapping_fcn, positional_arg_map=None): +def rule_wrapper(rule, wrapping_fcn, positional_arg_map=None, map_types=None): """Wrap a rule with another function This utility method provides a way to wrap a function (rule) with @@ -230,7 +233,7 @@ def rule_wrapper(rule, wrapping_fcn, positional_arg_map=None): """ if isinstance(wrapping_fcn, dict): - wrapping_fcn = rule_result_substituter(wrapping_fcn) + wrapping_fcn = rule_result_substituter(wrapping_fcn, map_types) if not inspect.isfunction(rule): return wrapping_fcn(rule) # Because some of our processing of initializer functions relies on diff --git a/pyomo/core/base/param.py b/pyomo/core/base/param.py index 03d700140e8..3ef33b9ee45 100644 --- a/pyomo/core/base/param.py +++ b/pyomo/core/base/param.py @@ -162,16 +162,31 @@ def set_value(self, value, idx=NOTSET): # required to be mutable. # _comp = self.parent_component() - if type(value) in native_types: + if value.__class__ in native_types: # TODO: warn/error: check if this Param has units: assigning # a dimensionless value to a united param should be an error pass elif _comp._units is not None: _src_magnitude = expr_value(value) - _src_units = units.get_units(value) - value = units.convert_value( - num_value=_src_magnitude, from_units=_src_units, to_units=_comp._units - ) + # Note: expr_value() could have just registered a new numeric type + if value.__class__ in native_types: + value = _src_magnitude + else: + _src_units = units.get_units(value) + value = units.convert_value( + num_value=_src_magnitude, + from_units=_src_units, + to_units=_comp._units, + ) + # FIXME: we should call value() here [to ensure types get + # registered], but doing so breaks non-numeric Params (which we + # allow). The real fix will be to follow the precedent from + # GetItemExpression and have separate types based on which + # expression "system" the Param should participate in (numeric, + # logical, or structural). + # + # else: + # value = expr_value(value) old_value, self._value = self._value, value try: diff --git a/pyomo/core/base/var.py b/pyomo/core/base/var.py index d03fd0b677f..f426c9c4f55 100644 --- a/pyomo/core/base/var.py +++ b/pyomo/core/base/var.py @@ -384,17 +384,22 @@ def set_value(self, val, skip_validation=False): # # Check if this Var has units: assigning dimensionless # values to a variable with units should be an error - if type(val) not in native_numeric_types: - if self.parent_component()._units is not None: - _src_magnitude = value(val) + if val.__class__ in native_numeric_types: + pass + elif self.parent_component()._units is not None: + _src_magnitude = value(val) + # Note: value() could have just registered a new numeric type + if val.__class__ in native_numeric_types: + val = _src_magnitude + else: _src_units = units.get_units(val) val = units.convert_value( num_value=_src_magnitude, from_units=_src_units, to_units=self.parent_component()._units, ) - else: - val = value(val) + else: + val = value(val) if not skip_validation: if val not in self.domain: diff --git a/pyomo/core/tests/unit/test_block.py b/pyomo/core/tests/unit/test_block.py index c9c68a820f7..88646643703 100644 --- a/pyomo/core/tests/unit/test_block.py +++ b/pyomo/core/tests/unit/test_block.py @@ -3437,6 +3437,64 @@ def test_private_data(self): mfe4 = m.b.b[1].private_data('pyomo.core.tests') self.assertIs(mfe4, mfe3) + def test_register_private_data(self): + _save = Block._private_data_initializers + + Block._private_data_initializers = pdi = _save.copy() + pdi.clear() + try: + self.assertEqual(len(pdi), 0) + b = Block(concrete=True) + ps = b.private_data() + self.assertEqual(ps, {}) + self.assertEqual(len(pdi), 1) + finally: + Block._private_data_initializers = _save + + def init(): + return {'a': None, 'b': 1} + + Block._private_data_initializers = pdi = _save.copy() + pdi.clear() + try: + self.assertEqual(len(pdi), 0) + Block.register_private_data_initializer(init) + self.assertEqual(len(pdi), 1) + + b = Block(concrete=True) + ps = b.private_data() + self.assertEqual(ps, {'a': None, 'b': 1}) + self.assertEqual(len(pdi), 1) + finally: + Block._private_data_initializers = _save + + Block._private_data_initializers = pdi = _save.copy() + pdi.clear() + try: + Block.register_private_data_initializer(init) + self.assertEqual(len(pdi), 1) + Block.register_private_data_initializer(init, 'pyomo') + self.assertEqual(len(pdi), 2) + + with self.assertRaisesRegex( + RuntimeError, + r"Duplicate initializer registration for 'private_data' " + r"dictionary \(scope=pyomo.core.tests.unit.test_block\)", + ): + Block.register_private_data_initializer(init) + + with self.assertRaisesRegex( + ValueError, + r"'private_data' scope must be substrings of the caller's " + r"module name. Received 'invalid' when calling " + r"register_private_data_initializer\(\).", + ): + Block.register_private_data_initializer(init, 'invalid') + + self.assertEqual(len(pdi), 2) + finally: + Block._private_data_initializers = _save + if __name__ == "__main__": unittest.main() diff --git a/pyomo/core/tests/unit/test_numvalue.py b/pyomo/core/tests/unit/test_numvalue.py index eceab3a42d9..bd784d655e8 100644 --- a/pyomo/core/tests/unit/test_numvalue.py +++ b/pyomo/core/tests/unit/test_numvalue.py @@ -562,7 +562,8 @@ def test_numpy_basic_bool_registration(self): @unittest.skipUnless(numpy_available, "This test requires NumPy") def test_automatic_numpy_registration(self): cmd = ( - 'import pyomo; from pyomo.core.base import Var, Param; import numpy as np; ' + 'import pyomo; from pyomo.core.base import Var, Param; ' + 'from pyomo.core.base.units_container import units; import numpy as np; ' 'print(np.float64 in pyomo.common.numeric_types.native_numeric_types); ' '%s; print(np.float64 in pyomo.common.numeric_types.native_numeric_types)' ) @@ -582,6 +583,11 @@ def _tester(expr): _tester('Var() + np.float64(5)') _tester('v = Var(); v.construct(); v.value = np.float64(5)') _tester('p = Param(mutable=True); p.construct(); p.value = np.float64(5)') + _tester('v = Var(units=units.m); v.construct(); v.value = np.float64(5)') + _tester( + 'p = Param(mutable=True, units=units.m); p.construct(); ' + 'p.value = np.float64(5)' + ) if __name__ == "__main__": diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index bdd353a6136..1f9f561b192 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -224,11 +224,10 @@ def _transform_disjunctionData( or_expr += disjunct.binary_indicator_var self._transform_disjunct(disjunct, bigM, transBlock) - rhs = 1 if parent_disjunct is None else parent_disjunct.binary_indicator_var if obj.xor: - xorConstraint[index] = or_expr == rhs + xorConstraint[index] = or_expr == 1 else: - xorConstraint[index] = or_expr >= rhs + xorConstraint[index] = or_expr >= 1 # Mark the DisjunctionData as transformed by mapping it to its XOR # constraint. obj._algebraic_constraint = weakref_ref(xorConstraint[index]) diff --git a/pyomo/gdp/plugins/binary_multiplication.py b/pyomo/gdp/plugins/binary_multiplication.py index ef4239e09dc..d68f7efe76f 100644 --- a/pyomo/gdp/plugins/binary_multiplication.py +++ b/pyomo/gdp/plugins/binary_multiplication.py @@ -92,11 +92,10 @@ def _transform_disjunctionData( or_expr += disjunct.binary_indicator_var self._transform_disjunct(disjunct, transBlock) - rhs = 1 if parent_disjunct is None else parent_disjunct.binary_indicator_var if obj.xor: - xorConstraint[index] = or_expr == rhs + xorConstraint[index] = or_expr == 1 else: - xorConstraint[index] = or_expr >= rhs + xorConstraint[index] = or_expr >= 1 # Mark the DisjunctionData as transformed by mapping it to its XOR # constraint. obj._algebraic_constraint = weakref_ref(xorConstraint[index]) diff --git a/pyomo/gdp/plugins/gdp_to_mip_transformation.py b/pyomo/gdp/plugins/gdp_to_mip_transformation.py index 96d97206c97..59cb221321a 100644 --- a/pyomo/gdp/plugins/gdp_to_mip_transformation.py +++ b/pyomo/gdp/plugins/gdp_to_mip_transformation.py @@ -213,21 +213,26 @@ def _setup_transform_disjunctionData(self, obj, root_disjunct): "likely indicative of a modeling error." % obj.name ) - # Create or fetch the transformation block + # We always need to create or fetch a transformation block on the parent block. + trans_block, new_block = self._add_transformation_block(obj.parent_block()) + # This is where we put exactly_one/or constraint + algebraic_constraint = self._add_xor_constraint( + obj.parent_component(), trans_block + ) + + # If requested, create or fetch the transformation block above the + # nested hierarchy if root_disjunct is not None: - # We want to put all the transformed things on the root - # Disjunct's parent's block so that they do not get - # re-transformed - transBlock, new_block = self._add_transformation_block( + # We want to put some transformed things on the root Disjunct's + # parent's block so that they do not get re-transformed. (Note this + # is never true for hull, but it calls this method with + # root_disjunct=None. BigM can't put the exactly-one constraint up + # here, but it can put everything else.) + trans_block, new_block = self._add_transformation_block( root_disjunct.parent_block() ) - else: - # This isn't nested--just put it on the parent block. - transBlock, new_block = self._add_transformation_block(obj.parent_block()) - - xorConstraint = self._add_xor_constraint(obj.parent_component(), transBlock) - return transBlock, xorConstraint + return trans_block, algebraic_constraint def _get_disjunct_transformation_block(self, disjunct, transBlock): if disjunct.transformation_block is not None: diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 630560b57f0..d1c38bde039 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -11,9 +11,12 @@ import logging +from collections import defaultdict + +from pyomo.common.autoslots import AutoSlots import pyomo.common.config as cfg from pyomo.common import deprecated -from pyomo.common.collections import ComponentMap, ComponentSet +from pyomo.common.collections import ComponentMap, ComponentSet, DefaultComponentMap from pyomo.common.modeling import unique_component_name from pyomo.core.expr.numvalue import ZeroConstant import pyomo.core.expr as EXPR @@ -39,6 +42,7 @@ Binary, ) from pyomo.gdp import Disjunct, Disjunction, GDP_Error +from pyomo.gdp.disjunct import _DisjunctData from pyomo.gdp.plugins.gdp_to_mip_transformation import GDP_to_MIP_Transformation from pyomo.gdp.transformed_disjunct import _TransformedDisjunct from pyomo.gdp.util import ( @@ -47,11 +51,24 @@ _warn_for_active_disjunct, ) from pyomo.core.util import target_list +from pyomo.util.vars_from_expressions import get_vars_from_components from weakref import ref as weakref_ref logger = logging.getLogger('pyomo.gdp.hull') +class _HullTransformationData(AutoSlots.Mixin): + __slots__ = ('disaggregated_var_map', 'original_var_map', 'bigm_constraint_map') + + def __init__(self): + self.disaggregated_var_map = DefaultComponentMap(ComponentMap) + self.original_var_map = ComponentMap() + self.bigm_constraint_map = DefaultComponentMap(ComponentMap) + + +Block.register_private_data_initializer(_HullTransformationData) + + @TransformationFactory.register( 'gdp.hull', doc="Relax disjunctive model by forming the hull reformulation." ) @@ -76,19 +93,11 @@ class Hull_Reformulation(GDP_to_MIP_Transformation): list of blocks and Disjunctions [default: the instance] The transformation will create a new Block with a unique - name beginning "_pyomo_gdp_hull_reformulation". - The block will have a dictionary "_disaggregatedVarMap: - 'srcVar': ComponentMap(:), - 'disaggregatedVar': ComponentMap(:) - - It will also have a ComponentMap "_bigMConstraintMap": - - : - - Last, it will contain an indexed Block named "relaxedDisjuncts", - which will hold the relaxed disjuncts. This block is indexed by - an integer indicating the order in which the disjuncts were relaxed. - Each block has a dictionary "_constraintMap": + name beginning "_pyomo_gdp_hull_reformulation". It will contain an + indexed Block named "relaxedDisjuncts" that will hold the relaxed + disjuncts. This block is indexed by an integer indicating the order + in which the disjuncts were relaxed. Each block has a dictionary + "_constraintMap": 'srcConstraints': ComponentMap(: ), @@ -104,7 +113,6 @@ class Hull_Reformulation(GDP_to_MIP_Transformation): The _pyomo_gdp_hull_reformulation block will have a ComponentMap "_disaggregationConstraintMap": :ComponentMap(: ) - """ CONFIG = cfg.ConfigDict('gdp.hull') @@ -204,33 +212,40 @@ def __init__(self): super().__init__(logger) self._targets = set() - def _add_local_vars(self, block, local_var_dict): + def _collect_local_vars_from_block(self, block, local_var_dict): localVars = block.component('LocalVars') - if type(localVars) is Suffix: + if localVars is not None and localVars.ctype is Suffix: for disj, var_list in localVars.items(): - if local_var_dict.get(disj) is None: - local_var_dict[disj] = ComponentSet(var_list) - else: - local_var_dict[disj].update(var_list) - - def _get_local_var_suffixes(self, block, local_var_dict): - # You can specify suffixes on any block (disjuncts included). This - # method starts from a Disjunct (presumably) and checks for a LocalVar - # suffixes going both up and down the tree, adding them into the - # dictionary that is the second argument. - - # first look beneath where we are (there could be Blocks on this - # disjunct) - for b in block.component_data_objects( - Block, descend_into=(Block), active=True, sort=SortComponents.deterministic - ): - self._add_local_vars(b, local_var_dict) - # now traverse upwards and get what's above - while block is not None: - self._add_local_vars(block, local_var_dict) - block = block.parent_block() - - return local_var_dict + local_var_dict[disj].update(var_list) + + def _get_user_defined_local_vars(self, targets): + user_defined_local_vars = defaultdict(ComponentSet) + seen_blocks = set() + # we go through the targets looking both up and down the hierarchy, but + # we cache what Blocks/Disjuncts we've already looked on so that we + # don't duplicate effort. + for t in targets: + if t.ctype is Disjunct: + # first look beneath where we are (there could be Blocks on this + # disjunct) + for b in t.component_data_objects( + Block, + descend_into=Block, + active=True, + sort=SortComponents.deterministic, + ): + if b not in seen_blocks: + self._collect_local_vars_from_block(b, user_defined_local_vars) + seen_blocks.add(b) + # now look up in the tree + blk = t + while blk is not None: + if blk in seen_blocks: + break + self._collect_local_vars_from_block(blk, user_defined_local_vars) + seen_blocks.add(blk) + blk = blk.parent_block() + return user_defined_local_vars def _apply_to(self, instance, **kwds): try: @@ -239,7 +254,6 @@ def _apply_to(self, instance, **kwds): self._restore_state() self._transformation_blocks.clear() self._algebraic_constraints.clear() - self._targets_set = set() def _apply_to_impl(self, instance, **kwds): self._process_arguments(instance, **kwds) @@ -253,16 +267,17 @@ def _apply_to_impl(self, instance, **kwds): # Preprocess in order to find what disjunctive components need # transformation gdp_tree = self._get_gdp_tree_from_targets(instance, targets) - preprocessed_targets = gdp_tree.topological_sort() - self._targets_set = set(preprocessed_targets) + # Transform from leaf to root: This is important for hull because for + # nested GDPs, we will introduce variables that need disaggregating into + # parent Disjuncts as we transform their child Disjunctions. + preprocessed_targets = gdp_tree.reverse_topological_sort() + # Get all LocalVars from Suffixes ahead of time + local_vars_by_disjunct = self._get_user_defined_local_vars(preprocessed_targets) for t in preprocessed_targets: if t.ctype is Disjunction: self._transform_disjunctionData( - t, - t.index(), - parent_disjunct=gdp_tree.parent(t), - root_disjunct=gdp_tree.root_disjunct(t), + t, t.index(), gdp_tree.parent(t), local_vars_by_disjunct ) # We skip disjuncts now, because we need information from the # disjunctions to transform them (which variables to disaggregate), @@ -274,15 +289,7 @@ def _add_transformation_block(self, to_block): return transBlock, new_block transBlock.lbub = Set(initialize=['lb', 'ub', 'eq']) - # Map between disaggregated variables and their - # originals - transBlock._disaggregatedVarMap = { - 'srcVar': ComponentMap(), - 'disaggregatedVar': ComponentMap(), - } - # Map between disaggregated variables and their lb*indicator <= var <= - # ub*indicator constraints - transBlock._bigMConstraintMap = ComponentMap() + # We will store all of the disaggregation constraints for any # Disjunctions we transform onto this block here. transBlock.disaggregationConstraints = Constraint(NonNegativeIntegers) @@ -299,19 +306,27 @@ def _add_transformation_block(self, to_block): return transBlock, True def _transform_disjunctionData( - self, obj, index, parent_disjunct=None, root_disjunct=None + self, obj, index, parent_disjunct, local_vars_by_disjunct ): # Hull reformulation doesn't work if this is an OR constraint. So if # xor is false, give up if not obj.xor: raise GDP_Error( "Cannot do hull reformulation for " - "Disjunction '%s' with OR constraint. " + "Disjunction '%s' with OR constraint. " "Must be an XOR!" % obj.name ) - + # collect the Disjuncts we are going to transform now because we will + # change their active status when we transform them, but we still need + # this list after the fact. + active_disjuncts = [disj for disj in obj.disjuncts if disj.active] + + # We put *all* transformed things on the parent Block of this + # disjunction. We'll mark the disaggregated Vars as local, but beyond + # that, we actually need everything to get transformed again as we go up + # the nested hierarchy (if there is one) transBlock, xorConstraint = self._setup_transform_disjunctionData( - obj, root_disjunct + obj, root_disjunct=None ) disaggregationConstraint = transBlock.disaggregationConstraints @@ -319,26 +334,25 @@ def _transform_disjunctionData( disaggregatedVars = transBlock._disaggregatedVars disaggregated_var_bounds = transBlock._boundsConstraints - # We first go through and collect all the variables that we - # are going to disaggregate. - varOrder_set = ComponentSet() - varOrder = [] - varsByDisjunct = ComponentMap() - localVarsByDisjunct = ComponentMap() - include_fixed_vars = not self._config.assume_fixed_vars_permanent - for disjunct in obj.disjuncts: - if not disjunct.active: - continue - disjunctVars = varsByDisjunct[disjunct] = ComponentSet() + # We first go through and collect all the variables that we are going to + # disaggregate. We do this in its own pass because we want to know all + # the Disjuncts that each Var appears in since that will tell us exactly + # which diaggregated variables we need. + var_order = ComponentSet() + disjuncts_var_appears_in = ComponentMap() + # For each disjunct in the disjunction, we will store a list of Vars + # that need a disaggregated counterpart in that disjunct. + disjunct_disaggregated_var_map = {} + for disjunct in active_disjuncts: # create the key for each disjunct now - transBlock._disaggregatedVarMap['disaggregatedVar'][ - disjunct - ] = ComponentMap() - for cons in disjunct.component_data_objects( + disjunct_disaggregated_var_map[disjunct] = ComponentMap() + for var in get_vars_from_components( + disjunct, Constraint, + include_fixed=not self._config.assume_fixed_vars_permanent, active=True, sort=SortComponents.deterministic, - descend_into=(Block, Disjunct), + descend_into=Block, ): # [ESJ 02/14/2020] By default, we disaggregate fixed variables # on the philosophy that fixing is not a promise for the future @@ -347,172 +361,145 @@ def _transform_disjunctionData( # with their transformed model. However, the user may have set # assume_fixed_vars_permanent to True in which case we will skip # them - for var in EXPR.identify_variables( - cons.body, include_fixed=include_fixed_vars - ): - # Note the use of a list so that we will - # eventually disaggregate the vars in a - # deterministic order (the order that we found - # them) - disjunctVars.add(var) - if not var in varOrder_set: - varOrder.append(var) - varOrder_set.add(var) - - # check for LocalVars Suffix - localVarsByDisjunct = self._get_local_var_suffixes( - disjunct, localVarsByDisjunct - ) - # We will disaggregate all variables that are not explicitly declared as - # being local. Since we transform from leaf to root, we are implicitly - # treating our own disaggregated variables as local, so they will not be + # Note that, because ComponentSets are ordered, we will + # eventually disaggregate the vars in a deterministic order + # (the order that we found them) + if var not in var_order: + var_order.add(var) + disjuncts_var_appears_in[var] = ComponentSet([disjunct]) + else: + disjuncts_var_appears_in[var].add(disjunct) + + # Now, we will disaggregate all variables that are not explicitly + # declared as being local. If we are moving up in a nested tree, we have + # marked our own disaggregated variables as local, so they will not be # re-disaggregated. - varSet = [] - varSet = {disj: [] for disj in obj.disjuncts} - # Note that variables are local with respect to a Disjunct. We deal with - # them here to do some error checking (if something is obviously not - # local since it is used in multiple Disjuncts in this Disjunction) and - # also to get a deterministic order in which to process them when we - # transform the Disjuncts: Values of localVarsByDisjunct are - # ComponentSets, so we need this for determinism (we iterate through the - # localVars of a Disjunct later) - localVars = ComponentMap() - varsToDisaggregate = [] - disjunctsVarAppearsIn = ComponentMap() - for var in varOrder: - disjuncts = disjunctsVarAppearsIn[var] = [ - d for d in varsByDisjunct if var in varsByDisjunct[d] - ] + vars_to_disaggregate = {disj: ComponentSet() for disj in obj.disjuncts} + all_vars_to_disaggregate = ComponentSet() + # We will ignore variables declared as local in a Disjunct that don't + # actually appear in any Constraints on that Disjunct, but in order to + # do this, we will explicitly collect the set of local_vars in this + # loop. + local_vars = defaultdict(ComponentSet) + for var in var_order: + disjuncts = disjuncts_var_appears_in[var] # clearly not local if used in more than one disjunct if len(disjuncts) > 1: if self._generate_debug_messages: logger.debug( "Assuming '%s' is not a local var since it is" - "used in multiple disjuncts." - % var.getname(fully_qualified=True) + "used in multiple disjuncts." % var.name ) for disj in disjuncts: - varSet[disj].append(var) - varsToDisaggregate.append(var) - # disjuncts is a list of length 1 - elif localVarsByDisjunct.get(disjuncts[0]) is not None: - if var in localVarsByDisjunct[disjuncts[0]]: - localVars_thisDisjunct = localVars.get(disjuncts[0]) - if localVars_thisDisjunct is not None: - localVars[disjuncts[0]].append(var) - else: - localVars[disjuncts[0]] = [var] - else: - # It's not local to this Disjunct - varSet[disjuncts[0]].append(var) - varsToDisaggregate.append(var) - else: - # We don't even have have any local vars for this Disjunct. - varSet[disjuncts[0]].append(var) - varsToDisaggregate.append(var) + vars_to_disaggregate[disj].add(var) + all_vars_to_disaggregate.add(var) + else: # var only appears in one disjunct + disjunct = next(iter(disjuncts)) + # We check if the user declared it as local + if disjunct in local_vars_by_disjunct: + if var in local_vars_by_disjunct[disjunct]: + local_vars[disjunct].add(var) + continue + # It's not declared local to this Disjunct, so we + # disaggregate + vars_to_disaggregate[disjunct].add(var) + all_vars_to_disaggregate.add(var) # Now that we know who we need to disaggregate, we will do it # while we also transform the disjuncts. - local_var_set = self._get_local_var_set(obj) + + # Get the list of local variables for the parent Disjunct so that we can + # add the disaggregated variables we're about to make to it: + parent_local_var_list = self._get_local_var_list(parent_disjunct) or_expr = 0 for disjunct in obj.disjuncts: or_expr += disjunct.indicator_var.get_associated_binary() - self._transform_disjunct( - disjunct, - transBlock, - varSet[disjunct], - localVars.get(disjunct, []), - local_var_set, - ) - rhs = 1 if parent_disjunct is None else parent_disjunct.binary_indicator_var - xorConstraint.add(index, (or_expr, rhs)) + if disjunct.active: + self._transform_disjunct( + obj=disjunct, + transBlock=transBlock, + vars_to_disaggregate=vars_to_disaggregate[disjunct], + local_vars=local_vars[disjunct], + parent_local_var_suffix=parent_local_var_list, + parent_disjunct_local_vars=local_vars_by_disjunct[parent_disjunct], + disjunct_disaggregated_var_map=disjunct_disaggregated_var_map, + ) + xorConstraint.add(index, (or_expr, 1)) # map the DisjunctionData to its XOR constraint to mark it as # transformed obj._algebraic_constraint = weakref_ref(xorConstraint[index]) - # add the reaggregation constraints - for i, var in enumerate(varsToDisaggregate): + # Now add the reaggregation constraints + for var in all_vars_to_disaggregate: # There are two cases here: Either the var appeared in every # disjunct in the disjunction, or it didn't. If it did, there's # nothing special to do: All of the disaggregated variables have # been created, and we can just proceed and make this constraint. If # it didn't, we need one more disaggregated variable, correctly # defined. And then we can make the constraint. - if len(disjunctsVarAppearsIn[var]) < len(obj.disjuncts): + if len(disjuncts_var_appears_in[var]) < len(active_disjuncts): # create one more disaggregated var idx = len(disaggregatedVars) disaggregated_var = disaggregatedVars[idx] - # mark this as local because we won't re-disaggregate if this is - # a nested disjunction - if local_var_set is not None: - local_var_set.append(disaggregated_var) + # mark this as local because we won't re-disaggregate it if this + # is a nested disjunction + if parent_local_var_list is not None: + parent_local_var_list.append(disaggregated_var) + local_vars_by_disjunct[parent_disjunct].add(disaggregated_var) var_free = 1 - sum( disj.indicator_var.get_associated_binary() - for disj in disjunctsVarAppearsIn[var] + for disj in disjuncts_var_appears_in[var] ) self._declare_disaggregated_var_bounds( - var, - disaggregated_var, - obj, - disaggregated_var_bounds, - (idx, 'lb'), - (idx, 'ub'), - var_free, + original_var=var, + disaggregatedVar=disaggregated_var, + disjunct=obj, + bigmConstraint=disaggregated_var_bounds, + lb_idx=(idx, 'lb'), + ub_idx=(idx, 'ub'), + var_free_indicator=var_free, + ) + # Update mappings: + var_info = var.parent_block().private_data() + disaggregated_var_map = var_info.disaggregated_var_map + dis_var_info = disaggregated_var.parent_block().private_data() + + dis_var_info.bigm_constraint_map[disaggregated_var][obj] = Reference( + disaggregated_var_bounds[idx, :] ) - # maintain the mappings - for disj in obj.disjuncts: + dis_var_info.original_var_map[disaggregated_var] = var + + # For every Disjunct the Var does not appear in, we want to map + # that this new variable is its disaggreggated variable. + for disj in active_disjuncts: # Because we called _transform_disjunct above, we know that # if this isn't transformed it is because it was cleanly # deactivated, and we can just skip it. if ( disj._transformation_block is not None - and disj not in disjunctsVarAppearsIn[var] + and disj not in disjuncts_var_appears_in[var] ): - relaxationBlock = disj._transformation_block().parent_block() - relaxationBlock._bigMConstraintMap[disaggregated_var] = ( - Reference(disaggregated_var_bounds[idx, :]) - ) - relaxationBlock._disaggregatedVarMap['srcVar'][ - disaggregated_var - ] = var - relaxationBlock._disaggregatedVarMap['disaggregatedVar'][disj][ - var - ] = disaggregated_var + disaggregated_var_map[disj][var] = disaggregated_var + # start the expression for the reaggregation constraint with + # this var disaggregatedExpr = disaggregated_var else: disaggregatedExpr = 0 - for disjunct in disjunctsVarAppearsIn[var]: - if disjunct._transformation_block is None: - # Because we called _transform_disjunct above, we know that - # if this isn't transformed it is because it was cleanly - # deactivated, and we can just skip it. - continue + for disjunct in disjuncts_var_appears_in[var]: + disaggregatedExpr += disjunct_disaggregated_var_map[disjunct][var] - disaggregatedVar = ( - disjunct._transformation_block() - .parent_block() - ._disaggregatedVarMap['disaggregatedVar'][disjunct][var] - ) - disaggregatedExpr += disaggregatedVar - - # We equate the sum of the disaggregated vars to var (the original) - # if parent_disjunct is None, else it needs to be the disaggregated - # var corresponding to var on the parent disjunct. This is the - # reason we transform from root to leaf: This constraint is now - # correct regardless of how nested something may have been. - parent_var = ( - var - if parent_disjunct is None - else self.get_disaggregated_var(var, parent_disjunct) - ) cons_idx = len(disaggregationConstraint) - disaggregationConstraint.add(cons_idx, parent_var == disaggregatedExpr) + # We always aggregate to the original var. If this is nested, this + # constraint will be transformed again. (And if it turns out + # everything in it is local, then that transformation won't actually + # change the mathematical expression, so it's okay. + disaggregationConstraint.add(cons_idx, var == disaggregatedExpr) # and update the map so that we can find this later. We index by # variable and the particular disjunction because there is a # different one for each disjunction - if disaggregationConstraintMap.get(var) is not None: + if var in disaggregationConstraintMap: disaggregationConstraintMap[var][obj] = disaggregationConstraint[ cons_idx ] @@ -523,13 +510,16 @@ def _transform_disjunctionData( # deactivate for the writers obj.deactivate() - def _transform_disjunct(self, obj, transBlock, varSet, localVars, local_var_set): - # We're not using the preprocessed list here, so this could be - # inactive. We've already done the error checking in preprocessing, so - # we just skip it here. - if not obj.active: - return - + def _transform_disjunct( + self, + obj, + transBlock, + vars_to_disaggregate, + local_vars, + parent_local_var_suffix, + parent_disjunct_local_vars, + disjunct_disaggregated_var_map, + ): relaxationBlock = self._get_disjunct_transformation_block(obj, transBlock) # Put the disaggregated variables all on their own block so that we can @@ -539,7 +529,7 @@ def _transform_disjunct(self, obj, transBlock, varSet, localVars, local_var_set) # add the disaggregated variables and their bigm constraints # to the relaxationBlock - for var in varSet: + for var in vars_to_disaggregate: disaggregatedVar = Var(within=Reals, initialize=var.value) # naming conflicts are possible here since this is a bunch # of variables from different blocks coming together, so we @@ -550,10 +540,13 @@ def _transform_disjunct(self, obj, transBlock, varSet, localVars, local_var_set) relaxationBlock.disaggregatedVars.add_component( disaggregatedVarName, disaggregatedVar ) - # mark this as local because we won't re-disaggregate if this is a - # nested disjunction - if local_var_set is not None: - local_var_set.append(disaggregatedVar) + # mark this as local via the Suffix in case this is a partial + # transformation: + if parent_local_var_suffix is not None: + parent_local_var_suffix.append(disaggregatedVar) + # Record that it's local for our own bookkeeping in case we're in a + # nested tree in *this* transformation + parent_disjunct_local_vars.add(disaggregatedVar) # add the bigm constraint bigmConstraint = Constraint(transBlock.lbub) @@ -562,19 +555,22 @@ def _transform_disjunct(self, obj, transBlock, varSet, localVars, local_var_set) ) self._declare_disaggregated_var_bounds( - var, - disaggregatedVar, - obj, - bigmConstraint, - 'lb', - 'ub', - obj.indicator_var.get_associated_binary(), - transBlock, + original_var=var, + disaggregatedVar=disaggregatedVar, + disjunct=obj, + bigmConstraint=bigmConstraint, + lb_idx='lb', + ub_idx='ub', + var_free_indicator=obj.indicator_var.get_associated_binary(), ) + # update the bigm constraint mappings + data_dict = disaggregatedVar.parent_block().private_data() + data_dict.bigm_constraint_map[disaggregatedVar][obj] = bigmConstraint + disjunct_disaggregated_var_map[obj][var] = disaggregatedVar - for var in localVars: - # we don't need to disaggregated, we can use this Var, but we do - # need to set up its bounds constraints. + for var in local_vars: + # we don't need to disaggregate, i.e., we can use this Var, but we + # do need to set up its bounds constraints. # naming conflicts are possible here since this is a bunch # of variables from different blocks coming together, so we @@ -585,36 +581,38 @@ def _transform_disjunct(self, obj, transBlock, varSet, localVars, local_var_set) bigmConstraint = Constraint(transBlock.lbub) relaxationBlock.add_component(conName, bigmConstraint) + parent_block = var.parent_block() + self._declare_disaggregated_var_bounds( - var, - var, - obj, - bigmConstraint, - 'lb', - 'ub', - obj.indicator_var.get_associated_binary(), - transBlock, + original_var=var, + disaggregatedVar=var, + disjunct=obj, + bigmConstraint=bigmConstraint, + lb_idx='lb', + ub_idx='ub', + var_free_indicator=obj.indicator_var.get_associated_binary(), ) + # update the bigm constraint mappings + data_dict = var.parent_block().private_data() + data_dict.bigm_constraint_map[var][obj] = bigmConstraint + disjunct_disaggregated_var_map[obj][var] = var var_substitute_map = dict( - (id(v), newV) - for v, newV in transBlock._disaggregatedVarMap['disaggregatedVar'][ - obj - ].items() + (id(v), newV) for v, newV in disjunct_disaggregated_var_map[obj].items() ) zero_substitute_map = dict( (id(v), ZeroConstant) - for v, newV in transBlock._disaggregatedVarMap['disaggregatedVar'][ - obj - ].items() + for v, newV in disjunct_disaggregated_var_map[obj].items() ) - zero_substitute_map.update((id(v), ZeroConstant) for v in localVars) # Transform each component within this disjunct self._transform_block_components( obj, obj, var_substitute_map, zero_substitute_map ) + # Anything that was local to this Disjunct is also local to the parent, + # and just got "promoted" up there, so to speak. + parent_disjunct_local_vars.update(local_vars) # deactivate disjunct so writers can be happy obj._deactivate_without_fixing_indicator() @@ -627,10 +625,7 @@ def _declare_disaggregated_var_bounds( lb_idx, ub_idx, var_free_indicator, - transBlock=None, ): - # If transBlock is None then this is a disaggregated variable for - # multiple Disjuncts and we will handle the mappings separately. lb = original_var.lb ub = original_var.ub if lb is None or ub is None: @@ -648,54 +643,32 @@ def _declare_disaggregated_var_bounds( if ub: bigmConstraint.add(ub_idx, disaggregatedVar <= ub * var_free_indicator) + original_var_info = original_var.parent_block().private_data() + disaggregated_var_map = original_var_info.disaggregated_var_map + disaggregated_var_info = disaggregatedVar.parent_block().private_data() + # store the mappings from variables to their disaggregated selves on - # the transformation block. - if transBlock is not None: - transBlock._disaggregatedVarMap['disaggregatedVar'][disjunct][ - original_var - ] = disaggregatedVar - transBlock._disaggregatedVarMap['srcVar'][disaggregatedVar] = original_var - transBlock._bigMConstraintMap[disaggregatedVar] = bigmConstraint - - def _get_local_var_set(self, disjunction): - # add Suffix to the relaxation block that disaggregated variables are - # local (in case this is nested in another Disjunct) - local_var_set = None - parent_disjunct = disjunction.parent_block() - while parent_disjunct is not None: - if parent_disjunct.ctype is Disjunct: - break - parent_disjunct = parent_disjunct.parent_block() + # the transformation block + disaggregated_var_map[disjunct][original_var] = disaggregatedVar + disaggregated_var_info.original_var_map[disaggregatedVar] = original_var + + def _get_local_var_list(self, parent_disjunct): + # Add or retrieve Suffix from parent_disjunct so that, if this is + # nested, we can use it to declare that the disaggregated variables are + # local. We return the list so that we can add to it. + local_var_list = None if parent_disjunct is not None: # This limits the cases that a user is allowed to name something # (other than a Suffix) 'LocalVars' on a Disjunct. But I am assuming # that the Suffix has to be somewhere above the disjunct in the # tree, so I can't put it on a Block that I own. And if I'm coopting # something of theirs, it may as well be here. - self._add_local_var_suffix(parent_disjunct) + self._get_local_var_suffix(parent_disjunct) if parent_disjunct.LocalVars.get(parent_disjunct) is None: parent_disjunct.LocalVars[parent_disjunct] = [] - local_var_set = parent_disjunct.LocalVars[parent_disjunct] - - return local_var_set + local_var_list = parent_disjunct.LocalVars[parent_disjunct] - def _warn_for_active_disjunct( - self, innerdisjunct, outerdisjunct, var_substitute_map, zero_substitute_map - ): - # We override the base class method because in hull, it might just be - # that we haven't gotten here yet. - disjuncts = ( - innerdisjunct.values() if innerdisjunct.is_indexed() else (innerdisjunct,) - ) - for disj in disjuncts: - if disj in self._targets_set: - # We're getting to this, have some patience. - continue - else: - # But if it wasn't in the targets after preprocessing, it - # doesn't belong in an active Disjunction that we are - # transforming and we should be confused. - _warn_for_active_disjunct(innerdisjunct, outerdisjunct) + return local_var_list def _transform_constraint( self, obj, disjunct, var_substitute_map, zero_substitute_map @@ -870,7 +843,7 @@ def _transform_constraint( # deactivate now that we have transformed obj.deactivate() - def _add_local_var_suffix(self, disjunct): + def _get_local_var_suffix(self, disjunct): # If the Suffix is there, we will borrow it. If not, we make it. If it's # something else, we complain. localSuffix = disjunct.component("LocalVars") @@ -885,7 +858,7 @@ def _add_local_var_suffix(self, disjunct): % (disjunct.getname(fully_qualified=True), localSuffix.ctype) ) - def get_disaggregated_var(self, v, disjunct): + def get_disaggregated_var(self, v, disjunct, raise_exception=True): """ Returns the disaggregated variable corresponding to the Var v and the Disjunct disjunct. @@ -899,15 +872,16 @@ def get_disaggregated_var(self, v, disjunct): """ if disjunct._transformation_block is None: raise GDP_Error("Disjunct '%s' has not been transformed" % disjunct.name) - transBlock = disjunct._transformation_block().parent_block() - try: - return transBlock._disaggregatedVarMap['disaggregatedVar'][disjunct][v] - except: - logger.error( - "It does not appear '%s' is a " - "variable that appears in disjunct '%s'" % (v.name, disjunct.name) - ) - raise + msg = ( + "It does not appear '%s' is a " + "variable that appears in disjunct '%s'" % (v.name, disjunct.name) + ) + disaggregated_var_map = v.parent_block().private_data().disaggregated_var_map + if v in disaggregated_var_map[disjunct]: + return disaggregated_var_map[disjunct][v] + else: + if raise_exception: + raise GDP_Error(msg) def get_src_var(self, disaggregated_var): """ @@ -916,35 +890,24 @@ def get_src_var(self, disaggregated_var): Parameters ---------- - disaggregated_var: a Var which was created by the hull + disaggregated_var: a Var that was created by the hull transformation as a disaggregated variable (and so appears on a transformation block of some Disjunct) """ - msg = ( + var_map = disaggregated_var.parent_block().private_data() + if disaggregated_var in var_map.original_var_map: + return var_map.original_var_map[disaggregated_var] + raise GDP_Error( "'%s' does not appear to be a " "disaggregated variable" % disaggregated_var.name ) - # There are two possibilities: It is declared on a Disjunct - # transformation Block, or it is declared on the parent of a Disjunct - # transformation block (if it is a single variable for multiple - # Disjuncts the original doesn't appear in) - transBlock = disaggregated_var.parent_block() - if not hasattr(transBlock, '_disaggregatedVarMap'): - try: - transBlock = transBlock.parent_block().parent_block() - except: - logger.error(msg) - raise - try: - return transBlock._disaggregatedVarMap['srcVar'][disaggregated_var] - except: - logger.error(msg) - raise # retrieves the disaggregation constraint for original_var resulting from # transforming disjunction - def get_disaggregation_constraint(self, original_var, disjunction): + def get_disaggregation_constraint( + self, original_var, disjunction, raise_exception=True + ): """ Returns the disaggregation (re-aggregation?) constraint (which links the disaggregated variables to their original) @@ -957,7 +920,7 @@ def get_disaggregation_constraint(self, original_var, disjunction): disjunction: a transformed Disjunction containing original_var """ for disjunct in disjunction.disjuncts: - transBlock = disjunct._transformation_block + transBlock = disjunct.transformation_block if transBlock is not None: break if transBlock is None: @@ -968,20 +931,23 @@ def get_disaggregation_constraint(self, original_var, disjunction): ) try: - return ( - transBlock() - .parent_block() - ._disaggregationConstraintMap[original_var][disjunction] - ) + cons = transBlock.parent_block()._disaggregationConstraintMap[original_var][ + disjunction + ] except: - logger.error( - "It doesn't appear that '%s' is a variable that was " - "disaggregated by Disjunction '%s'" - % (original_var.name, disjunction.name) - ) - raise + if raise_exception: + logger.error( + "It doesn't appear that '%s' is a variable that was " + "disaggregated by Disjunction '%s'" + % (original_var.name, disjunction.name) + ) + raise + return None + while not cons.active: + cons = self.get_transformed_constraints(cons)[0] + return cons - def get_var_bounds_constraint(self, v): + def get_var_bounds_constraint(self, v, disjunct=None): """ Returns the IndexedConstraint which sets a disaggregated variable to be within its bounds when its Disjunct is active and to @@ -990,28 +956,43 @@ def get_var_bounds_constraint(self, v): Parameters ---------- - v: a Var which was created by the hull transformation as a + v: a Var that was created by the hull transformation as a disaggregated variable (and so appears on a transformation block of some Disjunct) + disjunct: (For nested Disjunctions) Which Disjunct in the + hierarchy the bounds Constraint should correspond to. + Optional since for non-nested models this can be inferred. """ - msg = ( + info = v.parent_block().private_data() + if v in info.bigm_constraint_map: + if len(info.bigm_constraint_map[v]) == 1: + # Not nested, or it's at the top layer, so we're fine. + return list(info.bigm_constraint_map[v].values())[0] + elif disjunct is not None: + # This is nested, so we need to walk up to find the active ones + return info.bigm_constraint_map[v][disjunct] + else: + raise ValueError( + "It appears that the variable '%s' appears " + "within a nested GDP hierarchy, and no " + "'disjunct' argument was specified. Please " + "specify for which Disjunct the bounds " + "constraint for '%s' should be returned." % (v, v) + ) + raise GDP_Error( "Either '%s' is not a disaggregated variable, or " "the disjunction that disaggregates it has not " "been properly transformed." % v.name ) - # This can only go well if v is a disaggregated var - transBlock = v.parent_block() - if not hasattr(transBlock, '_bigMConstraintMap'): - try: - transBlock = transBlock.parent_block().parent_block() - except: - logger.error(msg) - raise - try: - return transBlock._bigMConstraintMap[v] - except: - logger.error(msg) - raise + + def get_transformed_constraints(self, cons): + cons = super().get_transformed_constraints(cons) + while not cons[0].active: + transformed_cons = [] + for con in cons: + transformed_cons += super().get_transformed_constraints(con) + cons = transformed_cons + return cons @TransformationFactory.register( diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index a2e7d5beeec..6177de3c037 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -336,8 +336,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, root_disjunct) for disjunct in active_disjuncts: or_expr += disjunct.indicator_var.get_associated_binary() self._transform_disjunct(disjunct, transBlock, active_disjuncts, Ms) - rhs = 1 if parent_disjunct is None else parent_disjunct.binary_indicator_var - algebraic_constraint.add(index, (or_expr, rhs)) + algebraic_constraint.add(index, or_expr == 1) # map the DisjunctionData to its XOR constraint to mark it as # transformed obj._algebraic_constraint = weakref_ref(algebraic_constraint[index]) diff --git a/pyomo/gdp/tests/common_tests.py b/pyomo/gdp/tests/common_tests.py index e6d38ef1502..28025816262 100644 --- a/pyomo/gdp/tests/common_tests.py +++ b/pyomo/gdp/tests/common_tests.py @@ -697,32 +697,29 @@ def check_indexedDisj_only_targets_transformed(self, transformation): trans.get_transformed_constraints(m.disjunct1[1, 0].c)[0] .parent_block() .parent_block(), - disjBlock[2], + disjBlock[0], ) self.assertIs( trans.get_transformed_constraints(m.disjunct1[1, 1].c)[0].parent_block(), - disjBlock[3], + disjBlock[1], ) # In the disaggregated var bounds self.assertIs( trans.get_transformed_constraints(m.disjunct1[2, 0].c)[0] .parent_block() .parent_block(), - disjBlock[0], + disjBlock[2], ) self.assertIs( trans.get_transformed_constraints(m.disjunct1[2, 1].c)[0].parent_block(), - disjBlock[1], + disjBlock[3], ) # This relies on the disjunctions being transformed in the same order # every time. These are the mappings between the indices of the original # disjuncts and the indices on the indexed block on the transformation # block. - if transformation == 'bigm': - pairs = [((1, 0), 0), ((1, 1), 1), ((2, 0), 2), ((2, 1), 3)] - elif transformation == 'hull': - pairs = [((2, 0), 0), ((2, 1), 1), ((1, 0), 2), ((1, 1), 3)] + pairs = [((1, 0), 0), ((1, 1), 1), ((2, 0), 2), ((2, 1), 3)] for i, j in pairs: self.assertIs(trans.get_src_disjunct(disjBlock[j]), m.disjunct1[i]) @@ -1721,17 +1718,69 @@ def check_all_components_transformed(self, m): def check_transformation_blocks_nestedDisjunctions(self, m, transformation): disjunctionTransBlock = m.disj.algebraic_constraint.parent_block() transBlocks = disjunctionTransBlock.relaxedDisjuncts - self.assertEqual(len(transBlocks), 4) if transformation == 'bigm': + self.assertEqual(len(transBlocks), 4) self.assertIs(transBlocks[0], m.d1.d3.transformation_block) self.assertIs(transBlocks[1], m.d1.d4.transformation_block) self.assertIs(transBlocks[2], m.d1.transformation_block) self.assertIs(transBlocks[3], m.d2.transformation_block) if transformation == 'hull': - self.assertIs(transBlocks[2], m.d1.d3.transformation_block) - self.assertIs(transBlocks[3], m.d1.d4.transformation_block) - self.assertIs(transBlocks[0], m.d1.transformation_block) - self.assertIs(transBlocks[1], m.d2.transformation_block) + # This is a much more comprehensive test that doesn't depend on + # transformation Block structure, so just reuse it: + hull = TransformationFactory('gdp.hull') + d3 = hull.get_disaggregated_var(m.d1.d3.binary_indicator_var, m.d1) + d4 = hull.get_disaggregated_var(m.d1.d4.binary_indicator_var, m.d1) + self.check_transformed_model_nestedDisjuncts(m, d3, d4) + + # Check the 4 constraints that are unique to the case where we didn't + # declare d1.d3 and d1.d4 as local + d32 = hull.get_disaggregated_var(m.d1.d3.binary_indicator_var, m.d2) + d42 = hull.get_disaggregated_var(m.d1.d4.binary_indicator_var, m.d2) + # check the additional disaggregated indicator var bound constraints + cons = hull.get_var_bounds_constraint(d32) + self.assertEqual(len(cons), 1) + check_obj_in_active_tree(self, cons['ub']) + cons_expr = self.simplify_leq_cons(cons['ub']) + # Note that this comes out as d32 <= 1 - d1.ind_var because it's the + # "extra" disaggregated var that gets created when it need to be + # disaggregated for d1, but it's not used in d2 + assertExpressionsEqual( + self, cons_expr, d32 + m.d1.binary_indicator_var - 1 <= 0.0 + ) + + cons = hull.get_var_bounds_constraint(d42) + self.assertEqual(len(cons), 1) + check_obj_in_active_tree(self, cons['ub']) + cons_expr = self.simplify_leq_cons(cons['ub']) + # Note that this comes out as d42 <= 1 - d1.ind_var because it's the + # "extra" disaggregated var that gets created when it need to be + # disaggregated for d1, but it's not used in d2 + assertExpressionsEqual( + self, cons_expr, d42 + m.d1.binary_indicator_var - 1 <= 0.0 + ) + # check the aggregation constraints for the disaggregated indicator vars + cons = hull.get_disaggregation_constraint(m.d1.d3.binary_indicator_var, m.disj) + check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual( + self, cons_expr, m.d1.d3.binary_indicator_var - d32 - d3 == 0.0 + ) + cons = hull.get_disaggregation_constraint(m.d1.d4.binary_indicator_var, m.disj) + check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual( + self, cons_expr, m.d1.d4.binary_indicator_var - d42 - d4 == 0.0 + ) + + num_cons = len( + list(m.component_data_objects(Constraint, active=True, descend_into=Block)) + ) + # 30 total constraints in transformed model minus 10 trivial bounds + # (lower bounds of 0) gives us 20 constraints total: + self.assertEqual(num_cons, 20) + # (And this is 4 more than we test in + # self.check_transformed_model_nestedDisjuncts, so that's comforting + # too.) def check_nested_disjunction_target(self, transformation): @@ -1895,3 +1944,17 @@ def check_nested_disjuncts_in_flat_gdp(self, transformation): for t in m.T: self.assertTrue(value(m.disj1[t].indicator_var)) self.assertTrue(value(m.disj1[t].sub1.indicator_var)) + + +def check_do_not_assume_nested_indicators_local(self, transformation): + m = models.why_indicator_vars_are_not_always_local() + TransformationFactory(transformation).apply_to(m) + + results = SolverFactory('gurobi').solve(m) + self.assertEqual(results.solver.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(value(m.obj), 9) + self.assertAlmostEqual(value(m.x), 9) + self.assertTrue(value(m.Y2.indicator_var)) + self.assertFalse(value(m.Y1.indicator_var)) + self.assertTrue(value(m.Z1.indicator_var)) + self.assertTrue(value(m.Z1.indicator_var)) diff --git a/pyomo/gdp/tests/jobshop_large_hull.lp b/pyomo/gdp/tests/jobshop_large_hull.lp index df3833bdee3..ee8ee0a73d2 100644 --- a/pyomo/gdp/tests/jobshop_large_hull.lp +++ b/pyomo/gdp/tests/jobshop_large_hull.lp @@ -42,87 +42,87 @@ c_u_Feas(G)_: <= -17 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(0)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(G)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(1)_: -+1 t(F) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(F)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(2)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(G)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(3)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(E)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(4)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(G)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(5)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(E)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(6)_: -+1 t(F) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(F)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(7)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(E)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(8)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(G)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(9)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(D)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(10)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(G)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(11)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(D)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(12)_: -+1 t(F) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(13)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(D)_ ++1 t(F) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(14)_: @@ -132,81 +132,81 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(14)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(15)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(D)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(16)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(E)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(17)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(D)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(18)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(E)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(19)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(D)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(20)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(G)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(21)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(C)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(22)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(G)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(23)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(C)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(24)_: -+1 t(F) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(F)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(25)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(C)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(26)_: -+1 t(F) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(F)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(27)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(C)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(28)_: @@ -216,33 +216,33 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(28)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(29)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(C)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(30)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(D)_ ++1 t(F) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(F)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(31)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(C)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(32)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(D)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(33)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(C)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(34)_: @@ -258,27 +258,27 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(35)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(36)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(G)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(37)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(B)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(38)_: -+1 t(F) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(F)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(39)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(B)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(40)_: @@ -288,81 +288,81 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(40)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(41)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(B)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(42)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(E)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(43)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(B)_ ++1 t(F) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(F)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(44)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(E)_ ++1 t(F) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(F)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(45)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(B)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(46)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(D)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(47)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(B)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(48)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(D)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(49)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(B)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(50)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(C)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(51)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(B)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(52)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(G)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(53)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(A)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(54)_: @@ -372,9 +372,9 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(54)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(55)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(A)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(56)_: @@ -384,81 +384,81 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(56)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(57)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(A)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(58)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(E)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(59)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(A)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(60)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(E)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(61)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(A)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(62)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(D)_ ++1 t(F) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(F)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(63)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(A)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(64)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(C)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(65)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(A)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(66)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(B)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(67)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(A)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(68)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(B)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(69)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(A)_ ++1 t(F) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(F)_ = 0 c_e__pyomo_gdp_hull_reformulation_disj_xor(A_B_3)_: @@ -637,546 +637,544 @@ c_e__pyomo_gdp_hull_reformulation_disj_xor(F_G_4)_: = 1 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(F)_ -+6.0 NoClash(F_G_4_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ ++4.0 NoClash(A_B_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(G)_ --92 NoClash(F_G_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ +-92 NoClash(A_B_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(F)_ --92 NoClash(F_G_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ +-92 NoClash(A_B_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(F)_ -+6.0 NoClash(F_G_4_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ ++5.0 NoClash(A_B_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(G)_ --92 NoClash(F_G_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ +-92 NoClash(A_B_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(F)_ --92 NoClash(F_G_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ +-92 NoClash(A_B_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(E)_ -+7.0 NoClash(E_G_5_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ ++2.0 NoClash(A_B_5_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(G)_ --92 NoClash(E_G_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(B)_ +-92 NoClash(A_B_5_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(E)_ --92 NoClash(E_G_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ +-92 NoClash(A_B_5_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(E)_ --1 NoClash(E_G_5_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ ++3.0 NoClash(A_B_5_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(G)_ --92 NoClash(E_G_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(B)_ +-92 NoClash(A_B_5_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(E)_ --92 NoClash(E_G_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ +-92 NoClash(A_B_5_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(E)_ -+8.0 NoClash(E_G_2_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ ++6.0 NoClash(A_C_1_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(G)_ --92 NoClash(E_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ +-92 NoClash(A_C_1_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(E)_ --92 NoClash(E_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ +-92 NoClash(A_C_1_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(E)_ -+4.0 NoClash(E_G_2_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ ++3.0 NoClash(A_C_1_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(G)_ --92 NoClash(E_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ +-92 NoClash(A_C_1_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(E)_ --92 NoClash(E_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ +-92 NoClash(A_C_1_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(E)_ -+3.0 NoClash(E_F_3_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(A)_ ++10.0 NoClash(A_D_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(F)_ --92 NoClash(E_F_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(D)_ +-92 NoClash(A_D_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(E)_ --92 NoClash(E_F_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(A)_ +-92 NoClash(A_D_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(E)_ -+8.0 NoClash(E_F_3_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(D)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(A)_ <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(F)_ --92 NoClash(E_F_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(D)_ +-92 NoClash(A_D_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(E)_ --92 NoClash(E_F_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(A)_ +-92 NoClash(A_D_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(D)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(A)_ ++7.0 NoClash(A_E_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(G)_ --92 NoClash(D_G_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(E)_ +-92 NoClash(A_E_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(D)_ --92 NoClash(D_G_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(A)_ +-92 NoClash(A_E_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(D)_ -+6.0 NoClash(D_G_4_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(E)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(A)_ ++4.0 NoClash(A_E_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(G)_ --92 NoClash(D_G_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(E)_ +-92 NoClash(A_E_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(D)_ --92 NoClash(D_G_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(A)_ +-92 NoClash(A_E_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(D)_ -+8.0 NoClash(D_G_2_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(A)_ ++4.0 NoClash(A_E_5_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(G)_ --92 NoClash(D_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(E)_ +-92 NoClash(A_E_5_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(D)_ --92 NoClash(D_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(A)_ +-92 NoClash(A_E_5_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(D)_ -+8.0 NoClash(D_G_2_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(E)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(A)_ <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(G)_ --92 NoClash(D_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(E)_ +-92 NoClash(A_E_5_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(D)_ --92 NoClash(D_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(A)_ +-92 NoClash(A_E_5_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_transformedConstraints(c_0_ub)_: +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(D)_ -+1 NoClash(D_F_4_0)_binary_indicator_var ++2.0 NoClash(A_F_1_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ --92 NoClash(D_F_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ +-92 NoClash(A_F_1_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(D)_ --92 NoClash(D_F_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ +-92 NoClash(A_F_1_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_transformedConstraints(c_0_ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(D)_ -+7.0 NoClash(D_F_4_1)_binary_indicator_var ++3.0 NoClash(A_F_1_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ --92 NoClash(D_F_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ +-92 NoClash(A_F_1_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(D)_ --92 NoClash(D_F_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ +-92 NoClash(A_F_1_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_transformedConstraints(c_0_ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(D)_ --1 NoClash(D_F_3_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(A)_ ++4.0 NoClash(A_F_3_0)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)__t(F)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(F)_ --92 NoClash(D_F_3_0)_binary_indicator_var +-92 NoClash(A_F_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(D)_ --92 NoClash(D_F_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(A)_ +-92 NoClash(A_F_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_transformedConstraints(c_0_ub)_: -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(D)_ -+11.0 NoClash(D_F_3_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(A)_ ++6.0 NoClash(A_F_3_1)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)__t(F)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(F)_ --92 NoClash(D_F_3_1)_binary_indicator_var +-92 NoClash(A_F_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(D)_ --92 NoClash(D_F_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(A)_ +-92 NoClash(A_F_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(D)_ -+2.0 NoClash(D_E_3_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(A)_ ++9.0 NoClash(A_G_5_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(E)_ --92 NoClash(D_E_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(G)_ +-92 NoClash(A_G_5_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(D)_ --92 NoClash(D_E_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(A)_ +-92 NoClash(A_G_5_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(D)_ -+9.0 NoClash(D_E_3_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(A)_ +-3.0 NoClash(A_G_5_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(E)_ --92 NoClash(D_E_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(G)_ +-92 NoClash(A_G_5_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(D)_ --92 NoClash(D_E_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(A)_ +-92 NoClash(A_G_5_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(D)_ -+4.0 NoClash(D_E_2_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(C)_ ++9.0 NoClash(B_C_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(E)_ --92 NoClash(D_E_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ +-92 NoClash(B_C_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(D)_ --92 NoClash(D_E_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(C)_ +-92 NoClash(B_C_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(D)_ -+8.0 NoClash(D_E_2_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(C)_ +-3.0 NoClash(B_C_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(E)_ --92 NoClash(D_E_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ +-92 NoClash(B_C_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(D)_ --92 NoClash(D_E_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(C)_ +-92 NoClash(B_C_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(C)_ -+4.0 NoClash(C_G_4_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(D)_ ++8.0 NoClash(B_D_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(G)_ --92 NoClash(C_G_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ +-92 NoClash(B_D_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(C)_ --92 NoClash(C_G_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(D)_ +-92 NoClash(B_D_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(C)_ -+7.0 NoClash(C_G_4_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(D)_ ++3.0 NoClash(B_D_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(G)_ --92 NoClash(C_G_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ +-92 NoClash(B_D_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(C)_ --92 NoClash(C_G_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(D)_ +-92 NoClash(B_D_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(C)_ -+2.0 NoClash(C_G_2_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(B)_ ++10.0 NoClash(B_D_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(G)_ --92 NoClash(C_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(D)_ +-92 NoClash(B_D_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(C)_ --92 NoClash(C_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(B)_ +-92 NoClash(B_D_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(C)_ -+9.0 NoClash(C_G_2_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(D)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(B)_ +-1 NoClash(B_D_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(G)_ --92 NoClash(C_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(D)_ +-92 NoClash(B_D_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(C)_ --92 NoClash(C_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(B)_ +-92 NoClash(B_D_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(C)_ -+5.0 NoClash(C_F_4_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(E)_ ++4.0 NoClash(B_E_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(F)_ --92 NoClash(C_F_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ +-92 NoClash(B_E_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(C)_ --92 NoClash(C_F_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(E)_ +-92 NoClash(B_E_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(C)_ -+8.0 NoClash(C_F_4_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(E)_ ++3.0 NoClash(B_E_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(F)_ --92 NoClash(C_F_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ +-92 NoClash(B_E_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(C)_ --92 NoClash(C_F_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(E)_ +-92 NoClash(B_E_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(C)_ -+2.0 NoClash(C_F_1_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(B)_ ++7.0 NoClash(B_E_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(F)_ --92 NoClash(C_F_1_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(E)_ +-92 NoClash(B_E_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(C)_ --92 NoClash(C_F_1_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(B)_ +-92 NoClash(B_E_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(C)_ -+6.0 NoClash(C_F_1_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(E)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(B)_ ++3.0 NoClash(B_E_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(F)_ --92 NoClash(C_F_1_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(E)_ +-92 NoClash(B_E_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(C)_ --92 NoClash(C_F_1_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(B)_ +-92 NoClash(B_E_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_transformedConstraints(c_0_ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(C)_ --2.0 NoClash(C_E_2_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(B)_ ++5.0 NoClash(B_E_5_0)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)__t(E)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(E)_ --92 NoClash(C_E_2_0)_binary_indicator_var +-92 NoClash(B_E_5_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(C)_ --92 NoClash(C_E_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(B)_ +-92 NoClash(B_E_5_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_transformedConstraints(c_0_ub)_: -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(C)_ -+9.0 NoClash(C_E_2_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(B)_ <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)__t(E)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(E)_ --92 NoClash(C_E_2_1)_binary_indicator_var +-92 NoClash(B_E_5_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(C)_ --92 NoClash(C_E_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(B)_ +-92 NoClash(B_E_5_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(C)_ -+5.0 NoClash(C_D_4_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(B)_ ++4.0 NoClash(B_F_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(D)_ --92 NoClash(C_D_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(F)_ +-92 NoClash(B_F_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(C)_ --92 NoClash(C_D_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(B)_ +-92 NoClash(B_F_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(D)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(C)_ -+2.0 NoClash(C_D_4_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(F)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(B)_ ++5.0 NoClash(B_F_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(D)_ --92 NoClash(C_D_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(F)_ +-92 NoClash(B_F_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(C)_ --92 NoClash(C_D_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(B)_ +-92 NoClash(B_F_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(C)_ -+2.0 NoClash(C_D_2_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(G)_ ++8.0 NoClash(B_G_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(D)_ --92 NoClash(C_D_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ +-92 NoClash(B_G_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(C)_ --92 NoClash(C_D_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(G)_ +-92 NoClash(B_G_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(D)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(C)_ -+9.0 NoClash(C_D_2_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(G)_ ++3.0 NoClash(B_G_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(D)_ --92 NoClash(C_D_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ +-92 NoClash(B_G_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(C)_ --92 NoClash(C_D_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(G)_ +-92 NoClash(B_G_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(34)_transformedConstraints(c_0_ub)_: @@ -1212,544 +1210,546 @@ c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(35)__t(B)_bounds_(ub)_: <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(B)_ -+8.0 NoClash(B_G_2_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(C)_ ++2.0 NoClash(C_D_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(G)_ --92 NoClash(B_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(D)_ +-92 NoClash(C_D_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(B)_ --92 NoClash(B_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(C)_ +-92 NoClash(C_D_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(B)_ -+3.0 NoClash(B_G_2_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(D)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(C)_ ++9.0 NoClash(C_D_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(G)_ --92 NoClash(B_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(D)_ +-92 NoClash(C_D_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(B)_ --92 NoClash(B_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(C)_ +-92 NoClash(C_D_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(B)_ -+4.0 NoClash(B_F_3_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(C)_ ++5.0 NoClash(C_D_4_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(F)_ --92 NoClash(B_F_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(D)_ +-92 NoClash(C_D_4_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(B)_ --92 NoClash(B_F_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(C)_ +-92 NoClash(C_D_4_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(B)_ -+5.0 NoClash(B_F_3_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(D)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(C)_ ++2.0 NoClash(C_D_4_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(F)_ --92 NoClash(B_F_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(D)_ +-92 NoClash(C_D_4_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(B)_ --92 NoClash(B_F_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(C)_ +-92 NoClash(C_D_4_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_transformedConstraints(c_0_ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(B)_ -+5.0 NoClash(B_E_5_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(C)_ +-2.0 NoClash(C_E_2_0)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)__t(E)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(E)_ --92 NoClash(B_E_5_0)_binary_indicator_var +-92 NoClash(C_E_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(B)_ --92 NoClash(B_E_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(C)_ +-92 NoClash(C_E_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_transformedConstraints(c_0_ub)_: -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(C)_ ++9.0 NoClash(C_E_2_1)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)__t(E)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(E)_ --92 NoClash(B_E_5_1)_binary_indicator_var +-92 NoClash(C_E_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(B)_ --92 NoClash(B_E_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(C)_ +-92 NoClash(C_E_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(B)_ -+7.0 NoClash(B_E_3_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(F)_ ++2.0 NoClash(C_F_1_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(E)_ --92 NoClash(B_E_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ +-92 NoClash(C_F_1_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(B)_ --92 NoClash(B_E_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(F)_ +-92 NoClash(C_F_1_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(B)_ -+3.0 NoClash(B_E_3_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(F)_ ++6.0 NoClash(C_F_1_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(E)_ --92 NoClash(B_E_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ +-92 NoClash(C_F_1_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(B)_ --92 NoClash(B_E_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(F)_ +-92 NoClash(C_F_1_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(B)_ -+4.0 NoClash(B_E_2_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(C)_ ++5.0 NoClash(C_F_4_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(E)_ --92 NoClash(B_E_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(F)_ +-92 NoClash(C_F_4_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(B)_ --92 NoClash(B_E_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(C)_ +-92 NoClash(C_F_4_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(B)_ -+3.0 NoClash(B_E_2_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(F)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(C)_ ++8.0 NoClash(C_F_4_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(E)_ --92 NoClash(B_E_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(F)_ +-92 NoClash(C_F_4_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(B)_ --92 NoClash(B_E_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(C)_ +-92 NoClash(C_F_4_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(B)_ -+10.0 NoClash(B_D_3_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(C)_ ++2.0 NoClash(C_G_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(D)_ --92 NoClash(B_D_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(G)_ +-92 NoClash(C_G_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(B)_ --92 NoClash(B_D_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(C)_ +-92 NoClash(C_G_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(D)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(B)_ --1 NoClash(B_D_3_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(C)_ ++9.0 NoClash(C_G_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(D)_ --92 NoClash(B_D_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(G)_ +-92 NoClash(C_G_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(B)_ --92 NoClash(B_D_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(C)_ +-92 NoClash(C_G_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(B)_ -+8.0 NoClash(B_D_2_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(C)_ ++4.0 NoClash(C_G_4_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(D)_ --92 NoClash(B_D_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(G)_ +-92 NoClash(C_G_4_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(B)_ --92 NoClash(B_D_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(C)_ +-92 NoClash(C_G_4_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(D)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(B)_ -+3.0 NoClash(B_D_2_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(C)_ ++7.0 NoClash(C_G_4_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(D)_ --92 NoClash(B_D_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(G)_ +-92 NoClash(C_G_4_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(B)_ --92 NoClash(B_D_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(C)_ +-92 NoClash(C_G_4_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(B)_ -+9.0 NoClash(B_C_2_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(E)_ ++4.0 NoClash(D_E_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(C)_ --92 NoClash(B_C_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ +-92 NoClash(D_E_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(B)_ --92 NoClash(B_C_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(E)_ +-92 NoClash(D_E_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(C)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(B)_ --3.0 NoClash(B_C_2_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(E)_ ++8.0 NoClash(D_E_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(C)_ --92 NoClash(B_C_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ +-92 NoClash(D_E_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(B)_ --92 NoClash(B_C_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(E)_ +-92 NoClash(D_E_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(A)_ -+9.0 NoClash(A_G_5_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(D)_ ++2.0 NoClash(D_E_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(G)_ --92 NoClash(A_G_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(E)_ +-92 NoClash(D_E_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(A)_ --92 NoClash(A_G_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(D)_ +-92 NoClash(D_E_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(A)_ --3.0 NoClash(A_G_5_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(E)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(D)_ ++9.0 NoClash(D_E_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(G)_ --92 NoClash(A_G_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(E)_ +-92 NoClash(D_E_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(A)_ --92 NoClash(A_G_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(D)_ +-92 NoClash(D_E_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_transformedConstraints(c_0_ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(A)_ -+4.0 NoClash(A_F_3_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(D)_ +-1 NoClash(D_F_3_0)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)__t(F)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(F)_ --92 NoClash(A_F_3_0)_binary_indicator_var +-92 NoClash(D_F_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(A)_ --92 NoClash(A_F_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(D)_ +-92 NoClash(D_F_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_transformedConstraints(c_0_ub)_: -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(A)_ -+6.0 NoClash(A_F_3_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(D)_ ++11.0 NoClash(D_F_3_1)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)__t(F)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(F)_ --92 NoClash(A_F_3_1)_binary_indicator_var +-92 NoClash(D_F_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(A)_ --92 NoClash(A_F_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(D)_ +-92 NoClash(D_F_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_transformedConstraints(c_0_ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(A)_ -+2.0 NoClash(A_F_1_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(D)_ ++1 NoClash(D_F_4_0)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)__t(F)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(F)_ --92 NoClash(A_F_1_0)_binary_indicator_var +-92 NoClash(D_F_4_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(A)_ --92 NoClash(A_F_1_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(D)_ +-92 NoClash(D_F_4_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_transformedConstraints(c_0_ub)_: -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(A)_ -+3.0 NoClash(A_F_1_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(D)_ ++7.0 NoClash(D_F_4_1)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)__t(F)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(F)_ --92 NoClash(A_F_1_1)_binary_indicator_var +-92 NoClash(D_F_4_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(A)_ --92 NoClash(A_F_1_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(D)_ +-92 NoClash(D_F_4_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(A)_ -+4.0 NoClash(A_E_5_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(G)_ ++8.0 NoClash(D_G_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(E)_ --92 NoClash(A_E_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ +-92 NoClash(D_G_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(A)_ --92 NoClash(A_E_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(G)_ +-92 NoClash(D_G_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(A)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(G)_ ++8.0 NoClash(D_G_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(E)_ --92 NoClash(A_E_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ +-92 NoClash(D_G_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(A)_ --92 NoClash(A_E_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(G)_ +-92 NoClash(D_G_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(A)_ -+7.0 NoClash(A_E_3_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(D)_ <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(E)_ --92 NoClash(A_E_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(G)_ +-92 NoClash(D_G_4_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(A)_ --92 NoClash(A_E_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(D)_ +-92 NoClash(D_G_4_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(A)_ -+4.0 NoClash(A_E_3_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(D)_ ++6.0 NoClash(D_G_4_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(E)_ --92 NoClash(A_E_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(G)_ +-92 NoClash(D_G_4_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(A)_ --92 NoClash(A_E_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(D)_ +-92 NoClash(D_G_4_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(A)_ -+10.0 NoClash(A_D_3_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(E)_ ++3.0 NoClash(E_F_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(D)_ --92 NoClash(A_D_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(F)_ +-92 NoClash(E_F_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(A)_ --92 NoClash(A_D_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(E)_ +-92 NoClash(E_F_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(D)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(F)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(E)_ ++8.0 NoClash(E_F_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(D)_ --92 NoClash(A_D_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(F)_ +-92 NoClash(E_F_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(A)_ --92 NoClash(A_D_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(E)_ +-92 NoClash(E_F_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(A)_ -+6.0 NoClash(A_C_1_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(G)_ ++8.0 NoClash(E_G_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(C)_ --92 NoClash(A_C_1_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ +-92 NoClash(E_G_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(A)_ --92 NoClash(A_C_1_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(G)_ +-92 NoClash(E_G_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(C)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(A)_ -+3.0 NoClash(A_C_1_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(G)_ ++4.0 NoClash(E_G_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(C)_ --92 NoClash(A_C_1_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ +-92 NoClash(E_G_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(A)_ --92 NoClash(A_C_1_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(G)_ +-92 NoClash(E_G_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(A)_ -+2.0 NoClash(A_B_5_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(E)_ ++7.0 NoClash(E_G_5_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(B)_ --92 NoClash(A_B_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(G)_ +-92 NoClash(E_G_5_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(A)_ --92 NoClash(A_B_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(E)_ +-92 NoClash(E_G_5_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(B)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(A)_ -+3.0 NoClash(A_B_5_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(E)_ +-1 NoClash(E_G_5_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(B)_ --92 NoClash(A_B_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(G)_ +-92 NoClash(E_G_5_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(A)_ --92 NoClash(A_B_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(E)_ +-92 NoClash(E_G_5_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(A)_ -+4.0 NoClash(A_B_3_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(F)_ ++6.0 NoClash(F_G_4_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(B)_ --92 NoClash(A_B_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(G)_ +-92 NoClash(F_G_4_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(A)_ --92 NoClash(A_B_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(F)_ +-92 NoClash(F_G_4_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(B)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(A)_ -+5.0 NoClash(A_B_3_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(F)_ ++6.0 NoClash(F_G_4_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(B)_ --92 NoClash(A_B_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(G)_ +-92 NoClash(F_G_4_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(A)_ --92 NoClash(A_B_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(F)_ +-92 NoClash(F_G_4_1)_binary_indicator_var <= 0 bounds @@ -1761,146 +1761,146 @@ bounds 0 <= t(E) <= 92 0 <= t(F) <= 92 0 <= t(G) <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(B)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(34)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(35)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(34)_disaggregatedVars__t(B)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(35)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(C)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(F)_ <= 92 0 <= NoClash(A_B_3_0)_binary_indicator_var <= 1 0 <= NoClash(A_B_3_1)_binary_indicator_var <= 1 0 <= NoClash(A_B_5_0)_binary_indicator_var <= 1 diff --git a/pyomo/gdp/tests/jobshop_small_hull.lp b/pyomo/gdp/tests/jobshop_small_hull.lp index c07b9cd048e..ae2d738d29c 100644 --- a/pyomo/gdp/tests/jobshop_small_hull.lp +++ b/pyomo/gdp/tests/jobshop_small_hull.lp @@ -22,29 +22,29 @@ c_u_Feas(C)_: <= -6 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(0)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(C)_ -= 0 - -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(1)_: +1 t(B) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ = 0 -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(2)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(1)_: ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ = 0 -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(3)_: +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(2)_: +1 t(A) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ = 0 +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(3)_: ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ += 0 + c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(4)_: +1 t(B) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ @@ -52,9 +52,9 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(4)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(5)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disj_xor(A_B_3)_: @@ -73,98 +73,98 @@ c_e__pyomo_gdp_hull_reformulation_disj_xor(B_C_2)_: = 1 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ -+6.0 NoClash(B_C_2_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(C)_ --19 NoClash(B_C_2_0)_binary_indicator_var -<= 0 - c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)__t(B)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ --19 NoClash(B_C_2_0)_binary_indicator_var +-19 NoClash(A_B_3_0)_binary_indicator_var +<= 0 + +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ +-19 NoClash(A_B_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(C)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ -+1 NoClash(B_C_2_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ ++5.0 NoClash(A_B_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(C)_ --19 NoClash(B_C_2_1)_binary_indicator_var -<= 0 - c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(B)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ --19 NoClash(B_C_2_1)_binary_indicator_var +-19 NoClash(A_B_3_1)_binary_indicator_var +<= 0 + +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ +-19 NoClash(A_B_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ +2.0 NoClash(A_C_1_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ -19 NoClash(A_C_1_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ -19 NoClash(A_C_1_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ +5.0 NoClash(A_C_1_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ -19 NoClash(A_C_1_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ -19 NoClash(A_C_1_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ ++6.0 NoClash(B_C_2_0)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(B)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ --19 NoClash(A_B_3_0)_binary_indicator_var +-19 NoClash(B_C_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ --19 NoClash(A_B_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ +-19 NoClash(B_C_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ -+5.0 NoClash(A_B_3_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ ++1 NoClash(B_C_2_1)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(B)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ --19 NoClash(A_B_3_1)_binary_indicator_var +-19 NoClash(B_C_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ --19 NoClash(A_B_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ +-19 NoClash(B_C_2_1)_binary_indicator_var <= 0 bounds @@ -172,18 +172,18 @@ bounds 0 <= t(A) <= 19 0 <= t(B) <= 19 0 <= t(C) <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(C)_ <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(C)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ <= 19 0 <= NoClash(A_B_3_0)_binary_indicator_var <= 1 0 <= NoClash(A_B_3_1)_binary_indicator_var <= 1 0 <= NoClash(A_C_1_0)_binary_indicator_var <= 1 diff --git a/pyomo/gdp/tests/models.py b/pyomo/gdp/tests/models.py index 273bdec7261..0b84641899c 100644 --- a/pyomo/gdp/tests/models.py +++ b/pyomo/gdp/tests/models.py @@ -474,7 +474,7 @@ def makeNestedDisjunctions(): (makeNestedDisjunctions_NestedDisjuncts is a much simpler model. All this adds is that it has a nested disjunction on a DisjunctData as well - as on a SimpleDisjunct. So mostly it exists for historical reasons.) + as on a ScalarDisjunct. So mostly it exists for historical reasons.) """ m = ConcreteModel() m.x = Var(bounds=(-9, 9)) @@ -563,6 +563,44 @@ def makeNestedDisjunctions_NestedDisjuncts(): return m +def why_indicator_vars_are_not_always_local(): + m = ConcreteModel() + m.x = Var(bounds=(1, 10)) + + @m.Disjunct() + def Z1(d): + m = d.model() + d.c = Constraint(expr=m.x >= 1.1) + + @m.Disjunct() + def Z2(d): + m = d.model() + d.c = Constraint(expr=m.x >= 1.2) + + @m.Disjunct() + def Y1(d): + m = d.model() + d.c = Constraint(expr=(1.15, m.x, 8)) + d.disjunction = Disjunction(expr=[m.Z1, m.Z2]) + + @m.Disjunct() + def Y2(d): + m = d.model() + d.c = Constraint(expr=m.x == 9) + + m.disjunction = Disjunction(expr=[m.Y1, m.Y2]) + + m.logical_cons = LogicalConstraint( + expr=m.Y2.indicator_var.implies(m.Z1.indicator_var.land(m.Z2.indicator_var)) + ) + + # optimal value is 9, but it will be 8 if we wrongly assume that the nested + # indicator_vars are local. + m.obj = Objective(expr=m.x, sense=maximize) + + return m + + def makeTwoSimpleDisjunctions(): """Two SimpleDisjunctions on the same model.""" m = ConcreteModel() diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index d518219eabd..00efcb46485 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -33,6 +33,7 @@ assertExpressionsStructurallyEqual, ) from pyomo.repn import generate_standard_repn +from pyomo.repn.linear import LinearRepnVisitor from pyomo.common.log import LoggingIntercept import logging @@ -1764,22 +1765,19 @@ def test_transformation_block_structure(self): # we have the XOR constraints for both the outer and inner disjunctions self.assertIsInstance(transBlock.component("disjunction_xor"), Constraint) - def test_transformation_block_on_inner_disjunct_empty(self): - m = models.makeNestedDisjunctions() - TransformationFactory('gdp.bigm').apply_to(m) - self.assertIsNone(m.disjunct[1].component("_pyomo_gdp_bigm_reformulation")) - def test_mappings_between_disjunctions_and_xors(self): m = models.makeNestedDisjunctions() transform = TransformationFactory('gdp.bigm') transform.apply_to(m) transBlock1 = m.component("_pyomo_gdp_bigm_reformulation") + transBlock2 = m.disjunct[1].component("_pyomo_gdp_bigm_reformulation") + transBlock3 = m.simpledisjunct.component("_pyomo_gdp_bigm_reformulation") disjunctionPairs = [ (m.disjunction, transBlock1.disjunction_xor), - (m.disjunct[1].innerdisjunction[0], transBlock1.innerdisjunction_xor_4[0]), - (m.simpledisjunct.innerdisjunction, transBlock1.innerdisjunction_xor), + (m.disjunct[1].innerdisjunction[0], transBlock2.innerdisjunction_xor[0]), + (m.simpledisjunct.innerdisjunction, transBlock3.innerdisjunction_xor), ] # check disjunction mappings @@ -1899,19 +1897,32 @@ def check_bigM_constraint(self, cons, variable, M, indicator_var): ct.check_linear_coef(self, repn, variable, 1) ct.check_linear_coef(self, repn, indicator_var, M) - def check_inner_xor_constraint( - self, inner_disjunction, outer_disjunct, inner_disjuncts - ): - self.assertIsNotNone(inner_disjunction.algebraic_constraint) - cons = inner_disjunction.algebraic_constraint - self.assertEqual(cons.lower, 0) - self.assertEqual(cons.upper, 0) - repn = generate_standard_repn(cons.body) - self.assertTrue(repn.is_linear()) - self.assertEqual(repn.constant, 0) - for disj in inner_disjuncts: - ct.check_linear_coef(self, repn, disj.binary_indicator_var, 1) - ct.check_linear_coef(self, repn, outer_disjunct.binary_indicator_var, -1) + def check_inner_xor_constraint(self, inner_disjunction, outer_disjunct, bigm): + inner_xor = inner_disjunction.algebraic_constraint + sum_indicators = sum( + d.binary_indicator_var for d in inner_disjunction.disjuncts + ) + assertExpressionsEqual(self, inner_xor.expr, sum_indicators == 1) + # this guy has been transformed + self.assertFalse(inner_xor.active) + cons = bigm.get_transformed_constraints(inner_xor) + self.assertEqual(len(cons), 2) + lb = cons[0] + ct.check_obj_in_active_tree(self, lb) + lb_expr = self.simplify_cons(lb, leq=False) + assertExpressionsEqual( + self, + lb_expr, + 1.0 <= sum_indicators - outer_disjunct.binary_indicator_var + 1.0, + ) + ub = cons[1] + ct.check_obj_in_active_tree(self, ub) + ub_expr = self.simplify_cons(ub, leq=True) + assertExpressionsEqual( + self, + ub_expr, + sum_indicators + outer_disjunct.binary_indicator_var - 1 <= 1.0, + ) def test_transformed_constraints(self): # We'll check all the transformed constraints to make sure @@ -1993,26 +2004,9 @@ def test_transformed_constraints(self): # Here we check that the xor constraint from # simpledisjunct.innerdisjunction is transformed. - cons5 = m.simpledisjunct.innerdisjunction.algebraic_constraint - self.assertIsNotNone(cons5) self.check_inner_xor_constraint( - m.simpledisjunct.innerdisjunction, - m.simpledisjunct, - [m.simpledisjunct.innerdisjunct0, m.simpledisjunct.innerdisjunct1], - ) - self.assertIsInstance(cons5, Constraint) - self.assertEqual(cons5.lower, 0) - self.assertEqual(cons5.upper, 0) - repn = generate_standard_repn(cons5.body) - self.assertTrue(repn.is_linear()) - self.assertEqual(repn.constant, 0) - ct.check_linear_coef( - self, repn, m.simpledisjunct.innerdisjunct0.binary_indicator_var, 1 - ) - ct.check_linear_coef( - self, repn, m.simpledisjunct.innerdisjunct1.binary_indicator_var, 1 + m.simpledisjunct.innerdisjunction, m.simpledisjunct, bigm ) - ct.check_linear_coef(self, repn, m.simpledisjunct.binary_indicator_var, -1) cons6 = bigm.get_transformed_constraints(m.disjunct[0].c) self.assertEqual(len(cons6), 2) @@ -2028,9 +2022,7 @@ def test_transformed_constraints(self): # now we check that the xor constraint from disjunct[1].innerdisjunction # is correct. self.check_inner_xor_constraint( - m.disjunct[1].innerdisjunction[0], - m.disjunct[1], - [m.disjunct[1].innerdisjunct[0], m.disjunct[1].innerdisjunct[1]], + m.disjunct[1].innerdisjunction[0], m.disjunct[1], bigm ) cons8 = bigm.get_transformed_constraints(m.disjunct[1].c) @@ -2136,33 +2128,27 @@ def check_second_disjunct_constraint(self, disj2c, x, ind_var): ct.check_squared_term_coef(self, repn, x[i], 1) ct.check_linear_coef(self, repn, x[i], -6) + def simplify_cons(self, cons, leq): + visitor = LinearRepnVisitor({}, {}, {}, None) + repn = visitor.walk_expression(cons.body) + self.assertIsNone(repn.nonlinear) + if leq: + self.assertIsNone(cons.lower) + ub = cons.upper + return ub >= repn.to_expression(visitor) + else: + self.assertIsNone(cons.upper) + lb = cons.lower + return lb <= repn.to_expression(visitor) + def check_hierarchical_nested_model(self, m, bigm): outer_xor = m.disjunction_block.disjunction.algebraic_constraint ct.check_two_term_disjunction_xor( self, outer_xor, m.disj1, m.disjunct_block.disj2 ) - inner_xor = m.disjunct_block.disj2.disjunction.algebraic_constraint - self.assertEqual(inner_xor.lower, 0) - self.assertEqual(inner_xor.upper, 0) - repn = generate_standard_repn(inner_xor.body) - self.assertTrue(repn.is_linear()) - self.assertEqual(len(repn.linear_vars), 3) - self.assertEqual(repn.constant, 0) - ct.check_linear_coef( - self, - repn, - m.disjunct_block.disj2.disjunction_disjuncts[0].binary_indicator_var, - 1, - ) - ct.check_linear_coef( - self, - repn, - m.disjunct_block.disj2.disjunction_disjuncts[1].binary_indicator_var, - 1, - ) - ct.check_linear_coef( - self, repn, m.disjunct_block.disj2.binary_indicator_var, -1 + self.check_inner_xor_constraint( + m.disjunct_block.disj2.disjunction, m.disjunct_block.disj2, bigm ) # outer disjunction constraints @@ -2214,6 +2200,10 @@ def test_decl_order_opposite_instantiation_order(self): # the same check to make sure everything is transformed correctly. self.check_hierarchical_nested_model(m, bigm) + @unittest.skipUnless(gurobi_available, "Gurobi is not available") + def test_do_not_assume_nested_indicators_local(self): + ct.check_do_not_assume_nested_indicators_local(self, 'gdp.bigm') + class IndexedDisjunction(unittest.TestCase): # this tests that if the targets are a subset of the diff --git a/pyomo/gdp/tests/test_binary_multiplication.py b/pyomo/gdp/tests/test_binary_multiplication.py index 5f4c4f90ab6..aa846c4710a 100644 --- a/pyomo/gdp/tests/test_binary_multiplication.py +++ b/pyomo/gdp/tests/test_binary_multiplication.py @@ -18,6 +18,7 @@ ConcreteModel, Var, Any, + SolverFactory, ) from pyomo.gdp import Disjunct, Disjunction from pyomo.core.expr.compare import assertExpressionsEqual @@ -30,6 +31,11 @@ import random +gurobi_available = ( + SolverFactory('gurobi').available(exception_flag=False) + and SolverFactory('gurobi').license_is_valid() +) + class CommonTests: def diff_apply_to_and_create_using(self, model): @@ -297,5 +303,13 @@ def test_local_var(self): self.assertEqual(eq.ub, 0) +class TestNestedGDP(unittest.TestCase): + @unittest.skipUnless(gurobi_available, "Gurobi is not available") + def test_do_not_assume_nested_indicators_local(self): + ct.check_do_not_assume_nested_indicators_local( + self, 'gdp.binary_multiplication' + ) + + if __name__ == '__main__': unittest.main() diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index cf0ce3234af..858764759ee 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -41,6 +41,7 @@ import pyomo.core.expr as EXPR from pyomo.core.base import constraint from pyomo.repn import generate_standard_repn +from pyomo.repn.linear import LinearRepnVisitor from pyomo.gdp import Disjunct, Disjunction, GDP_Error import pyomo.gdp.tests.models as models @@ -51,6 +52,7 @@ import os from os.path import abspath, dirname, join + currdir = dirname(abspath(__file__)) from filecmp import cmp @@ -402,19 +404,13 @@ def test_error_for_or(self): self.assertRaisesRegex( GDP_Error, "Cannot do hull reformulation for Disjunction " - "'disjunction' with OR constraint. Must be an XOR!*", + "'disjunction' with OR constraint. Must be an XOR!*", TransformationFactory('gdp.hull').apply_to, m, ) def check_disaggregation_constraint(self, cons, var, disvar1, disvar2): - repn = generate_standard_repn(cons.body) - self.assertEqual(cons.lower, 0) - self.assertEqual(cons.upper, 0) - self.assertEqual(len(repn.linear_vars), 3) - ct.check_linear_coef(self, repn, var, 1) - ct.check_linear_coef(self, repn, disvar1, -1) - ct.check_linear_coef(self, repn, disvar2, -1) + assertExpressionsEqual(self, cons.expr, var == disvar1 + disvar2) def test_disaggregation_constraint(self): m = models.makeTwoTermDisj_Nonlinear() @@ -426,8 +422,8 @@ def test_disaggregation_constraint(self): self.check_disaggregation_constraint( hull.get_disaggregation_constraint(m.w, m.disjunction), m.w, - disjBlock[1].disaggregatedVars.w, transBlock._disaggregatedVars[1], + disjBlock[1].disaggregatedVars.w, ) self.check_disaggregation_constraint( hull.get_disaggregation_constraint(m.x, m.disjunction), @@ -438,8 +434,8 @@ def test_disaggregation_constraint(self): self.check_disaggregation_constraint( hull.get_disaggregation_constraint(m.y, m.disjunction), m.y, - disjBlock[0].disaggregatedVars.y, transBlock._disaggregatedVars[0], + disjBlock[0].disaggregatedVars.y, ) def test_xor_constraint_mapping(self): @@ -510,10 +506,10 @@ def test_disaggregatedVar_mappings(self): for i in [0, 1]: mappings = ComponentMap() mappings[m.x] = disjBlock[i].disaggregatedVars.x - if i == 1: # this disjunct as x, w, and no y + if i == 1: # this disjunct has x, w, and no y mappings[m.w] = disjBlock[i].disaggregatedVars.w mappings[m.y] = transBlock._disaggregatedVars[0] - elif i == 0: # this disjunct as x, y, and no w + elif i == 0: # this disjunct has x, y, and no w mappings[m.y] = disjBlock[i].disaggregatedVars.y mappings[m.w] = transBlock._disaggregatedVars[1] @@ -668,17 +664,38 @@ def test_global_vars_local_to_a_disjunction_disaggregated(self): self.assertIs(hull.get_src_var(x), m.disj1.x) # there is a spare x on disjunction1's block - x2 = m.disjunction1.algebraic_constraint.parent_block()._disaggregatedVars[2] + x2 = m.disjunction1.algebraic_constraint.parent_block()._disaggregatedVars[0] self.assertIs(hull.get_disaggregated_var(m.disj1.x, m.disj2), x2) self.assertIs(hull.get_src_var(x2), m.disj1.x) + # What really matters is that the above matches this: + agg_cons = hull.get_disaggregation_constraint(m.disj1.x, m.disjunction1) + assertExpressionsEqual( + self, + agg_cons.expr, + m.disj1.x == x2 + hull.get_disaggregated_var(m.disj1.x, m.disj1), + ) # and both a spare x and y on disjunction2's block - x2 = m.disjunction2.algebraic_constraint.parent_block()._disaggregatedVars[0] - y1 = m.disjunction2.algebraic_constraint.parent_block()._disaggregatedVars[1] + x2 = m.disjunction2.algebraic_constraint.parent_block()._disaggregatedVars[1] + y1 = m.disjunction2.algebraic_constraint.parent_block()._disaggregatedVars[2] self.assertIs(hull.get_disaggregated_var(m.disj1.x, m.disj4), x2) self.assertIs(hull.get_src_var(x2), m.disj1.x) self.assertIs(hull.get_disaggregated_var(m.disj1.y, m.disj3), y1) self.assertIs(hull.get_src_var(y1), m.disj1.y) + # and again what really matters is that these align with the + # disaggregation constraints: + agg_cons = hull.get_disaggregation_constraint(m.disj1.x, m.disjunction2) + assertExpressionsEqual( + self, + agg_cons.expr, + m.disj1.x == x2 + hull.get_disaggregated_var(m.disj1.x, m.disj3), + ) + agg_cons = hull.get_disaggregation_constraint(m.disj1.y, m.disjunction2) + assertExpressionsEqual( + self, + agg_cons.expr, + m.disj1.y == y1 + hull.get_disaggregated_var(m.disj1.y, m.disj4), + ) def check_name_collision_disaggregated_vars(self, m, disj): hull = TransformationFactory('gdp.hull') @@ -1101,7 +1118,7 @@ def check_trans_block_disjunctions_of_disjunct_datas(self, m): self.assertEqual(len(transBlock1.relaxedDisjuncts), 4) hull = TransformationFactory('gdp.hull') - firstTerm2 = transBlock1.relaxedDisjuncts[0] + firstTerm2 = transBlock1.relaxedDisjuncts[2] self.assertIs(firstTerm2, m.firstTerm[2].transformation_block) self.assertIsInstance(firstTerm2.disaggregatedVars.component("x"), Var) constraints = hull.get_transformed_constraints(m.firstTerm[2].cons) @@ -1115,7 +1132,7 @@ def check_trans_block_disjunctions_of_disjunct_datas(self, m): self.assertIs(cons.parent_block(), firstTerm2) self.assertEqual(len(cons), 2) - secondTerm2 = transBlock1.relaxedDisjuncts[1] + secondTerm2 = transBlock1.relaxedDisjuncts[3] self.assertIs(secondTerm2, m.secondTerm[2].transformation_block) self.assertIsInstance(secondTerm2.disaggregatedVars.component("x"), Var) constraints = hull.get_transformed_constraints(m.secondTerm[2].cons) @@ -1129,7 +1146,7 @@ def check_trans_block_disjunctions_of_disjunct_datas(self, m): self.assertIs(cons.parent_block(), secondTerm2) self.assertEqual(len(cons), 2) - firstTerm1 = transBlock1.relaxedDisjuncts[2] + firstTerm1 = transBlock1.relaxedDisjuncts[0] self.assertIs(firstTerm1, m.firstTerm[1].transformation_block) self.assertIsInstance(firstTerm1.disaggregatedVars.component("x"), Var) self.assertTrue(firstTerm1.disaggregatedVars.x.is_fixed()) @@ -1147,7 +1164,7 @@ def check_trans_block_disjunctions_of_disjunct_datas(self, m): self.assertIs(cons.parent_block(), firstTerm1) self.assertEqual(len(cons), 2) - secondTerm1 = transBlock1.relaxedDisjuncts[3] + secondTerm1 = transBlock1.relaxedDisjuncts[1] self.assertIs(secondTerm1, m.secondTerm[1].transformation_block) self.assertIsInstance(secondTerm1.disaggregatedVars.component("x"), Var) constraints = hull.get_transformed_constraints(m.secondTerm[1].cons) @@ -1375,9 +1392,8 @@ def test_deactivated_disjunct_leaves_nested_disjuncts_active(self): ct.check_deactivated_disjunct_leaves_nested_disjunct_active(self, 'hull') def test_mappings_between_disjunctions_and_xors(self): - # This test is nearly identical to the one in bigm, but because of - # different transformation orders, the name conflict gets resolved in - # the opposite way. + # Tests that the XOR constraints are put on the parent block of the + # disjunction, and checks the mappings. m = models.makeNestedDisjunctions() transform = TransformationFactory('gdp.hull') transform.apply_to(m) @@ -1386,8 +1402,17 @@ def test_mappings_between_disjunctions_and_xors(self): disjunctionPairs = [ (m.disjunction, transBlock.disjunction_xor), - (m.disjunct[1].innerdisjunction[0], transBlock.innerdisjunction_xor[0]), - (m.simpledisjunct.innerdisjunction, transBlock.innerdisjunction_xor_4), + ( + m.disjunct[1].innerdisjunction[0], + m.disjunct[1] + .innerdisjunction[0] + .algebraic_constraint.parent_block() + .innerdisjunction_xor[0], + ), + ( + m.simpledisjunct.innerdisjunction, + m.simpledisjunct.innerdisjunction.algebraic_constraint.parent_block().innerdisjunction_xor, + ), ] # check disjunction mappings @@ -1427,16 +1452,16 @@ def test_relaxation_feasibility(self): solver = SolverFactory(linear_solvers[0]) cases = [ - (1, 1, 1, 1, None), - (0, 0, 0, 0, None), - (1, 0, 0, 0, None), - (0, 1, 0, 0, 1.1), - (0, 0, 1, 0, None), - (0, 0, 0, 1, None), - (1, 1, 0, 0, None), - (1, 0, 1, 0, 1.2), - (1, 0, 0, 1, 1.3), - (1, 0, 1, 1, None), + (True, True, True, True, None), + (False, False, False, False, None), + (True, False, False, False, None), + (False, True, False, False, 1.1), + (False, False, True, False, None), + (False, False, False, True, None), + (True, True, False, False, None), + (True, False, True, False, 1.2), + (True, False, False, True, 1.3), + (True, False, True, True, None), ] for case in cases: m.d1.indicator_var.fix(case[0]) @@ -1468,16 +1493,16 @@ def test_relaxation_feasibility_transform_inner_first(self): solver = SolverFactory(linear_solvers[0]) cases = [ - (1, 1, 1, 1, None), - (0, 0, 0, 0, None), - (1, 0, 0, 0, None), - (0, 1, 0, 0, 1.1), - (0, 0, 1, 0, None), - (0, 0, 0, 1, None), - (1, 1, 0, 0, None), - (1, 0, 1, 0, 1.2), - (1, 0, 0, 1, 1.3), - (1, 0, 1, 1, None), + (True, True, True, True, None), + (False, False, False, False, None), + (True, False, False, False, None), + (False, True, False, False, 1.1), + (False, False, True, False, None), + (False, False, False, True, None), + (True, True, False, False, None), + (True, False, True, False, 1.2), + (True, False, False, True, 1.3), + (True, False, True, True, None), ] for case in cases: m.d1.indicator_var.fix(case[0]) @@ -1550,149 +1575,190 @@ def check_transformed_constraint(self, cons, dis, lb, ind_var): def test_transformed_model_nestedDisjuncts(self): # This test tests *everything* for a simple nested disjunction case. m = models.makeNestedDisjunctions_NestedDisjuncts() + m.LocalVars = Suffix(direction=Suffix.LOCAL) + m.LocalVars[m.d1] = [ + m.d1.binary_indicator_var, + m.d1.d3.binary_indicator_var, + m.d1.d4.binary_indicator_var, + ] hull = TransformationFactory('gdp.hull') hull.apply_to(m) + self.check_transformed_model_nestedDisjuncts( + m, m.d1.d3.binary_indicator_var, m.d1.d4.binary_indicator_var + ) + + # Last, check that there aren't things we weren't expecting + all_cons = list( + m.component_data_objects(Constraint, active=True, descend_into=Block) + ) + # 2 disaggregation constraints for x 0,3 + # + 6 bounds constraints for x 6,8,9,13,14,16 + # + 2 bounds constraints for inner indicator vars 11, 12 + # + 2 exactly-one constraints 1,4 + # + 4 transformed constraints 2,5,7,15 + self.assertEqual(len(all_cons), 16) + + def check_transformed_model_nestedDisjuncts(self, m, d3, d4): + # This function checks all of the 16 constraint expressions from + # transforming models.makeNestedDisjunction_NestedDisjuncts when + # declaring the inner indicator vars (d3 and d4) as local. Note that it + # also is a correct test for the case where the inner indicator vars are + # *not* declared as local, but not a complete one, since there are + # additional constraints in that case (see + # check_transformation_blocks_nestedDisjunctions in common_tests.py). + hull = TransformationFactory('gdp.hull') transBlock = m._pyomo_gdp_hull_reformulation self.assertTrue(transBlock.active) - # outer xor should be on this block + # check outer xor xor = transBlock.disj_xor self.assertIsInstance(xor, Constraint) - self.assertTrue(xor.active) - self.assertEqual(xor.lower, 1) - self.assertEqual(xor.upper, 1) - repn = generate_standard_repn(xor.body) - self.assertTrue(repn.is_linear()) - self.assertEqual(repn.constant, 0) - ct.check_linear_coef(self, repn, m.d1.binary_indicator_var, 1) - ct.check_linear_coef(self, repn, m.d2.binary_indicator_var, 1) + ct.check_obj_in_active_tree(self, xor) + assertExpressionsEqual( + self, xor.expr, m.d1.binary_indicator_var + m.d2.binary_indicator_var == 1 + ) self.assertIs(xor, m.disj.algebraic_constraint) self.assertIs(m.disj, hull.get_src_disjunction(xor)) - # inner xor should be on this block + # check inner xor xor = m.d1.disj2.algebraic_constraint - self.assertIs(xor.parent_block(), transBlock) - self.assertIsInstance(xor, Constraint) - self.assertTrue(xor.active) - self.assertEqual(xor.lower, 0) - self.assertEqual(xor.upper, 0) - repn = generate_standard_repn(xor.body) - self.assertTrue(repn.is_linear()) - self.assertEqual(repn.constant, 0) - ct.check_linear_coef(self, repn, m.d1.d3.binary_indicator_var, 1) - ct.check_linear_coef(self, repn, m.d1.d4.binary_indicator_var, 1) - ct.check_linear_coef(self, repn, m.d1.binary_indicator_var, -1) self.assertIs(m.d1.disj2, hull.get_src_disjunction(xor)) - - # so should both disaggregation constraints - dis = transBlock.disaggregationConstraints - self.assertIsInstance(dis, Constraint) - self.assertTrue(dis.active) - self.assertEqual(len(dis), 2) - self.check_outer_disaggregation_constraint(dis[0], m.x, m.d1, m.d2) - self.assertIs(hull.get_disaggregation_constraint(m.x, m.disj), dis[0]) - self.check_outer_disaggregation_constraint( - dis[1], m.x, m.d1.d3, m.d1.d4, rhs=hull.get_disaggregated_var(m.x, m.d1) - ) - self.assertIs(hull.get_disaggregation_constraint(m.x, m.d1.disj2), dis[1]) - - # we should have four disjunct transformation blocks - disjBlocks = transBlock.relaxedDisjuncts - self.assertTrue(disjBlocks.active) - self.assertEqual(len(disjBlocks), 4) - - ## d1's transformation block - - disj1 = disjBlocks[0] - self.assertTrue(disj1.active) - self.assertIs(disj1, m.d1.transformation_block) - self.assertIs(m.d1, hull.get_src_disjunct(disj1)) - # check the disaggregated x is here - self.assertIsInstance(disj1.disaggregatedVars.x, Var) - self.assertEqual(disj1.disaggregatedVars.x.lb, 0) - self.assertEqual(disj1.disaggregatedVars.x.ub, 2) - self.assertIs(disj1.disaggregatedVars.x, hull.get_disaggregated_var(m.x, m.d1)) - self.assertIs(m.x, hull.get_src_var(disj1.disaggregatedVars.x)) - # check the bounds constraints - self.check_bounds_constraint_ub( - disj1.x_bounds, 2, disj1.disaggregatedVars.x, m.d1.indicator_var - ) - # transformed constraint x >= 1 - cons = hull.get_transformed_constraints(m.d1.c) - self.check_transformed_constraint( - cons, disj1.disaggregatedVars.x, 1, m.d1.indicator_var + xor = hull.get_transformed_constraints(xor) + self.assertEqual(len(xor), 1) + xor = xor[0] + ct.check_obj_in_active_tree(self, xor) + xor_expr = self.simplify_cons(xor) + assertExpressionsEqual( + self, xor_expr, d3 + d4 - m.d1.binary_indicator_var == 0.0 ) - ## d2's transformation block + # check disaggregation constraints + x_d3 = hull.get_disaggregated_var(m.x, m.d1.d3) + x_d4 = hull.get_disaggregated_var(m.x, m.d1.d4) + x_d1 = hull.get_disaggregated_var(m.x, m.d1) + x_d2 = hull.get_disaggregated_var(m.x, m.d2) + for x in [x_d1, x_d2, x_d3, x_d4]: + self.assertEqual(x.lb, 0) + self.assertEqual(x.ub, 2) + # Inner disjunction + cons = hull.get_disaggregation_constraint(m.x, m.d1.disj2) + ct.check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual(self, cons_expr, x_d1 - x_d3 - x_d4 == 0.0) + # Outer disjunction + cons = hull.get_disaggregation_constraint(m.x, m.disj) + ct.check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual(self, cons_expr, m.x - x_d1 - x_d2 == 0.0) - disj2 = disjBlocks[1] - self.assertTrue(disj2.active) - self.assertIs(disj2, m.d2.transformation_block) - self.assertIs(m.d2, hull.get_src_disjunct(disj2)) - # disaggregated var - x2 = disj2.disaggregatedVars.x - self.assertIsInstance(x2, Var) - self.assertEqual(x2.lb, 0) - self.assertEqual(x2.ub, 2) - self.assertIs(hull.get_disaggregated_var(m.x, m.d2), x2) - self.assertIs(hull.get_src_var(x2), m.x) - # bounds constraint - x_bounds = disj2.x_bounds - self.check_bounds_constraint_ub(x_bounds, 2, x2, m.d2.binary_indicator_var) - # transformed constraint x >= 1.1 - cons = hull.get_transformed_constraints(m.d2.c) - self.check_transformed_constraint(cons, x2, 1.1, m.d2.binary_indicator_var) - - ## d1.d3's transformation block - - disj3 = disjBlocks[2] - self.assertTrue(disj3.active) - self.assertIs(disj3, m.d1.d3.transformation_block) - self.assertIs(m.d1.d3, hull.get_src_disjunct(disj3)) - # disaggregated var - x3 = disj3.disaggregatedVars.x - self.assertIsInstance(x3, Var) - self.assertEqual(x3.lb, 0) - self.assertEqual(x3.ub, 2) - self.assertIs(hull.get_disaggregated_var(m.x, m.d1.d3), x3) - self.assertIs(hull.get_src_var(x3), m.x) - # bounds constraints - self.check_bounds_constraint_ub( - disj3.x_bounds, 2, x3, m.d1.d3.binary_indicator_var - ) - # transformed x >= 1.2 + ## Transformed constraints cons = hull.get_transformed_constraints(m.d1.d3.c) - self.check_transformed_constraint(cons, x3, 1.2, m.d1.d3.binary_indicator_var) - - ## d1.d4's transformation block - - disj4 = disjBlocks[3] - self.assertTrue(disj4.active) - self.assertIs(disj4, m.d1.d4.transformation_block) - self.assertIs(m.d1.d4, hull.get_src_disjunct(disj4)) - # disaggregated var - x4 = disj4.disaggregatedVars.x - self.assertIsInstance(x4, Var) - self.assertEqual(x4.lb, 0) - self.assertEqual(x4.ub, 2) - self.assertIs(hull.get_disaggregated_var(m.x, m.d1.d4), x4) - self.assertIs(hull.get_src_var(x4), m.x) - # bounds constraints - self.check_bounds_constraint_ub( - disj4.x_bounds, 2, x4, m.d1.d4.binary_indicator_var - ) - # transformed x >= 1.3 + self.assertEqual(len(cons), 1) + cons = cons[0] + ct.check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_leq_cons(cons) + assertExpressionsEqual(self, cons_expr, 1.2 * d3 - x_d3 <= 0.0) + cons = hull.get_transformed_constraints(m.d1.d4.c) - self.check_transformed_constraint(cons, x4, 1.3, m.d1.d4.binary_indicator_var) + self.assertEqual(len(cons), 1) + cons = cons[0] + ct.check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_leq_cons(cons) + assertExpressionsEqual(self, cons_expr, 1.3 * d4 - x_d4 <= 0.0) + + cons = hull.get_transformed_constraints(m.d1.c) + self.assertEqual(len(cons), 1) + cons = cons[0] + ct.check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_leq_cons(cons) + assertExpressionsEqual( + self, cons_expr, 1.0 * m.d1.binary_indicator_var - x_d1 <= 0.0 + ) + + cons = hull.get_transformed_constraints(m.d2.c) + self.assertEqual(len(cons), 1) + cons = cons[0] + ct.check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_leq_cons(cons) + assertExpressionsEqual( + self, cons_expr, 1.1 * m.d2.binary_indicator_var - x_d2 <= 0.0 + ) + + ## Bounds constraints + cons = hull.get_var_bounds_constraint(x_d1) + # the lb is trivial in this case, so we just have 1 + self.assertEqual(len(cons), 1) + ct.check_obj_in_active_tree(self, cons['ub']) + cons_expr = self.simplify_leq_cons(cons['ub']) + assertExpressionsEqual( + self, cons_expr, x_d1 - 2 * m.d1.binary_indicator_var <= 0.0 + ) + cons = hull.get_var_bounds_constraint(x_d2) + # the lb is trivial in this case, so we just have 1 + self.assertEqual(len(cons), 1) + ct.check_obj_in_active_tree(self, cons['ub']) + cons_expr = self.simplify_leq_cons(cons['ub']) + assertExpressionsEqual( + self, cons_expr, x_d2 - 2 * m.d2.binary_indicator_var <= 0.0 + ) + cons = hull.get_var_bounds_constraint(x_d3, m.d1.d3) + # the lb is trivial in this case, so we just have 1 + self.assertEqual(len(cons), 1) + # And we know it has actually been transformed again, so get that one + cons = hull.get_transformed_constraints(cons['ub']) + self.assertEqual(len(cons), 1) + ub = cons[0] + ct.check_obj_in_active_tree(self, ub) + cons_expr = self.simplify_leq_cons(ub) + assertExpressionsEqual(self, cons_expr, x_d3 - 2 * d3 <= 0.0) + cons = hull.get_var_bounds_constraint(x_d4, m.d1.d4) + # the lb is trivial in this case, so we just have 1 + self.assertEqual(len(cons), 1) + # And we know it has actually been transformed again, so get that one + cons = hull.get_transformed_constraints(cons['ub']) + self.assertEqual(len(cons), 1) + ub = cons[0] + ct.check_obj_in_active_tree(self, ub) + cons_expr = self.simplify_leq_cons(ub) + assertExpressionsEqual(self, cons_expr, x_d4 - 2 * d4 <= 0.0) + cons = hull.get_var_bounds_constraint(x_d3, m.d1) + self.assertEqual(len(cons), 1) + ub = cons['ub'] + ct.check_obj_in_active_tree(self, ub) + cons_expr = self.simplify_leq_cons(ub) + assertExpressionsEqual( + self, cons_expr, x_d3 - 2 * m.d1.binary_indicator_var <= 0.0 + ) + cons = hull.get_var_bounds_constraint(x_d4, m.d1) + self.assertEqual(len(cons), 1) + ub = cons['ub'] + ct.check_obj_in_active_tree(self, ub) + cons_expr = self.simplify_leq_cons(ub) + assertExpressionsEqual( + self, cons_expr, x_d4 - 2 * m.d1.binary_indicator_var <= 0.0 + ) + + # Bounds constraints for local vars + cons = hull.get_var_bounds_constraint(d3) + ct.check_obj_in_active_tree(self, cons['ub']) + assertExpressionsEqual(self, cons['ub'].expr, d3 <= m.d1.binary_indicator_var) + cons = hull.get_var_bounds_constraint(d4) + ct.check_obj_in_active_tree(self, cons['ub']) + assertExpressionsEqual(self, cons['ub'].expr, d4 <= m.d1.binary_indicator_var) @unittest.skipIf(not linear_solvers, "No linear solver available") def test_solve_nested_model(self): # This is really a test that our variable references have all been moved # up correctly. m = models.makeNestedDisjunctions_NestedDisjuncts() - + m.LocalVars = Suffix(direction=Suffix.LOCAL) + m.LocalVars[m.d1] = [ + m.d1.binary_indicator_var, + m.d1.d3.binary_indicator_var, + m.d1.d4.binary_indicator_var, + ] hull = TransformationFactory('gdp.hull') m_hull = hull.create_using(m) @@ -1722,10 +1788,10 @@ def test_disaggregated_vars_are_set_to_0_correctly(self): hull.apply_to(m) # this should be a feasible integer solution - m.d1.indicator_var.fix(0) - m.d2.indicator_var.fix(1) - m.d3.indicator_var.fix(0) - m.d4.indicator_var.fix(0) + m.d1.indicator_var.fix(False) + m.d2.indicator_var.fix(True) + m.d3.indicator_var.fix(False) + m.d4.indicator_var.fix(False) results = SolverFactory(linear_solvers[0]).solve(m) self.assertEqual( @@ -1739,10 +1805,10 @@ def test_disaggregated_vars_are_set_to_0_correctly(self): self.assertEqual(value(hull.get_disaggregated_var(m.x, m.d4)), 0) # and what if one of the inner disjuncts is true? - m.d1.indicator_var.fix(1) - m.d2.indicator_var.fix(0) - m.d3.indicator_var.fix(1) - m.d4.indicator_var.fix(0) + m.d1.indicator_var.fix(True) + m.d2.indicator_var.fix(False) + m.d3.indicator_var.fix(True) + m.d4.indicator_var.fix(False) results = SolverFactory(linear_solvers[0]).solve(m) self.assertEqual( @@ -1787,6 +1853,11 @@ def d_r(e): e.c1 = Constraint(expr=e.lambdas[1] + e.lambdas[2] == 1) e.c2 = Constraint(expr=m.x == 2 * e.lambdas[1] + 3 * e.lambdas[2]) + d.LocalVars = Suffix(direction=Suffix.LOCAL) + d.LocalVars[d] = [ + d.d_l.indicator_var.get_associated_binary(), + d.d_r.indicator_var.get_associated_binary(), + ] d.inner_disj = Disjunction(expr=[d.d_l, d.d_r]) m.disj = Disjunction(expr=[m.d_l, m.d_r]) @@ -1809,28 +1880,159 @@ def d_r(e): cons = hull.get_transformed_constraints(d.c1) self.assertEqual(len(cons), 1) convex_combo = cons[0] + convex_combo_expr = self.simplify_cons(convex_combo) assertExpressionsEqual( self, - convex_combo.expr, - lambda1 + lambda2 - (1 - d.indicator_var.get_associated_binary()) * 0.0 - == d.indicator_var.get_associated_binary(), + convex_combo_expr, + lambda1 + lambda2 - d.indicator_var.get_associated_binary() == 0.0, ) cons = hull.get_transformed_constraints(d.c2) self.assertEqual(len(cons), 1) get_x = cons[0] + get_x_expr = self.simplify_cons(get_x) assertExpressionsEqual( - self, - get_x.expr, - x - - (2 * lambda1 + 3 * lambda2) - - (1 - d.indicator_var.get_associated_binary()) * 0.0 - == 0.0 * d.indicator_var.get_associated_binary(), + self, get_x_expr, x - 2 * lambda1 - 3 * lambda2 == 0.0 ) cons = hull.get_disaggregation_constraint(m.x, m.disj) assertExpressionsEqual(self, cons.expr, m.x == x1 + x2) cons = hull.get_disaggregation_constraint(m.x, m.d_r.inner_disj) - assertExpressionsEqual(self, cons.expr, x2 == x3 + x4) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual(self, cons_expr, x2 - x3 - x4 == 0.0) + + def test_nested_with_var_that_does_not_appear_in_every_disjunct(self): + m = ConcreteModel() + m.x = Var(bounds=(0, 10)) + m.y = Var(bounds=(-4, 5)) + m.parent1 = Disjunct() + m.parent2 = Disjunct() + m.parent2.c = Constraint(expr=m.x == 0) + m.parent_disjunction = Disjunction(expr=[m.parent1, m.parent2]) + m.child1 = Disjunct() + m.child1.c = Constraint(expr=m.x <= 8) + m.child2 = Disjunct() + m.child2.c = Constraint(expr=m.x + m.y <= 3) + m.child3 = Disjunct() + m.child3.c = Constraint(expr=m.x <= 7) + m.parent1.disjunction = Disjunction(expr=[m.child1, m.child2, m.child3]) + + hull = TransformationFactory('gdp.hull') + hull.apply_to(m) + + y_c2 = hull.get_disaggregated_var(m.y, m.child2) + self.assertEqual(y_c2.bounds, (-4, 5)) + other_y = hull.get_disaggregated_var(m.y, m.child1) + self.assertEqual(other_y.bounds, (-4, 5)) + other_other_y = hull.get_disaggregated_var(m.y, m.child3) + self.assertIs(other_y, other_other_y) + y_p1 = hull.get_disaggregated_var(m.y, m.parent1) + self.assertEqual(y_p1.bounds, (-4, 5)) + y_p2 = hull.get_disaggregated_var(m.y, m.parent2) + self.assertEqual(y_p2.bounds, (-4, 5)) + + y_cons = hull.get_disaggregation_constraint(m.y, m.parent1.disjunction) + # check that the disaggregated ys in the nested just sum to the original + y_cons_expr = self.simplify_cons(y_cons) + assertExpressionsEqual(self, y_cons_expr, y_p1 - other_y - y_c2 == 0.0) + y_cons = hull.get_disaggregation_constraint(m.y, m.parent_disjunction) + y_cons_expr = self.simplify_cons(y_cons) + assertExpressionsEqual(self, y_cons_expr, m.y - y_p2 - y_p1 == 0.0) + + x_c1 = hull.get_disaggregated_var(m.x, m.child1) + x_c2 = hull.get_disaggregated_var(m.x, m.child2) + x_c3 = hull.get_disaggregated_var(m.x, m.child3) + x_p1 = hull.get_disaggregated_var(m.x, m.parent1) + x_p2 = hull.get_disaggregated_var(m.x, m.parent2) + x_cons_parent = hull.get_disaggregation_constraint(m.x, m.parent_disjunction) + assertExpressionsEqual(self, x_cons_parent.expr, m.x == x_p1 + x_p2) + x_cons_child = hull.get_disaggregation_constraint(m.x, m.parent1.disjunction) + x_cons_child_expr = self.simplify_cons(x_cons_child) + assertExpressionsEqual( + self, x_cons_child_expr, x_p1 - x_c1 - x_c2 - x_c3 == 0.0 + ) + + def simplify_cons(self, cons): + visitor = LinearRepnVisitor({}, {}, {}, None) + lb = cons.lower + ub = cons.upper + self.assertEqual(cons.lb, cons.ub) + repn = visitor.walk_expression(cons.body) + self.assertIsNone(repn.nonlinear) + return repn.to_expression(visitor) == lb + + def simplify_leq_cons(self, cons): + visitor = LinearRepnVisitor({}, {}, {}, None) + self.assertIsNone(cons.lower) + ub = cons.upper + repn = visitor.walk_expression(cons.body) + self.assertIsNone(repn.nonlinear) + return repn.to_expression(visitor) <= ub + + def test_nested_with_var_that_skips_a_level(self): + m = ConcreteModel() + + m.x = Var(bounds=(-2, 9)) + m.y = Var(bounds=(-3, 8)) + + m.y1 = Disjunct() + m.y1.c1 = Constraint(expr=m.x >= 4) + m.y1.z1 = Disjunct() + m.y1.z1.c1 = Constraint(expr=m.y == 2) + m.y1.z1.w1 = Disjunct() + m.y1.z1.w1.c1 = Constraint(expr=m.x == 3) + m.y1.z1.w2 = Disjunct() + m.y1.z1.w2.c1 = Constraint(expr=m.x >= 1) + m.y1.z1.disjunction = Disjunction(expr=[m.y1.z1.w1, m.y1.z1.w2]) + m.y1.z2 = Disjunct() + m.y1.z2.c1 = Constraint(expr=m.y == 1) + m.y1.disjunction = Disjunction(expr=[m.y1.z1, m.y1.z2]) + m.y2 = Disjunct() + m.y2.c1 = Constraint(expr=m.x == 4) + m.disjunction = Disjunction(expr=[m.y1, m.y2]) + + hull = TransformationFactory('gdp.hull') + hull.apply_to(m) + + x_y1 = hull.get_disaggregated_var(m.x, m.y1) + x_y2 = hull.get_disaggregated_var(m.x, m.y2) + x_z1 = hull.get_disaggregated_var(m.x, m.y1.z1) + x_z2 = hull.get_disaggregated_var(m.x, m.y1.z2) + x_w1 = hull.get_disaggregated_var(m.x, m.y1.z1.w1) + x_w2 = hull.get_disaggregated_var(m.x, m.y1.z1.w2) + + y_z1 = hull.get_disaggregated_var(m.y, m.y1.z1) + y_z2 = hull.get_disaggregated_var(m.y, m.y1.z2) + y_y1 = hull.get_disaggregated_var(m.y, m.y1) + y_y2 = hull.get_disaggregated_var(m.y, m.y2) + + cons = hull.get_disaggregation_constraint(m.x, m.y1.z1.disjunction) + self.assertTrue(cons.active) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual(self, cons_expr, x_z1 - x_w1 - x_w2 == 0.0) + cons = hull.get_disaggregation_constraint(m.x, m.y1.disjunction) + self.assertTrue(cons.active) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual(self, cons_expr, x_y1 - x_z2 - x_z1 == 0.0) + cons = hull.get_disaggregation_constraint(m.x, m.disjunction) + self.assertTrue(cons.active) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual(self, cons_expr, m.x - x_y1 - x_y2 == 0.0) + cons = hull.get_disaggregation_constraint( + m.y, m.y1.z1.disjunction, raise_exception=False + ) + self.assertIsNone(cons) + cons = hull.get_disaggregation_constraint(m.y, m.y1.disjunction) + self.assertTrue(cons.active) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual(self, cons_expr, y_y1 - y_z1 - y_z2 == 0.0) + cons = hull.get_disaggregation_constraint(m.y, m.disjunction) + self.assertTrue(cons.active) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual(self, cons_expr, m.y - y_y2 - y_y1 == 0.0) + + @unittest.skipUnless(gurobi_available, "Gurobi is not available") + def test_do_not_assume_nested_indicators_local(self): + ct.check_do_not_assume_nested_indicators_local(self, 'gdp.hull') class TestSpecialCases(unittest.TestCase): @@ -2100,20 +2302,13 @@ def test_mapping_method_errors(self): hull = TransformationFactory('gdp.hull') hull.apply_to(m) - log = StringIO() - with LoggingIntercept(log, 'pyomo.gdp.hull', logging.ERROR): - self.assertRaisesRegex( - AttributeError, - "'NoneType' object has no attribute 'parent_block'", - hull.get_var_bounds_constraint, - m.w, - ) - self.assertRegex( - log.getvalue(), + with self.assertRaisesRegex( + GDP_Error, ".*Either 'w' is not a disaggregated variable, " "or the disjunction that disaggregates it has " "not been properly transformed.", - ) + ): + hull.get_var_bounds_constraint(m.w) log = StringIO() with LoggingIntercept(log, 'pyomo.gdp.hull', logging.ERROR): @@ -2134,36 +2329,22 @@ def test_mapping_method_errors(self): r"Disjunction 'disjunction'", ) - log = StringIO() - with LoggingIntercept(log, 'pyomo.gdp.hull', logging.ERROR): - self.assertRaisesRegex( - AttributeError, - "'NoneType' object has no attribute 'parent_block'", - hull.get_src_var, - m.w, - ) - self.assertRegex( - log.getvalue(), ".*'w' does not appear to be a disaggregated variable" - ) + with self.assertRaisesRegex( + GDP_Error, ".*'w' does not appear to be a disaggregated variable" + ): + hull.get_src_var(m.w) - log = StringIO() - with LoggingIntercept(log, 'pyomo.gdp.hull', logging.ERROR): - self.assertRaisesRegex( - KeyError, - r".*_pyomo_gdp_hull_reformulation.relaxedDisjuncts\[1\]." - r"disaggregatedVars.w", - hull.get_disaggregated_var, - m.d[1].transformation_block.disaggregatedVars.w, - m.d[1], - ) - self.assertRegex( - log.getvalue(), + with self.assertRaisesRegex( + GDP_Error, r".*It does not appear " r"'_pyomo_gdp_hull_reformulation." r"relaxedDisjuncts\[1\].disaggregatedVars.w' " r"is a variable that appears in disjunct " r"'d\[1\]'", - ) + ): + hull.get_disaggregated_var( + m.d[1].transformation_block.disaggregatedVars.w, m.d[1] + ) m.random_disjunction = Disjunction(expr=[m.w == 2, m.w >= 7]) self.assertRaisesRegex( @@ -2398,12 +2579,12 @@ def OneCentroidPerPt(m, i): TransformationFactory('gdp.hull').apply_to(m) # fix an optimal solution - m.AssignPoint[1, 1].indicator_var.fix(1) - m.AssignPoint[1, 2].indicator_var.fix(0) - m.AssignPoint[2, 1].indicator_var.fix(0) - m.AssignPoint[2, 2].indicator_var.fix(1) - m.AssignPoint[3, 1].indicator_var.fix(1) - m.AssignPoint[3, 2].indicator_var.fix(0) + m.AssignPoint[1, 1].indicator_var.fix(True) + m.AssignPoint[1, 2].indicator_var.fix(False) + m.AssignPoint[2, 1].indicator_var.fix(False) + m.AssignPoint[2, 2].indicator_var.fix(True) + m.AssignPoint[3, 1].indicator_var.fix(True) + m.AssignPoint[3, 2].indicator_var.fix(False) m.cluster_center[1].fix(0.3059) m.cluster_center[2].fix(0.8043) diff --git a/pyomo/gdp/util.py b/pyomo/gdp/util.py index 343b2fd4f42..57eef29eded 100644 --- a/pyomo/gdp/util.py +++ b/pyomo/gdp/util.py @@ -169,7 +169,10 @@ def parent_disjunct(self, u): Arg: u : A node in the forest """ - return self.parent(self.parent(u)) + if u.ctype is Disjunct: + return self.parent(self.parent(u)) + else: + return self.parent(u) def root_disjunct(self, u): """Returns the highest parent Disjunct in the hierarchy, or None if @@ -183,7 +186,7 @@ def root_disjunct(self, u): while True: if parent is None: return rootmost_disjunct - if isinstance(parent, _DisjunctData) or parent.ctype is Disjunct: + if parent.ctype is Disjunct: rootmost_disjunct = parent parent = self.parent(parent) @@ -243,7 +246,7 @@ def leaves(self): @property def disjunct_nodes(self): for v in self._vertices: - if isinstance(v, _DisjunctData) or v.ctype is Disjunct: + if v.ctype is Disjunct: yield v