diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 0eba6d96310..db75f2e795a 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -141,6 +141,15 @@ jobs: TARGET: linux PYENV: pip + - os: ubuntu-latest + python: 3.12 + other: /numpy2 + slim: 1 + skip_doctest: 1 + TARGET: linux + PYENV: pip + PACKAGES: "gurobipy dill numpy>2.0 scipy networkx" + - os: ubuntu-latest python: 3.9 other: /pyutilib diff --git a/pyomo/contrib/pynumero/interfaces/tests/test_external_pyomo_block.py b/pyomo/contrib/pynumero/interfaces/tests/test_external_pyomo_block.py index 913d3055c9c..1807f24afdd 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/test_external_pyomo_block.py +++ b/pyomo/contrib/pynumero/interfaces/tests/test_external_pyomo_block.py @@ -15,10 +15,10 @@ from pyomo.core.expr.visitor import identify_variables import pyomo.environ as pyo +from pyomo.common.dependencies import networkx_available as nx_available from pyomo.contrib.pynumero.dependencies import ( numpy as np, numpy_available, - scipy, scipy_available, ) @@ -151,6 +151,7 @@ def flow_out_eqn(m, t): class TestExternalGreyBoxBlock(unittest.TestCase): + @unittest.skipUnless(nx_available, "SCCImplicitFunctionSolver requires networkx") def test_construct_scalar(self): m = pyo.ConcreteModel() m.ex_block = ExternalGreyBoxBlock(concrete=True) @@ -171,6 +172,7 @@ def test_construct_scalar(self): self.assertEqual(len(block.outputs), 0) self.assertEqual(len(block._equality_constraint_names), 2) + @unittest.skipUnless(nx_available, "SCCImplicitFunctionSolver requires networkx") def test_construct_indexed(self): block = ExternalGreyBoxBlock([0, 1, 2], concrete=True) self.assertIs(type(block), IndexedExternalGreyBoxBlock) @@ -192,6 +194,7 @@ def test_construct_indexed(self): self.assertEqual(len(b._equality_constraint_names), 2) @unittest.skipUnless(cyipopt_available, "cyipopt is not available") + @unittest.skipUnless(nx_available, "SCCImplicitFunctionSolver requires networkx") def test_solve_square(self): m = pyo.ConcreteModel() m.ex_block = ExternalGreyBoxBlock(concrete=True) @@ -234,6 +237,7 @@ def test_solve_square(self): self.assertAlmostEqual(m_ex.y.value, y.value, delta=1e-8) @unittest.skipUnless(cyipopt_available, "cyipopt is not available") + @unittest.skipUnless(nx_available, "SCCImplicitFunctionSolver requires networkx") def test_optimize(self): m = pyo.ConcreteModel() m.ex_block = ExternalGreyBoxBlock(concrete=True) @@ -292,6 +296,7 @@ def test_optimize(self): self.assertAlmostEqual(m_ex.y.value, y.value, delta=1e-8) @unittest.skipUnless(cyipopt_available, "cyipopt is not available") + @unittest.skipUnless(nx_available, "SCCImplicitFunctionSolver requires networkx") def test_optimize_with_cyipopt_for_inner_problem(self): # Use CyIpopt, rather than the default SciPy solvers, # for the inner problem @@ -427,6 +432,7 @@ def test_optimize_no_decomposition(self): self.assertAlmostEqual(m_ex.x.value, x.value, delta=1e-8) self.assertAlmostEqual(m_ex.y.value, y.value, delta=1e-8) + @unittest.skipUnless(nx_available, "SCCImplicitFunctionSolver requires networkx") def test_construct_dynamic(self): m = make_dynamic_model() time = m.time @@ -504,6 +510,7 @@ def test_construct_dynamic(self): ) @unittest.skipUnless(cyipopt_available, "cyipopt is not available") + @unittest.skipUnless(nx_available, "SCCImplicitFunctionSolver requires networkx") def test_solve_square_dynamic(self): # Create the "external model" m = make_dynamic_model() @@ -571,6 +578,7 @@ def linking_constraint_rule(m, i, t): self.assertStructuredAlmostEqual(values, target_values, delta=1e-5) @unittest.skipUnless(cyipopt_available, "cyipopt is not available") + @unittest.skipUnless(nx_available, "SCCImplicitFunctionSolver requires networkx") def test_optimize_dynamic(self): # Create the "external model" m = make_dynamic_model() @@ -653,6 +661,7 @@ def linking_constraint_rule(m, i, t): self.assertStructuredAlmostEqual(values, target_values, delta=1e-5) @unittest.skipUnless(cyipopt_available, "cyipopt is not available") + @unittest.skipUnless(nx_available, "SCCImplicitFunctionSolver requires networkx") def test_optimize_dynamic_references(self): """ When when pre-existing variables are attached to the EGBB @@ -717,7 +726,8 @@ def test_optimize_dynamic_references(self): self.assertStructuredAlmostEqual(values, target_values, delta=1e-5) -class TestPyomoNLPWithGreyBoxBLocks(unittest.TestCase): +@unittest.skipUnless(nx_available, "SCCImplicitFunctionSolver requires networkx") +class TestPyomoNLPWithGreyBoxBlocks(unittest.TestCase): def test_set_and_evaluate(self): m = pyo.ConcreteModel() m.ex_block = ExternalGreyBoxBlock(concrete=True) diff --git a/pyomo/contrib/pynumero/interfaces/tests/test_external_pyomo_model.py b/pyomo/contrib/pynumero/interfaces/tests/test_external_pyomo_model.py index 9773fa7e4a8..5e7ea023166 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/test_external_pyomo_model.py +++ b/pyomo/contrib/pynumero/interfaces/tests/test_external_pyomo_model.py @@ -13,6 +13,7 @@ import pyomo.common.unittest as unittest import pyomo.environ as pyo +from pyomo.common.dependencies import networkx_available as nx_available from pyomo.contrib.pynumero.dependencies import ( numpy as np, numpy_available, @@ -513,6 +514,7 @@ def test_explicit_zeros(self): np.testing.assert_allclose(hess.data, data, rtol=1e-8) +@unittest.skipUnless(nx_available, "SCCImplicitFunctionSolver requires networkx") class TestExternalPyomoModel(unittest.TestCase): def test_evaluate_SimpleModel1(self): model = SimpleModel1() @@ -838,6 +840,7 @@ def test_evaluate_hessian_lagrangian_SimpleModel2x2_1(self): np.testing.assert_allclose(hess_lag, expected_hess_lag, rtol=1e-8) +@unittest.skipUnless(nx_available, "SCCImplicitFunctionSolver requires networkx") class TestUpdatedHessianCalculationMethods(unittest.TestCase): """ These tests exercise the methods for fast Hessian-of-Lagrangian @@ -1021,6 +1024,7 @@ def test_evaluate_hessian_equality_constraints_order(self): ) +@unittest.skipUnless(nx_available, "SCCImplicitFunctionSolver requires networkx") class TestScaling(unittest.TestCase): def con_3_body(self, x, y, u, v): return 1e5 * x**2 + 1e4 * y**2 + 1e1 * u**2 + 1e0 * v**2 diff --git a/pyomo/contrib/pynumero/sparse/base_block.py b/pyomo/contrib/pynumero/sparse/base_block.py index 0b923ce6efb..1baa10e1f73 100644 --- a/pyomo/contrib/pynumero/sparse/base_block.py +++ b/pyomo/contrib/pynumero/sparse/base_block.py @@ -11,6 +11,8 @@ # These classes are for checking types consistently and raising errors +from ..dependencies import numpy as np + class BaseBlockVector(object): """Base class for block vectors""" @@ -177,3 +179,128 @@ def transpose(self, *axes): def tostring(self, order='C'): msg = "tostring not implemented for {}".format(self.__class__.__name__) raise NotImplementedError(msg) + + +#: NumPy ufuncs that take one vector and are compatible with pyNumero vectors +vec_unary_ufuncs = { + ## MATH ufuncs + np.negative, + np.positive, + np.absolute, + np.fabs, + np.rint, + np.sign, + np.conj, + np.conjugate, + np.exp, + np.exp2, + np.log, + np.log2, + np.log10, + np.expm1, + np.log1p, + np.sqrt, + np.square, + np.cbrt, + np.reciprocal, + ## TRIG ufuncs + np.sin, + np.cos, + np.tan, + np.arcsin, + np.arccos, + np.arctan, + np.sinh, + np.cosh, + np.tanh, + np.arcsinh, + np.arccosh, + np.arctanh, + np.degrees, + np.radians, + np.deg2rad, + np.rad2deg, + ## COMPARISON ufuncs + np.logical_not, + ## BIT-TWIDDLING ufuncs + np.invert, + ## FLOATING ufuncs + np.isfinite, + np.isinf, + np.isnan, + # np.isnat, # only defined for datetime + np.fabs, # numpy docs list here and in MATH + np.signbit, + np.spacing, + # np.modf, # disabled because shape is not preserved + # np.frexp, # disabled because shape is not preserved + np.floor, + np.ceil, + np.trunc, + # OTHER (not listed in ufuncs docs) + np.abs, +} + +#: NumPy ufuncs that take two vectors and are compatible with pyNumero vectors +vec_binary_ufuncs = { + ## MATH ufuncs + np.add, + np.subtract, + np.multiply, + # np.matmult, # disabled because shape is not preserved + np.divide, + np.logaddexp, + np.logaddexp2, + np.true_divide, + np.floor_divide, + np.power, + np.float_power, + np.remainder, + np.mod, + np.fmod, + # np.divmod, # disabled because shape is not preserved + np.heaviside, + np.gcd, + np.lcm, + ## TRIG ufuncs + np.arctan2, + np.hypot, + ## BIT-TWIDDLING ufuncs + np.bitwise_and, + np.bitwise_or, + np.bitwise_xor, + np.left_shift, + np.right_shift, + ## COMPARISON ufuncs + np.greater, + np.greater_equal, + np.less, + np.less_equal, + np.not_equal, + np.equal, + np.logical_and, + np.logical_or, + np.logical_xor, + np.maximum, + np.minimum, + np.fmax, + np.fmin, + ## FLOATING ufincs + np.copysign, + np.nextafter, + np.ldexp, + np.fmod, # numpy docs list here and in MATH +} + +#: NumPy ufuncs can be used as reductions for pyNumero vectors +vec_associative_reductions = { + np.add, + np.multiply, + np.bitwise_and, + np.bitwise_or, + np.bitwise_xor, + np.maximum, + np.minimum, + np.fmax, + np.fmin, +} diff --git a/pyomo/contrib/pynumero/sparse/block_vector.py b/pyomo/contrib/pynumero/sparse/block_vector.py index 37c2d7b2cf7..53fa1d6518c 100644 --- a/pyomo/contrib/pynumero/sparse/block_vector.py +++ b/pyomo/contrib/pynumero/sparse/block_vector.py @@ -57,7 +57,7 @@ * :py:meth:`~numpy.ndarray.conj` * :py:meth:`~numpy.ndarray.conjugate` * :py:meth:`~numpy.ndarray.nonzero` - * :py:meth:`~numpy.ndarray.ptp` + * :py:meth:`~numpy.ndarray.ptp` (NumPy 1.x only) * :py:meth:`~numpy.ndarray.round` * :py:meth:`~numpy.ndarray.std` * :py:meth:`~numpy.ndarray.var` @@ -183,7 +183,12 @@ import operator from ..dependencies import numpy as np -from .base_block import BaseBlockVector +from .base_block import ( + BaseBlockVector, + vec_unary_ufuncs, + vec_binary_ufuncs, + vec_associative_reductions, +) class NotFullyDefinedBlockVectorError(Exception): @@ -265,115 +270,49 @@ def __array_wrap__(self, out_arr, context=None): return super(BlockVector, self).__array_wrap__(self, out_arr, context) def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): - """Runs ufuncs speciallizations to BlockVector""" - # functions that take one vector - unary_funcs = [ - np.log10, - np.sin, - np.cos, - np.exp, - np.ceil, - np.floor, - np.tan, - np.arctan, - np.arcsin, - np.arccos, - np.sinh, - np.cosh, - np.abs, - np.tanh, - np.arccosh, - np.arcsinh, - np.arctanh, - np.fabs, - np.sqrt, - np.log, - np.log2, - np.absolute, - np.isfinite, - np.isinf, - np.isnan, - np.log1p, - np.logical_not, - np.expm1, - np.exp2, - np.sign, - np.rint, - np.square, - np.positive, - np.negative, - np.rad2deg, - np.deg2rad, - np.conjugate, - np.reciprocal, - np.signbit, + """Runs ufuncs specializations to BlockVector""" + if kwargs.get('out', None) is not None: + return NotImplemented + if method == 'reduce' and ufunc in vec_associative_reductions: + (arg,) = inputs + return self._reduction_operation(ufunc, method, arg, kwargs) + if method == '__call__': + if ufunc in vec_unary_ufuncs: + (arg,) = inputs + return self._unary_operation(ufunc, method, arg, kwargs) + if ufunc in vec_binary_ufuncs: + return self._binary_operation(ufunc, method, inputs, kwargs) + return NotImplemented + + def _reduction_operation(self, ufunc, method, x, kwargs): + results = [ + self._unary_operation(ufunc, method, x.get_block(i), kwargs) + for i in range(x.nblocks) ] - - # functions that take two vectors - binary_funcs = [ - np.add, - np.multiply, - np.divide, - np.subtract, - np.greater, - np.greater_equal, - np.less, - np.less_equal, - np.not_equal, - np.maximum, - np.minimum, - np.fmax, - np.fmin, - np.equal, - np.logical_and, - np.logical_or, - np.logical_xor, - np.logaddexp, - np.logaddexp2, - np.remainder, - np.heaviside, - np.hypot, - ] - - args = [input_ for i, input_ in enumerate(inputs)] - outputs = kwargs.pop('out', None) - if outputs is not None: - raise NotImplementedError( - str(ufunc) - + ' cannot be used with BlockVector if the out keyword argument is given.' - ) - - if ufunc in unary_funcs: - results = self._unary_operation(ufunc, method, *args, **kwargs) - return results - elif ufunc in binary_funcs: - results = self._binary_operation(ufunc, method, *args, **kwargs) - return results + if len(results) == 1: + return results[0] else: - raise NotImplementedError(str(ufunc) + "not supported for BlockVector") + return super().__array_ufunc__(ufunc, method, np.array(results), **kwargs) - def _unary_operation(self, ufunc, method, *args, **kwargs): + def _unary_operation(self, ufunc, method, x, kwargs): """Run recursion to perform unary_funcs on BlockVector""" # ToDo: deal with out - x = args[0] if isinstance(x, BlockVector): v = BlockVector(x.nblocks) for i in range(x.nblocks): - _args = [x.get_block(i)] + [args[j] for j in range(1, len(args))] - v.set_block(i, self._unary_operation(ufunc, method, *_args, **kwargs)) + v.set_block( + i, self._unary_operation(ufunc, method, x.get_block(i), kwargs) + ) return v elif type(x) == np.ndarray: - return super(BlockVector, self).__array_ufunc__( - ufunc, method, *args, **kwargs - ) + return super().__array_ufunc__(ufunc, method, x, **kwargs) else: - raise NotImplementedError() + return NotImplemented - def _binary_operation(self, ufunc, method, *args, **kwargs): + def _binary_operation(self, ufunc, method, args, kwargs): """Run recursion to perform binary_funcs on BlockVector""" # ToDo: deal with out - x1 = args[0] - x2 = args[1] + x1, x2 = args if isinstance(x1, BlockVector) and isinstance(x2, BlockVector): assert_block_structure(x1) assert_block_structure(x2) @@ -386,14 +325,8 @@ def _binary_operation(self, ufunc, method, *args, **kwargs): res = BlockVector(x1.nblocks) for i in range(x1.nblocks): - _args = ( - [x1.get_block(i)] - + [x2.get_block(i)] - + [args[j] for j in range(2, len(args))] - ) - res.set_block( - i, self._binary_operation(ufunc, method, *_args, **kwargs) - ) + _args = (x1.get_block(i), x2.get_block(i)) + res.set_block(i, self._binary_operation(ufunc, method, _args, kwargs)) return res elif type(x1) == np.ndarray and isinstance(x2, BlockVector): assert_block_structure(x2) @@ -404,14 +337,8 @@ def _binary_operation(self, ufunc, method, *args, **kwargs): accum = 0 for i in range(x2.nblocks): nelements = x2._brow_lengths[i] - _args = ( - [x1[accum : accum + nelements]] - + [x2.get_block(i)] - + [args[j] for j in range(2, len(args))] - ) - res.set_block( - i, self._binary_operation(ufunc, method, *_args, **kwargs) - ) + _args = (x1[accum : accum + nelements], x2.get_block(i)) + res.set_block(i, self._binary_operation(ufunc, method, _args, kwargs)) accum += nelements return res elif type(x2) == np.ndarray and isinstance(x1, BlockVector): @@ -423,37 +350,23 @@ def _binary_operation(self, ufunc, method, *args, **kwargs): accum = 0 for i in range(x1.nblocks): nelements = x1._brow_lengths[i] - _args = ( - [x1.get_block(i)] - + [x2[accum : accum + nelements]] - + [args[j] for j in range(2, len(args))] - ) - res.set_block( - i, self._binary_operation(ufunc, method, *_args, **kwargs) - ) + _args = (x1.get_block(i), x2[accum : accum + nelements]) + res.set_block(i, self._binary_operation(ufunc, method, _args, kwargs)) accum += nelements return res elif np.isscalar(x1) and isinstance(x2, BlockVector): assert_block_structure(x2) res = BlockVector(x2.nblocks) for i in range(x2.nblocks): - _args = ( - [x1] + [x2.get_block(i)] + [args[j] for j in range(2, len(args))] - ) - res.set_block( - i, self._binary_operation(ufunc, method, *_args, **kwargs) - ) + _args = (x1, x2.get_block(i)) + res.set_block(i, self._binary_operation(ufunc, method, _args, kwargs)) return res elif np.isscalar(x2) and isinstance(x1, BlockVector): assert_block_structure(x1) res = BlockVector(x1.nblocks) for i in range(x1.nblocks): - _args = ( - [x1.get_block(i)] + [x2] + [args[j] for j in range(2, len(args))] - ) - res.set_block( - i, self._binary_operation(ufunc, method, *_args, **kwargs) - ) + _args = (x1.get_block(i), x2) + res.set_block(i, self._binary_operation(ufunc, method, _args, kwargs)) return res elif (type(x1) == np.ndarray or np.isscalar(x1)) and ( type(x2) == np.ndarray or np.isscalar(x2) @@ -466,7 +379,7 @@ def _binary_operation(self, ufunc, method, *args, **kwargs): raise RuntimeError('Operation not supported by BlockVector') if x2.__class__.__name__ == 'MPIBlockVector': raise RuntimeError('Operation not supported by BlockVector') - raise NotImplementedError() + return NotImplemented @property def nblocks(self): @@ -742,13 +655,15 @@ def nonzero(self): result.set_block(idx, self.get_block(idx).nonzero()[0]) return (result,) - def ptp(self, axis=None, out=None, keepdims=False): - """ - Peak to peak (maximum - minimum) value along a given axis. - """ - assert_block_structure(self) - assert out is None, 'Out keyword not supported' - return self.max() - self.min() + if np.__version__[0] < '2': + + def ptp(self, axis=None, out=None, keepdims=False): + """ + Peak to peak (maximum - minimum) value along a given axis. + """ + assert_block_structure(self) + assert out is None, 'Out keyword not supported' + return self.max() - self.min() def round(self, decimals=0, out=None): """ diff --git a/pyomo/contrib/pynumero/sparse/mpi_block_vector.py b/pyomo/contrib/pynumero/sparse/mpi_block_vector.py index f86d450a73e..f976ee2853d 100644 --- a/pyomo/contrib/pynumero/sparse/mpi_block_vector.py +++ b/pyomo/contrib/pynumero/sparse/mpi_block_vector.py @@ -11,7 +11,12 @@ from pyomo.common.dependencies import mpi4py from pyomo.contrib.pynumero.sparse import BlockVector -from .base_block import BaseBlockVector +from .base_block import ( + BaseBlockVector, + vec_unary_ufuncs, + vec_binary_ufuncs, + vec_associative_reductions, +) from .block_vector import NotFullyDefinedBlockVectorError from .block_vector import assert_block_structure as block_vector_assert_block_structure import numpy as np @@ -136,74 +141,6 @@ def __array_wrap__(self, out_arr, context=None): def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): """Runs ufuncs speciallizations to MPIBlockVector""" - # functions that take one vector - unary_funcs = [ - np.log10, - np.sin, - np.cos, - np.exp, - np.ceil, - np.floor, - np.tan, - np.arctan, - np.arcsin, - np.arccos, - np.sinh, - np.cosh, - np.abs, - np.tanh, - np.arccosh, - np.arcsinh, - np.arctanh, - np.fabs, - np.sqrt, - np.log, - np.log2, - np.absolute, - np.isfinite, - np.isinf, - np.isnan, - np.log1p, - np.logical_not, - np.expm1, - np.exp2, - np.sign, - np.rint, - np.square, - np.positive, - np.negative, - np.rad2deg, - np.deg2rad, - np.conjugate, - np.reciprocal, - np.signbit, - ] - # functions that take two vectors - binary_funcs = [ - np.add, - np.multiply, - np.divide, - np.subtract, - np.greater, - np.greater_equal, - np.less, - np.less_equal, - np.not_equal, - np.maximum, - np.minimum, - np.fmax, - np.fmin, - np.equal, - np.logical_and, - np.logical_or, - np.logical_xor, - np.logaddexp, - np.logaddexp2, - np.remainder, - np.heaviside, - np.hypot, - ] - outputs = kwargs.pop('out', None) if outputs is not None: raise NotImplementedError( @@ -211,10 +148,10 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): + ' cannot be used with MPIBlockVector if the out keyword argument is given.' ) - if ufunc in unary_funcs: + if ufunc in vec_unary_ufuncs: results = self._unary_operation(ufunc, method, *inputs, **kwargs) return results - elif ufunc in binary_funcs: + elif ufunc in vec_binary_ufuncs: results = self._binary_operation(ufunc, method, *inputs, **kwargs) return results else: diff --git a/pyomo/contrib/pynumero/sparse/tests/test_block_vector.py b/pyomo/contrib/pynumero/sparse/tests/test_block_vector.py index 610d41a09a4..397dac04113 100644 --- a/pyomo/contrib/pynumero/sparse/tests/test_block_vector.py +++ b/pyomo/contrib/pynumero/sparse/tests/test_block_vector.py @@ -24,6 +24,9 @@ from pyomo.contrib.pynumero.sparse.block_vector import ( BlockVector, NotFullyDefinedBlockVectorError, + vec_associative_reductions, + vec_unary_ufuncs, + vec_binary_ufuncs, ) @@ -201,6 +204,7 @@ def test_nonzero(self): for bid, blk in enumerate(n[0]): self.assertTrue(np.allclose(blk, v2.get_block(bid))) + @unittest.skipUnless(np.__version__[0] == "1", "PTP only included in Numpy 1.x") def test_ptp(self): v = BlockVector(2) a = np.arange(5) @@ -1023,64 +1027,34 @@ def test_copy_structure(self): self.assertEqual(v.get_block(1).size, v2.get_block(1).size) def test_unary_ufuncs(self): - v = BlockVector(2) a = np.ones(3) * 0.5 b = np.ones(2) * 0.8 + v = BlockVector(2) v.set_block(0, a) v.set_block(1, b) + # Some operations only accept integers + ai = np.ones(3, dtype='i') * 5 + bi = np.ones(2, dtype='i') * 8 + vi = BlockVector(2) + vi.set_block(0, ai) + vi.set_block(1, bi) - v2 = BlockVector(2) + _int_ufuncs = {np.invert, np.arccosh} - unary_funcs = [ - np.log10, - np.sin, - np.cos, - np.exp, - np.ceil, - np.floor, - np.tan, - np.arctan, - np.arcsin, - np.arccos, - np.sinh, - np.cosh, - np.abs, - np.tanh, - np.arcsinh, - np.arctanh, - np.fabs, - np.sqrt, - np.log, - np.log2, - np.absolute, - np.isfinite, - np.isinf, - np.isnan, - np.log1p, - np.logical_not, - np.exp2, - np.expm1, - np.sign, - np.rint, - np.square, - np.positive, - np.negative, - np.rad2deg, - np.deg2rad, - np.conjugate, - np.reciprocal, - ] - - for fun in unary_funcs: - v2.set_block(0, fun(v.get_block(0))) - v2.set_block(1, fun(v.get_block(1))) - res = fun(v) + v2 = BlockVector(2) + for fun in vec_unary_ufuncs: + _v = vi if fun in _int_ufuncs else v + v2.set_block(0, fun(_v.get_block(0))) + v2.set_block(1, fun(_v.get_block(1))) + res = fun(_v) self.assertIsInstance(res, BlockVector) self.assertEqual(res.nblocks, 2) for i in range(2): self.assertTrue(np.allclose(res.get_block(i), v2.get_block(i))) - other_funcs = [np.cumsum, np.cumprod, np.cumproduct] + other_funcs = [np.cumsum, np.cumprod] + if np.__version__[0] == '1': + other_funcs.append(np.cumproduct) for fun in other_funcs: res = fun(v) @@ -1088,16 +1062,20 @@ def test_unary_ufuncs(self): self.assertEqual(res.nblocks, 2) self.assertTrue(np.allclose(fun(v.flatten()), res.flatten())) - with self.assertRaises(Exception) as context: - np.cbrt(v) + with self.assertRaises(TypeError): + np.modf(v) def test_reduce_ufuncs(self): v = BlockVector(2) - a = np.ones(3) * 0.5 - b = np.ones(2) * 0.8 + # Some operations only accept integers, so we will test with integers + a = np.ones(3, dtype='i') * 5 + b = np.ones(2, dtype='i') * 8 v.set_block(0, a) v.set_block(1, b) + for fun in vec_associative_reductions: + self.assertAlmostEqual(fun.reduce(v), fun.reduce(v.flatten())) + reduce_funcs = [np.sum, np.max, np.min, np.prod, np.mean] for fun in reduce_funcs: self.assertAlmostEqual(fun(v), fun(v.flatten())) @@ -1108,56 +1086,45 @@ def test_reduce_ufuncs(self): def test_binary_ufuncs(self): v = BlockVector(2) - a = np.ones(3) * 0.5 - b = np.ones(2) * 0.8 - v.set_block(0, a) - v.set_block(1, b) - + v.set_blocks([np.ones(3) * 0.5, np.ones(2) * 0.8]) v2 = BlockVector(2) - a2 = np.ones(3) * 3.0 - b2 = np.ones(2) * 2.8 - v2.set_block(0, a2) - v2.set_block(1, b2) - - binary_ufuncs = [ - np.add, - np.multiply, - np.divide, - np.subtract, - np.greater, - np.greater_equal, - np.less, - np.less_equal, - np.not_equal, - np.maximum, - np.minimum, - np.fmax, - np.fmin, - np.equal, - np.logaddexp, - np.logaddexp2, - np.remainder, - np.heaviside, - np.hypot, - ] - - for fun in binary_ufuncs: - flat_res = fun(v.flatten(), v2.flatten()) - res = fun(v, v2) + v2.set_blocks([np.ones(3) * 3.0, np.ones(2) * 2.8]) + + vi = BlockVector(2) + vi.set_blocks([np.ones(3, dtype='i') * 5, np.ones(2, dtype='i') * 8]) + v2i = BlockVector(2) + v2i.set_blocks([np.ones(3, dtype='i') * 3, np.ones(2, dtype='i') * 2]) + + _int_ufuncs = { + np.gcd, + np.lcm, + np.ldexp, + np.left_shift, + np.right_shift, + np.bitwise_and, + np.bitwise_or, + np.bitwise_xor, + } + + for fun in vec_binary_ufuncs: + _v, _v2, _s = (vi, v2i, 3) if fun in _int_ufuncs else (v, v2, 3.0) + + flat_res = fun(_v.flatten(), _v2.flatten()) + res = fun(_v, _v2) self.assertTrue(np.allclose(flat_res, res.flatten())) - res = fun(v, v2.flatten()) + res = fun(_v, _v2.flatten()) self.assertTrue(np.allclose(flat_res, res.flatten())) - res = fun(v.flatten(), v2) + res = fun(_v.flatten(), _v2) self.assertTrue(np.allclose(flat_res, res.flatten())) - flat_res = fun(v.flatten(), 5) - res = fun(v, 5) + flat_res = fun(_v.flatten(), 5) + res = fun(_v, 5) self.assertTrue(np.allclose(flat_res, res.flatten())) - flat_res = fun(3.0, v2.flatten()) - res = fun(3.0, v2) + flat_res = fun(_s, _v2.flatten()) + res = fun(_s, _v2) self.assertTrue(np.allclose(flat_res, res.flatten())) v = BlockVector(2) diff --git a/pyomo/contrib/pynumero/sparse/tests/test_mpi_block_vector.py b/pyomo/contrib/pynumero/sparse/tests/test_mpi_block_vector.py index c28c524823a..c308b99a0b1 100644 --- a/pyomo/contrib/pynumero/sparse/tests/test_mpi_block_vector.py +++ b/pyomo/contrib/pynumero/sparse/tests/test_mpi_block_vector.py @@ -35,6 +35,10 @@ if not SKIPTESTS: from pyomo.contrib.pynumero.sparse import BlockVector + from pyomo.contrib.pynumero.sparse.base_block import ( + vec_unary_ufuncs, + vec_binary_ufuncs, + ) from pyomo.contrib.pynumero.sparse.mpi_block_vector import MPIBlockVector @@ -1403,48 +1407,13 @@ def test_unary_ufuncs(self): bv.set_block(0, a) bv.set_block(1, b) - unary_funcs = [ - np.log10, - np.sin, - np.cos, - np.exp, - np.ceil, - np.floor, - np.tan, - np.arctan, - np.arcsin, - np.arccos, - np.sinh, - np.cosh, - np.abs, - np.tanh, - np.arcsinh, - np.arctanh, - np.fabs, - np.sqrt, - np.log, - np.log2, - np.absolute, - np.isfinite, - np.isinf, - np.isnan, - np.log1p, - np.logical_not, - np.exp2, - np.expm1, - np.sign, - np.rint, - np.square, - np.positive, - np.negative, - np.rad2deg, - np.deg2rad, - np.conjugate, - np.reciprocal, - ] + _int_ufuncs = {np.invert, np.arccosh} bv2 = BlockVector(2) - for fun in unary_funcs: + for fun in vec_unary_ufuncs: + if fun in _int_ufuncs: + continue + bv2.set_block(0, fun(bv.get_block(0))) bv2.set_block(1, fun(bv.get_block(1))) res = fun(v) @@ -1454,7 +1423,7 @@ def test_unary_ufuncs(self): self.assertTrue(np.allclose(res.get_block(i), bv2.get_block(i))) with self.assertRaises(Exception) as context: - np.cbrt(v) + np.modf(v) with self.assertRaises(Exception) as context: np.cumsum(v) @@ -1504,29 +1473,21 @@ def test_binary_ufuncs(self): bv2.set_block(0, np.ones(3) * 3.0) bv2.set_block(1, np.ones(2) * 2.8) - binary_ufuncs = [ - np.add, - np.multiply, - np.divide, - np.subtract, - np.greater, - np.greater_equal, - np.less, - np.less_equal, - np.not_equal, - np.maximum, - np.minimum, - np.fmax, - np.fmin, - np.equal, - np.logaddexp, - np.logaddexp2, - np.remainder, - np.heaviside, - np.hypot, - ] + _int_ufuncs = { + np.gcd, + np.lcm, + np.ldexp, + np.left_shift, + np.right_shift, + np.bitwise_and, + np.bitwise_or, + np.bitwise_xor, + } + + for fun in vec_binary_ufuncs: + if fun in _int_ufuncs: + continue - for fun in binary_ufuncs: serial_res = fun(bv, bv2) res = fun(v, v2) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index e6ad46ce740..13195b3270e 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -779,7 +779,6 @@ def test_error_on_rank_deficient_psi_mat(self): beta=1 / 6, ) - @unittest.skipUnless(baron_available, "BARON is not available") @parameterized.expand( [ # map beta to expected parameter bounds @@ -813,6 +812,7 @@ def test_error_on_rank_deficient_psi_mat(self): ["beta1", 1, [(-3.0, 3.0), (-1.1, 3.1), (-13.0, 17.0), (-12.0, 18.0)]], ] ) + @unittest.skipUnless(baron_available, "BARON is not available") def test_compute_parameter_bounds(self, name, beta, expected_param_bounds): """ Test parameter bounds computations give expected results. @@ -2401,6 +2401,7 @@ def test_set_as_constraint(self): self.assertEqual(len(uq.uncertainty_cons), 3) self.assertEqual(len(uq.uncertain_param_vars), 2) + @unittest.skipUnless(baron_available, "BARON is not available") def test_compute_parameter_bounds(self): """ Test parameter bounds computations give expected results. diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index 8a780d4b988..ec011fb802a 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -9,10 +9,16 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pyomo.common.dependencies import dill_available +import logging +import sys +import random +from io import StringIO + import pyomo.common.unittest as unittest + +from pyomo.common.dependencies import dill_available from pyomo.common.log import LoggingIntercept -import logging +from pyomo.common.fileutils import this_file_dir from pyomo.environ import ( TransformationFactory, @@ -46,14 +52,8 @@ import pyomo.gdp.tests.models as models import pyomo.gdp.tests.common_tests as ct -import random -from io import StringIO -import os -from os.path import abspath, dirname, join - -currdir = dirname(abspath(__file__)) -from filecmp import cmp +currdir = this_file_dir() EPS = TransformationFactory('gdp.hull').CONFIG.EPS linear_solvers = ct.linear_solvers @@ -2874,7 +2874,15 @@ def test_pickle(self): @unittest.skipIf(not dill_available, "Dill is not available") def test_dill_pickle(self): - ct.check_transformed_model_pickles_with_dill(self, 'hull') + try: + # As of Nov 2024, this test needs a larger recursion limit + # due to the various references among the modeling objects + # 1385 is sufficient locally, but not always on GHA. + rl = sys.getrecursionlimit() + sys.setrecursionlimit(max(1500, rl)) + ct.check_transformed_model_pickles_with_dill(self, 'hull') + finally: + sys.setrecursionlimit(rl) @unittest.skipUnless(gurobi_available, "Gurobi is not available")