diff --git a/pyomo/contrib/fme/fourier_motzkin_elimination.py b/pyomo/contrib/fme/fourier_motzkin_elimination.py index cec16e656ac..0bf5abf7221 100644 --- a/pyomo/contrib/fme/fourier_motzkin_elimination.py +++ b/pyomo/contrib/fme/fourier_motzkin_elimination.py @@ -30,7 +30,7 @@ from pyomo.repn.standard_repn import generate_standard_repn from pyomo.common.collections import ComponentMap, ComponentSet from pyomo.opt import TerminationCondition -from pyomo.util.var_list_domain import var_component_set +from pyomo.util.config_domains import ComponentDataList import logging @@ -95,7 +95,7 @@ class Fourier_Motzkin_Elimination_Transformation(Transformation): 'vars_to_eliminate', ConfigValue( default=None, - domain=var_component_set, + domain=ComponentDataList(Var), description="Continuous variable or list of continuous variables to " "project out of the model", doc=""" diff --git a/pyomo/core/plugins/transform/lp_dual.py b/pyomo/core/plugins/transform/lp_dual.py index 2dc2bff7213..81c23e1d874 100644 --- a/pyomo/core/plugins/transform/lp_dual.py +++ b/pyomo/core/plugins/transform/lp_dual.py @@ -29,7 +29,7 @@ ) from pyomo.opt import WriterFactory from pyomo.repn.standard_repn import isclose_const -from pyomo.util.var_list_domain import var_component_set +from pyomo.util.config_domains import ComponentDataList class _LPDualData(AutoSlots.Mixin): @@ -54,7 +54,7 @@ class LinearProgrammingDual(object): 'parameterize_wrt', ConfigValue( default=None, - domain=var_component_set, + domain=ComponentDataList(Var), description="Vars to treat as data for the purposes of taking the dual", doc=""" Optional list of Vars to be treated as data while taking the LP dual. @@ -108,8 +108,8 @@ def create_using(self, model, ostream=None, **kwds): def _take_dual(self, model, std_form): if len(std_form.objectives) != 1: raise ValueError( - "Model '%s' has no objective or multiple active objectives. Cannot " - "take dual with more than one objective!" % model.name + "Model '%s' has no objective or multiple active objectives. Can " + "only take dual with exactly one active objective!" % model.name ) primal_sense = std_form.objectives[0].sense diff --git a/pyomo/core/tests/unit/test_lp_dual.py b/pyomo/core/tests/unit/test_lp_dual.py index 33e3e9287cd..5feacfe5c61 100644 --- a/pyomo/core/tests/unit/test_lp_dual.py +++ b/pyomo/core/tests/unit/test_lp_dual.py @@ -331,7 +331,7 @@ def test_multiple_obj_error(self): with self.assertRaisesRegex( ValueError, "Model 'primal' has no objective or multiple active objectives. " - "Cannot take dual with more than one objective!", + "Can only take dual with exactly one active objective!", ): dual = lp_dual.create_using(m, parameterize_wrt=[m.outer1, m.outer]) @@ -341,7 +341,7 @@ def test_multiple_obj_error(self): with self.assertRaisesRegex( ValueError, "Model 'primal' has no objective or multiple active objectives. " - "Cannot take dual with more than one objective!", + "Can only take dual with exactly one active objective!", ): dual = lp_dual.create_using(m, parameterize_wrt=[m.outer1, m.outer]) diff --git a/pyomo/repn/plugins/parameterized_standard_form.py b/pyomo/repn/plugins/parameterized_standard_form.py index ee844a0875d..ed924a241be 100644 --- a/pyomo/repn/plugins/parameterized_standard_form.py +++ b/pyomo/repn/plugins/parameterized_standard_form.py @@ -13,6 +13,7 @@ from pyomo.common.dependencies import numpy as np from pyomo.common.gc_manager import PauseGC from pyomo.common.numeric_types import native_numeric_types +from pyomo.core import Var from pyomo.opt import WriterFactory from pyomo.repn.parameterized_linear import ParameterizedLinearRepnVisitor @@ -21,7 +22,7 @@ LinearStandardFormCompiler, _LinearStandardFormCompiler_impl, ) -from pyomo.util.var_list_domain import var_component_set +from pyomo.util.config_domains import ComponentDataList @WriterFactory.register( @@ -36,7 +37,7 @@ class ParameterizedLinearStandardFormCompiler(LinearStandardFormCompiler): 'wrt', ConfigValue( default=None, - domain=var_component_set, + domain=ComponentDataList(Var), description="Vars to treat as data for the purposes of compiling" "the standard form", doc=""" @@ -191,6 +192,10 @@ def todense(self): def sum_duplicates(self): """Implements the algorithm from scipy's csr_sum_duplicates function in sparsetools. + + Note that this only removes duplicates that are adjacent, so it will remove + all duplicates if the incoming CSC matrix has sorted indices. (In particular + this will be true if it was just convered from CSR). """ ncols = self.shape[1] row_index = self.indices diff --git a/pyomo/util/config_domains.py b/pyomo/util/config_domains.py new file mode 100644 index 00000000000..5353698d338 --- /dev/null +++ b/pyomo/util/config_domains.py @@ -0,0 +1,53 @@ +# ___________________________________________________________________________ +# +# 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. +# ___________________________________________________________________________ + +from pyomo.common.collections import ComponentSet + + +class ComponentDataList(): + """ComponentDataList(ctype) + Domain validation class that accepts singleton or iterable arguments and + compiles them into a ComponentSet, verifying that they are all ComponentDatas + of type 'ctype.' + + Parameters + ---------- + ctype: The component type of the list + + Raises + ------ + ValueError if all of the arguments are not of type 'ctype' + """ + def __init__(self, ctype): + self._ctype = ctype + + def __call__(self, x): + if hasattr(x, 'ctype') and x.ctype is self._ctype: + if not x.is_indexed(): + return ComponentSet([x]) + ans = ComponentSet() + for j in x.index_set(): + ans.add(x[j]) + return ans + elif hasattr(x, '__iter__'): + ans = ComponentSet() + for i in x: + ans.update(self(i)) + return ans + else: + _ctype_name = str(self._ctype) + raise ValueError( + f"Expected {_ctype_name} or iterable of " + f"{_ctype_name}s.\n\tReceived {type(x)}") + + def domain_name(self): + _ctype_name = str(self._ctype) + return f'ComponentDataList({_ctype_name})' diff --git a/pyomo/util/var_list_domain.py b/pyomo/util/var_list_domain.py deleted file mode 100644 index 15c0f775b57..00000000000 --- a/pyomo/util/var_list_domain.py +++ /dev/null @@ -1,34 +0,0 @@ -# ___________________________________________________________________________ -# -# 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. -# ___________________________________________________________________________ - -from pyomo.common.collections import ComponentSet -from pyomo.core import Var - - -def var_component_set(x): - """ - For domain validation in ConfigDicts: Takes singletone or iterable argument 'x' - of Vars and converts it to a ComponentSet of Vars. - """ - if hasattr(x, 'ctype') and x.ctype is Var: - if not x.is_indexed(): - return ComponentSet([x]) - ans = ComponentSet() - for j in x.index_set(): - ans.add(x[j]) - return ans - elif hasattr(x, '__iter__'): - ans = ComponentSet() - for i in x: - ans.update(var_component_set(i)) - return ans - else: - raise ValueError("Expected Var or iterable of Vars.\n\tReceived %s" % type(x))