diff --git a/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py b/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py index 1806b96e0ec..5fa9739758e 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py +++ b/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py @@ -45,9 +45,7 @@ def __init__( self.num_solutions = num_solutions def cut_generator_callback(self, cb_m, cb_opt, cb_where): - from gurobipy import GRB - - if cb_where == GRB.Callback.MIPSOL: + if cb_where == gurobipy.GRB.Callback.MIPSOL: cb_opt.cbGetSolution(vars=self.variables) logger.info("***FOUND SOLUTION***") diff --git a/pyomo/contrib/alternative_solutions/solnpool.py b/pyomo/contrib/alternative_solutions/solnpool.py index 51acb57c8a5..a1ecbc55ba5 100644 --- a/pyomo/contrib/alternative_solutions/solnpool.py +++ b/pyomo/contrib/alternative_solutions/solnpool.py @@ -15,8 +15,6 @@ from pyomo.common.dependencies import attempt_import -gurobipy, gurobipy_available = attempt_import("gurobipy") - import pyomo.environ as pe from pyomo.contrib import appsi import pyomo.contrib.alternative_solutions.aos_utils as aos_utils @@ -67,10 +65,7 @@ def gurobi_generate_solutions( # # Setup gurobi # - if not gurobipy_available: - raise pyomo.common.errors.ApplicationError("Solver (gurobi) not available") opt = appsi.solvers.Gurobi() - if not opt.available(): raise pyomo.common.errors.ApplicationError("Solver (gurobi) not available") diff --git a/pyomo/contrib/alternative_solutions/tests/test_lp_enum_solnpool.py b/pyomo/contrib/alternative_solutions/tests/test_lp_enum_solnpool.py index 6d3b5211f9e..ee9f4657acf 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_lp_enum_solnpool.py +++ b/pyomo/contrib/alternative_solutions/tests/test_lp_enum_solnpool.py @@ -9,25 +9,29 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import pyomo.environ as pe -import pyomo.opt +from pyomo.common.dependencies import numpy_available +from pyomo.common import unittest import pyomo.contrib.alternative_solutions.tests.test_cases as tc from pyomo.contrib.alternative_solutions import lp_enum from pyomo.contrib.alternative_solutions import lp_enum_solnpool +from pyomo.opt import check_available_solvers -from pyomo.common.dependencies import attempt_import +import pyomo.environ as pe -numpy, numpy_available = attempt_import("numpy") -gurobipy, gurobi_available = attempt_import("gurobipy") +# lp_enum_solnpool uses both 'gurobi' and 'appsi_gurobi' +gurobi_available = len(check_available_solvers('gurobi', 'appsi_gurobi')) == 2 # # TODO: Setup detailed tests here # -def test_here(): - if numpy_available: +@unittest.skipUnless(gurobi_available, "Gurobi MIP solver not available") +@unittest.skipUnless(numpy_available, "NumPy not found") +class TestLPEnumSolnpool(unittest.TestCase): + + def test_here(self): n = tc.get_pentagonal_pyramid_mip() n.x.domain = pe.Reals n.y.domain = pe.Reals diff --git a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py index 0b9914a86dd..7e601906a69 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py +++ b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py @@ -9,21 +9,17 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pyomo.common.dependencies import numpy as numpy, numpy_available - -if numpy_available: - from numpy.testing import assert_array_almost_equal -from pyomo.common.dependencies import attempt_import - -gurobipy, gurobipy_available = attempt_import("gurobipy") - from collections import Counter -import pyomo.environ as pe +from pyomo.common.dependencies import numpy as np, numpy_available from pyomo.common import unittest - from pyomo.contrib.alternative_solutions import gurobi_generate_solutions +from pyomo.contrib.appsi.solvers import Gurobi + import pyomo.contrib.alternative_solutions.tests.test_cases as tc +import pyomo.environ as pe + +gurobipy_available = Gurobi().available() @unittest.skipIf(not gurobipy_available, "Gurobi MIP solver not available") @@ -51,7 +47,7 @@ def test_ip_feasibility(self): objectives = [round(result.objective[1], 2) for result in results] actual_solns_by_obj = m.num_ranked_solns unique_solns_by_obj = [val for val in Counter(objectives).values()] - assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) + np.testing.assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) @unittest.skipIf(not numpy_available, "Numpy not installed") def test_ip_num_solutions(self): @@ -66,7 +62,7 @@ def test_ip_num_solutions(self): objectives = [round(result.objective[1], 2) for result in results] actual_solns_by_obj = [6, 2] unique_solns_by_obj = [val for val in Counter(objectives).values()] - assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) + np.testing.assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) @unittest.skipIf(not numpy_available, "Numpy not installed") def test_mip_feasibility(self): @@ -80,7 +76,7 @@ def test_mip_feasibility(self): objectives = [round(result.objective[1], 2) for result in results] actual_solns_by_obj = m.num_ranked_solns unique_solns_by_obj = [val for val in Counter(objectives).values()] - assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) + np.testing.assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) @unittest.skipIf(not numpy_available, "Numpy not installed") def test_mip_rel_feasibility(self): @@ -95,7 +91,7 @@ def test_mip_rel_feasibility(self): objectives = [round(result.objective[1], 2) for result in results] actual_solns_by_obj = m.num_ranked_solns[0:2] unique_solns_by_obj = [val for val in Counter(objectives).values()] - assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) + np.testing.assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) @unittest.skipIf(not numpy_available, "Numpy not installed") def test_mip_rel_feasibility_options(self): @@ -112,7 +108,7 @@ def test_mip_rel_feasibility_options(self): objectives = [round(result.objective[1], 2) for result in results] actual_solns_by_obj = m.num_ranked_solns[0:2] unique_solns_by_obj = [val for val in Counter(objectives).values()] - assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) + np.testing.assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) @unittest.skipIf(not numpy_available, "Numpy not installed") def test_mip_abs_feasibility(self): @@ -127,7 +123,7 @@ def test_mip_abs_feasibility(self): objectives = [round(result.objective[1], 2) for result in results] actual_solns_by_obj = m.num_ranked_solns[0:3] unique_solns_by_obj = [val for val in Counter(objectives).values()] - assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) + np.testing.assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) @unittest.skipIf(True, "Ignoring fragile test for solver timeout.") def test_mip_no_time(self): diff --git a/pyomo/contrib/alternative_solutions/tests/test_solution.py b/pyomo/contrib/alternative_solutions/tests/test_solution.py index 9df9374daef..a3ef042b5fe 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_solution.py +++ b/pyomo/contrib/alternative_solutions/tests/test_solution.py @@ -15,8 +15,8 @@ import pyomo.contrib.alternative_solutions.aos_utils as au from pyomo.contrib.alternative_solutions import Solution -pyomo.opt.check_available_solvers("gurobi") mip_solver = "gurobi" +mip_available = pyomo.opt.check_available_solvers(mip_solver) class TestSolutionUnit(unittest.TestCase): @@ -40,10 +40,7 @@ def get_model(self): m.con_z = pe.Constraint(expr=m.z <= 3) return m - @unittest.skipUnless( - pe.SolverFactory(mip_solver).available(exception_flag=False), - "MIP solver not available", - ) + @unittest.skipUnless(mip_available, "MIP solver not available") def test_solution(self): """ Create a Solution Object, call its functions, and ensure the correct diff --git a/pyomo/contrib/pynumero/sparse/block_vector.py b/pyomo/contrib/pynumero/sparse/block_vector.py index b636dd74203..bcb5e786d08 100644 --- a/pyomo/contrib/pynumero/sparse/block_vector.py +++ b/pyomo/contrib/pynumero/sparse/block_vector.py @@ -38,7 +38,7 @@ def assert_block_structure(vec): raise NotFullyDefinedBlockVectorError(msg) -class BlockVector(np.ndarray, BaseBlockVector): +class BlockVector(BaseBlockVector, np.ndarray): """ Structured vector interface. This interface can be used to perform operations on vectors composed by vectors. For example, @@ -1592,86 +1592,3 @@ def toMPIBlockVector(self, rank_ownership, mpi_comm, assert_correct_owners=False mpi_bv.set_block(bid, self.get_block(bid)) return mpi_bv - - # the following methods are not supported by blockvector - - def argpartition(self, kth, axis=-1, kind='introselect', order=None): - BaseBlockVector.argpartition(self, kth, axis=axis, kind=kind, order=order) - - def argsort(self, axis=-1, kind='quicksort', order=None): - BaseBlockVector.argsort(self, axis=axis, kind=kind, order=order) - - def byteswap(self, inplace=False): - BaseBlockVector.byteswap(self, inplace=inplace) - - def choose(self, choices, out=None, mode='raise'): - BaseBlockVector.choose(self, choices, out=out, mode=mode) - - def diagonal(self, offset=0, axis1=0, axis2=1): - BaseBlockVector.diagonal(self, offset=offset, axis1=axis1, axis2=axis2) - - def dump(self, file): - BaseBlockVector.dump(self, file) - - def dumps(self): - BaseBlockVector.dumps(self) - - def getfield(self, dtype, offset=0): - BaseBlockVector.getfield(self, dtype, offset=offset) - - def item(self, *args): - BaseBlockVector.item(self, *args) - - def itemset(self, *args): - BaseBlockVector.itemset(self, *args) - - def newbyteorder(self, new_order='S'): - BaseBlockVector.newbyteorder(self, new_order=new_order) - - def put(self, indices, values, mode='raise'): - BaseBlockVector.put(self, indices, values, mode=mode) - - def partition(self, kth, axis=-1, kind='introselect', order=None): - BaseBlockVector.partition(self, kth, axis=axis, kind=kind, order=order) - - def repeat(self, repeats, axis=None): - BaseBlockVector.repeat(self, repeats, axis=axis) - - def reshape(self, shape, order='C'): - BaseBlockVector.reshape(self, shape, order=order) - - def resize(self, new_shape, refcheck=True): - BaseBlockVector.resize(self, new_shape, refcheck=refcheck) - - def searchsorted(self, v, side='left', sorter=None): - BaseBlockVector.searchsorted(self, v, side=side, sorter=sorter) - - def setfield(self, val, dtype, offset=0): - BaseBlockVector.setfield(self, val, dtype, offset=offset) - - def setflags(self, write=None, align=None, uic=None): - BaseBlockVector.setflags(self, write=write, align=align, uic=uic) - - def sort(self, axis=-1, kind='quicksort', order=None): - BaseBlockVector.sort(self, axis=axis, kind=kind, order=order) - - def squeeze(self, axis=None): - BaseBlockVector.squeeze(self, axis=axis) - - def swapaxes(self, axis1, axis2): - BaseBlockVector.swapaxes(self, axis1, axis2) - - def tobytes(self, order='C'): - BaseBlockVector.tobytes(self, order=order) - - def take(self, indices, axis=None, out=None, mode='raise'): - BaseBlockVector.take(self, indices, axis=axis, out=out, mode=mode) - - def trace(self, offset=0, axis1=0, axis2=1, dtype=None, out=None): - raise NotImplementedError('trace not implemented for BlockVector') - - def transpose(*axes): - BaseBlockVector.transpose(*axes) - - def tostring(order='C'): - BaseBlockVector.tostring(order=order) diff --git a/pyomo/contrib/pynumero/sparse/mpi_block_vector.py b/pyomo/contrib/pynumero/sparse/mpi_block_vector.py index 89cf136a5f7..f86d450a73e 100644 --- a/pyomo/contrib/pynumero/sparse/mpi_block_vector.py +++ b/pyomo/contrib/pynumero/sparse/mpi_block_vector.py @@ -24,7 +24,7 @@ def assert_block_structure(vec): raise NotFullyDefinedBlockVectorError(msg) -class MPIBlockVector(np.ndarray, BaseBlockVector): +class MPIBlockVector(BaseBlockVector, np.ndarray): """ Parallel structured vector interface. This interface can be used to perform parallel operations on vectors composed by vectors. The main @@ -1447,81 +1447,3 @@ def flatten(self, order='C'): def ravel(self, order='C'): raise RuntimeError('Operation not supported by MPIBlockVector') - - def argpartition(self, kth, axis=-1, kind='introselect', order=None): - BaseBlockVector.argpartition(self, kth, axis=axis, kind=kind, order=order) - - def argsort(self, axis=-1, kind='quicksort', order=None): - BaseBlockVector.argsort(self, axis=axis, kind=kind, order=order) - - def byteswap(self, inplace=False): - BaseBlockVector.byteswap(self, inplace=inplace) - - def choose(self, choices, out=None, mode='raise'): - BaseBlockVector.choose(self, choices, out=out, mode=mode) - - def diagonal(self, offset=0, axis1=0, axis2=1): - BaseBlockVector.diagonal(self, offset=offset, axis1=axis1, axis2=axis2) - - def dump(self, file): - BaseBlockVector.dump(self, file) - - def dumps(self): - BaseBlockVector.dumps(self) - - def getfield(self, dtype, offset=0): - BaseBlockVector.getfield(self, dtype, offset=offset) - - def item(self, *args): - BaseBlockVector.item(self, *args) - - def itemset(self, *args): - BaseBlockVector.itemset(self, *args) - - def newbyteorder(self, new_order='S'): - BaseBlockVector.newbyteorder(self, new_order=new_order) - - def put(self, indices, values, mode='raise'): - BaseBlockVector.put(self, indices, values, mode=mode) - - def partition(self, kth, axis=-1, kind='introselect', order=None): - BaseBlockVector.partition(self, kth, axis=axis, kind=kind, order=order) - - def repeat(self, repeats, axis=None): - BaseBlockVector.repeat(self, repeats, axis=axis) - - def reshape(self, shape, order='C'): - BaseBlockVector.reshape(self, shape, order=order) - - def resize(self, new_shape, refcheck=True): - BaseBlockVector.resize(self, new_shape, refcheck=refcheck) - - def searchsorted(self, v, side='left', sorter=None): - BaseBlockVector.searchsorted(self, v, side=side, sorter=sorter) - - def setfield(self, val, dtype, offset=0): - BaseBlockVector.setfield(self, val, dtype, offset=offset) - - def setflags(self, write=None, align=None, uic=None): - BaseBlockVector.setflags(self, write=write, align=align, uic=uic) - - def sort(self, axis=-1, kind='quicksort', order=None): - BaseBlockVector.sort(self, axis=axis, kind=kind, order=order) - - def squeeze(self, axis=None): - BaseBlockVector.squeeze(self, axis=axis) - - def swapaxes(self, axis1, axis2): - BaseBlockVector.swapaxes(self, axis1, axis2) - - def tobytes(self, order='C'): - BaseBlockVector.tobytes(self, order=order) - - def argmax(self, axis=None, out=None): - BaseBlockVector.argmax(self, axis=axis, out=out) - - def argmin(self, axis=None, out=None): - BaseBlockVector.argmax(self, axis=axis, out=out) - - def take(self, indices, axis=None, out=None, mode='raise'): - BaseBlockVector.take(self, indices, axis=axis, out=out, mode=mode) diff --git a/pyomo/core/base/expression.py b/pyomo/core/base/expression.py index a5120759236..64f250e9860 100644 --- a/pyomo/core/base/expression.py +++ b/pyomo/core/base/expression.py @@ -62,7 +62,7 @@ def __call__(self, exception=True): return arg return arg(exception=exception) - def create_node_with_local_data(self, values): + def create_node_with_local_data(self, values, classtype=None): """ Construct a simple expression after constructing the contained expression. @@ -70,7 +70,9 @@ def create_node_with_local_data(self, values): This class provides a consistent interface for constructing a node, which is used in tree visitor scripts. """ - obj = self.__class__() + if classtype is None: + classtype = self.parent_component()._ComponentDataClass + obj = classtype() obj._args_ = values return obj diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 69b21c4d78b..5a15321c084 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1484,18 +1484,7 @@ def _cb_validate_filter(self, mode, val_iter): try: flag = fcn(block, (), *vstar) if flag: - deprecation_warning( - f"{self.__class__.__name__} {self.name}: '{mode}=' " - "callback signature matched (block, *value). " - "Please update the callback to match the signature " - f"(block, value{', *index' if comp.is_indexed() else ''}).", - version='6.8.0', - ) - orig_fcn = fcn._fcn - fcn = ParameterizedScalarCallInitializer( - lambda m, v: orig_fcn(m, *v), True - ) - setattr(comp, '_' + mode, fcn) + self._filter_validate_scalar_api_deprecation(mode, warning=True) yield value continue except TypeError: @@ -1536,6 +1525,21 @@ def _cb_validate_filter(self, mode, val_iter): ) raise exc from None + def _filter_validate_scalar_api_deprecation(self, mode, warning): + comp = self.parent_component() + fcn = getattr(comp, '_' + mode) + if warning: + deprecation_warning( + f"{self.__class__.__name__} {self.name}: '{mode}=' " + "callback signature matched (block, *value). " + "Please update the callback to match the signature " + f"(block, value{', *index' if comp.is_indexed() else ''}).", + version='6.8.0', + ) + orig_fcn = fcn._fcn + fcn = ParameterizedScalarCallInitializer(lambda m, v: orig_fcn(m, *v), True) + setattr(comp, '_' + mode, fcn) + def _cb_normalized_dimen_verifier(self, dimen, val_iter): for value in val_iter: if value.__class__ in native_types: @@ -2256,14 +2260,20 @@ def __init__(self, *args, **kwds): self._init_values._init = CountedCallInitializer( self, self._init_values._init ) - # HACK: the DAT parser needs to know the domain of a set in - # order to correctly parse the data stream. + if not self.is_indexed(): + # HACK: the DAT parser needs to know the domain of a set in + # order to correctly parse the data stream. if self._init_domain.constant(): self._domain = self._init_domain(self.parent_block(), None, self) if self._init_dimen.constant(): self._dimen = self._init_dimen(self.parent_block(), None) + if self._filter.__class__ is ParameterizedIndexedCallInitializer: + self._filter_validate_scalar_api_deprecation('filter', warning=False) + if self._validate.__class__ is ParameterizedIndexedCallInitializer: + self._filter_validate_scalar_api_deprecation('validate', warning=False) + @deprecated( "check_values() is deprecated: Sets only contain valid members", version='5.7' ) diff --git a/pyomo/core/expr/numeric_expr.py b/pyomo/core/expr/numeric_expr.py index 21896c63219..0e67801ae8a 100644 --- a/pyomo/core/expr/numeric_expr.py +++ b/pyomo/core/expr/numeric_expr.py @@ -1094,7 +1094,7 @@ def create_node_with_local_data(self, args, classtype=None): # types, the simplest / fastest thing to do is just defer to # the operator dispatcher. return operator.mul(*args) - return self.__class__(args) + return classtype(args) class DivisionExpression(NumericExpression): diff --git a/pyomo/core/tests/unit/test_expression.py b/pyomo/core/tests/unit/test_expression.py index eb16f7c6142..92cb245fa22 100644 --- a/pyomo/core/tests/unit/test_expression.py +++ b/pyomo/core/tests/unit/test_expression.py @@ -30,6 +30,7 @@ sum_product, ) from pyomo.core.base.expression import ExpressionData +from pyomo.core.base.objective import ObjectiveData from pyomo.core.expr.compare import compare_expressions, assertExpressionsEqual from pyomo.common.tee import capture_output @@ -290,6 +291,36 @@ def obj_rule(model): self.assertEqual(inst.obj.expr(), 3.0) self.assertEqual(id(inst.obj.expr.arg(1)), id(inst.ec)) + def test_create_node_with_local_data(self): + m = ConcreteModel() + m.x = Var() + + m.e = Expression(expr=m.x) + ee = m.e.create_node_with_local_data([5]) + self.assertIsNot(m.e, ee) + self.assertIs(type(ee), ExpressionData) + self.assertEqual(ee._args_, [5]) + + m.f = Expression([0], rule=lambda m, i: m.x) + ff = m.f[0].create_node_with_local_data([5]) + self.assertIsNot(m.f, ff) + self.assertIsNot(m.f[0], ff) + self.assertIs(type(ff), ExpressionData) + self.assertEqual(ff._args_, [5]) + + m.g = Objective(expr=m.x) + gg = m.g.create_node_with_local_data([5]) + self.assertIsNot(m.g, gg) + self.assertIs(type(gg), ObjectiveData) + self.assertEqual(gg._args_, [5]) + + m.h = Objective([0], rule=lambda m, i: m.x) + hh = m.h[0].create_node_with_local_data([5]) + self.assertIsNot(m.h, hh) + self.assertIsNot(m.h[0], hh) + self.assertIs(type(hh), ObjectiveData) + self.assertEqual(hh._args_, [5]) + class TestExpression(unittest.TestCase): def setUp(self): diff --git a/pyomo/core/tests/unit/test_numeric_expr.py b/pyomo/core/tests/unit/test_numeric_expr.py index efb01e6d6ce..ca9fafd482d 100644 --- a/pyomo/core/tests/unit/test_numeric_expr.py +++ b/pyomo/core/tests/unit/test_numeric_expr.py @@ -4313,6 +4313,18 @@ def test_sin(self): total = counter.count - start self.assertEqual(total, 1) + def test_create_node_with_local_data(self): + e = self.m.p * self.m.a + self.assertIs(type(e), MonomialTermExpression) + + f = e.create_node_with_local_data([self.m.b, self.m.p]) + self.assertIs(type(f), MonomialTermExpression) + self.assertStructuredAlmostEqual(f._args_, [self.m.p, self.m.b]) + + g = e.create_node_with_local_data([self.m.b, self.m.p], ProductExpression) + self.assertIs(type(g), ProductExpression) + self.assertStructuredAlmostEqual(g._args_, [self.m.b, self.m.p]) + # # Fixed - Expr has a fixed value diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index 6529c2b60a9..6312aaf63c6 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -4181,6 +4181,19 @@ def test_indexed_set(self): self.assertIs(type(m.I[3]), InsertionOrderSetData) self.assertEqual(m.I.data(), {1: (4, 2, 5), 2: (4, 2, 5), 3: (4, 2, 5)}) + # Explicit (constant dict) construction + m = ConcreteModel() + m.I = Set([1, 2], initialize={1: (4, 2, 5), 2: (7, 6)}) + self.assertEqual(len(m.I), 2) + self.assertEqual(list(m.I[1]), [4, 2, 5]) + self.assertEqual(list(m.I[2]), [7, 6]) + self.assertIsNot(m.I[1], m.I[2]) + self.assertTrue(m.I[1].isordered()) + self.assertTrue(m.I[2].isordered()) + self.assertIs(type(m.I[1]), InsertionOrderSetData) + self.assertIs(type(m.I[2]), InsertionOrderSetData) + self.assertEqual(m.I.data(), {1: (4, 2, 5), 2: (7, 6)}) + # Explicit (constant) construction m = ConcreteModel() m.I = Set([1, 2, 3], initialize=(4, 2, 5), ordered=Set.SortedOrder) @@ -4255,7 +4268,7 @@ def test_indexing(self): def test_add_filter_validate(self): m = ConcreteModel() m.I = Set(domain=Integers) - self.assertIs(m.I.filter, None) + self.assertIs(m.I._filter, None) with self.assertRaisesRegex( ValueError, r"Cannot add value 1.5 to Set I.\n" @@ -4302,7 +4315,7 @@ def _l_tri(model, i, j): return i >= j m.K = Set(initialize=RangeSet(3) * RangeSet(3), filter=_l_tri) - self.assertIsInstance(m.K.filter, ParameterizedScalarCallInitializer) + self.assertIsInstance(m.K._filter, ParameterizedScalarCallInitializer) self.assertEqual(list(m.K), [(1, 1), (2, 1), (2, 2), (3, 1), (3, 2), (3, 3)]) output = StringIO() @@ -4334,6 +4347,18 @@ def _lt_3(model, i): self.assertEqual(output.getvalue(), "") self.assertEqual(list(m.L[2]), [1, 2, 0]) + # This tests that the deprecation path works correctly in the + # case that the callback doesn't raise an error or ever return + # False + + def _l_off_diag(model, i, j): + self.assertIs(model, m) + return i != j + + m.M = Set(initialize=RangeSet(3) * RangeSet(3), filter=_l_off_diag) + self.assertIsInstance(m.M._filter, ParameterizedScalarCallInitializer) + self.assertEqual(list(m.M), [(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]) + m = ConcreteModel() def _validate(model, val): @@ -4374,12 +4399,15 @@ def _validate(model, i, j): m.I2 = Set(validate=_validate) with LoggingIntercept(module='pyomo.core') as output: self.assertTrue(m.I2.add((0, 1))) - self.assertRegex( - output.getvalue().replace('\n', ' '), - r"DEPRECATED: OrderedScalarSet I2: 'validate=' callback " - r"signature matched \(block, \*value\). Please update the " - r"callback to match the signature \(block, value\)", - ) + # Note that we are not emitting a deprecation warning (yet) + # for scalar sets + # self.assertEqual(output.getvalue(), "") + # output.getvalue().replace('\n', ' '), + # r"DEPRECATED: OrderedScalarSet I2: 'validate=' callback " + # r"signature matched \(block, \*value\). Please update the " + # r"callback to match the signature \(block, value\)", + # ) + self.assertEqual(output.getvalue(), "") with LoggingIntercept(module='pyomo.core') as output: with self.assertRaisesRegex( ValueError, diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 854366c0cf0..f910f5775ad 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -20,7 +20,7 @@ from pyomo.common.modeling import unique_component_name from pyomo.core.expr.numvalue import ZeroConstant import pyomo.core.expr as EXPR -from pyomo.core.base import TransformationFactory, Reference +from pyomo.core.base import TransformationFactory from pyomo.core import ( Block, BooleanVar, @@ -447,19 +447,11 @@ def _transform_disjunctionData( disaggregatedVar=disaggregated_var, disjunct=obj, bigmConstraint=disaggregated_var_bounds, - lb_idx=(idx, 'lb'), - ub_idx=(idx, 'ub'), var_free_indicator=var_free, + var_idx=idx, ) - # 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, :] - ) - dis_var_info.original_var_map[disaggregated_var] = var + original_var_info = var.parent_block().private_data() + disaggregated_var_map = original_var_info.disaggregated_var_map # For every Disjunct the Var does not appear in, we want to map # that this new variable is its disaggreggated variable. @@ -544,8 +536,6 @@ def _transform_disjunct( 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 @@ -573,8 +563,6 @@ def _transform_disjunct( 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 @@ -607,10 +595,16 @@ def _declare_disaggregated_var_bounds( disaggregatedVar, disjunct, bigmConstraint, - lb_idx, - ub_idx, var_free_indicator, + var_idx=None, ): + # For updating mappings: + 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() + + disaggregated_var_info.bigm_constraint_map[disaggregatedVar][disjunct] = {} + lb = original_var.lb ub = original_var.ub if lb is None or ub is None: @@ -624,13 +618,21 @@ def _declare_disaggregated_var_bounds( disaggregatedVar.setub(max(0, ub)) if lb: + lb_idx = 'lb' + if var_idx is not None: + lb_idx = (var_idx, 'lb') bigmConstraint.add(lb_idx, var_free_indicator * lb <= disaggregatedVar) + disaggregated_var_info.bigm_constraint_map[disaggregatedVar][disjunct][ + 'lb' + ] = bigmConstraint[lb_idx] if ub: + ub_idx = 'ub' + if var_idx is not None: + ub_idx = (var_idx, '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() + disaggregated_var_info.bigm_constraint_map[disaggregatedVar][disjunct][ + 'ub' + ] = bigmConstraint[ub_idx] # store the mappings from variables to their disaggregated selves on # the transformation block @@ -928,10 +930,9 @@ def get_disaggregation_constraint( 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 - be 0 otherwise. (It is always an IndexedConstraint because each - bound becomes a separate constraint.) + Returns a dictionary mapping keys 'lb' and/or 'ub' to the Constraints that + set a disaggregated variable to be within its lower and upper bounds + (respectively) when its Disjunct is active and to be 0 otherwise. Parameters ---------- diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index 07876a9d213..8a780d4b988 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -32,7 +32,6 @@ Param, Objective, TerminationCondition, - Reference, ) from pyomo.core.expr.compare import ( assertExpressionsEqual, @@ -530,14 +529,18 @@ def test_bigMConstraint_mappings(self): mappings[disjBlock[i].disaggregatedVars.x] = disjBlock[i].x_bounds if i == 1: # this disjunct has x, w, and no y mappings[disjBlock[i].disaggregatedVars.w] = disjBlock[i].w_bounds - mappings[transBlock._disaggregatedVars[0]] = Reference( - transBlock._boundsConstraints[0, ...] - ) + mappings[transBlock._disaggregatedVars[0]] = { + key: val + for key, val in transBlock._boundsConstraints.items() + if key[0] == 0 + } elif i == 0: # this disjunct has x, y, and no w mappings[disjBlock[i].disaggregatedVars.y] = disjBlock[i].y_bounds - mappings[transBlock._disaggregatedVars[1]] = Reference( - transBlock._boundsConstraints[1, ...] - ) + mappings[transBlock._disaggregatedVars[1]] = { + key: val + for key, val in transBlock._boundsConstraints.items() + if key[0] == 1 + } for var, cons in mappings.items(): returned_cons = hull.get_var_bounds_constraint(var) # This sometimes refers a reference to the right part of a @@ -545,6 +548,8 @@ def test_bigMConstraint_mappings(self): # themselves might not be the same object. The ConstraintDatas # are though: for key, constraintData in cons.items(): + if type(key) is tuple: + key = key[1] self.assertIs(returned_cons[key], constraintData) def test_create_using_nonlinear(self): diff --git a/pyomo/solvers/tests/mip/test_qp.py b/pyomo/solvers/tests/mip/test_qp.py index 9c5cb5ffbc4..def2d8c91ec 100644 --- a/pyomo/solvers/tests/mip/test_qp.py +++ b/pyomo/solvers/tests/mip/test_qp.py @@ -53,7 +53,8 @@ def _qp_model(self): return m @unittest.skipUnless( - gurobi_lp.available(exception_flag=False), "needs Gurobi LP interface" + gurobi_lp.available(exception_flag=False) and gurobi_lp.license_is_valid(), + "needs Gurobi LP interface", ) def test_qp_objective_gurobi_lp(self): m = self._qp_model() @@ -61,7 +62,8 @@ def test_qp_objective_gurobi_lp(self): self.assertEqual(m.obj(), results['Problem'][0]['Upper bound']) @unittest.skipUnless( - gurobi_nl.available(exception_flag=False), "needs Gurobi NL interface" + gurobi_nl.available(exception_flag=False) and gurobi_nl.license_is_valid(), + "needs Gurobi NL interface", ) def test_qp_objective_gurobi_nl(self): m = self._qp_model()