From c9c12839ec8f5b80a2a0feec4698451fac0168ee Mon Sep 17 00:00:00 2001 From: Chris Laliwala Date: Tue, 24 Feb 2026 12:20:43 -0500 Subject: [PATCH 01/35] added kkt transformation to init file. --- pyomo/core/plugins/transform/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/core/plugins/transform/__init__.py b/pyomo/core/plugins/transform/__init__.py index 401875d1e78..cb2088de69e 100644 --- a/pyomo/core/plugins/transform/__init__.py +++ b/pyomo/core/plugins/transform/__init__.py @@ -23,4 +23,5 @@ scaling, logical_to_linear, lp_dual, + kkt, ) From 5797b937f5a9cd849732b93048561ea1e9294ffd Mon Sep 17 00:00:00 2001 From: Chris Laliwala Date: Tue, 24 Feb 2026 12:21:09 -0500 Subject: [PATCH 02/35] created kkt transformation. --- pyomo/core/plugins/transform/kkt.py | 438 ++++++++++++++++++++++++++++ 1 file changed, 438 insertions(+) create mode 100644 pyomo/core/plugins/transform/kkt.py diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py new file mode 100644 index 00000000000..aa857cec240 --- /dev/null +++ b/pyomo/core/plugins/transform/kkt.py @@ -0,0 +1,438 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2025 +# 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.autoslots import AutoSlots +from pyomo.common.collections import ComponentMap, ComponentSet +from pyomo.common.config import ConfigDict, ConfigValue +from pyomo.core import ( + Block, + Constraint, + ConstraintList, + Expression, + NonNegativeReals, + Objective, + RangeSet, + Reals, + Set, + TransformationFactory, + Var, + maximize, + minimize, +) +from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd +from pyomo.mpec import ComplementarityList, complements +from pyomo.util.vars_from_expressions import get_vars_from_components +from pyomo.util.config_domains import ComponentDataSet + + +class _KKTReformulationData(AutoSlots.Mixin): + __slots__ = ( + "equality_multiplier_from_con", + "equality_con_from_multiplier", + "inequality_multiplier_from_con", + "inequality_con_from_multiplier", + "var_bound_multiplier_index_to_con", + "equality_multiplier_index_to_con", + "inequality_multiplier_index_to_con", + "equality_con_to_expr", + "inequality_con_to_expr", + "ranged_constraints", + ) + + def __init__(self): + self.equality_multiplier_from_con = ComponentMap() + self.equality_con_from_multiplier = ComponentMap() + self.inequality_multiplier_from_con = ComponentMap() + self.inequality_con_from_multiplier = ComponentMap() + + self.var_bound_multiplier_index_to_con = {} + self.equality_multiplier_index_to_con = {} + self.inequality_multiplier_index_to_con = {} + self.equality_con_to_expr = ComponentMap() + self.inequality_con_to_expr = ComponentMap() + + self.ranged_constraints = ComponentSet() + + +Block.register_private_data_initializer(_KKTReformulationData) + + +@TransformationFactory.register( + 'core.kkt', 'Generate KKT reformulation of the given model' +) +class NonLinearProgrammingKKT: + CONFIG = ConfigDict("core.kkt") + CONFIG.declare( + 'kkt_block_name', + ConfigValue( + default='kkt', + doc=""" + Name of the block on which the kkt variables and constraints will be stored. + """, + ), + ) + CONFIG.declare( + 'parametrize_wrt', + ConfigValue( + default=None, + domain=ComponentDataSet(Var), + description='Vars to treat as data for the purposes of generating KKT reformulation', + doc=""" + Optional list of Vars to be treated as data while generating the KKT reformulation. + """, + ), + ) + + def apply_to(self, model, **kwds): + """ + Reformulate model with KKT conditions. + """ + config = self.CONFIG(kwds.pop('options', {})) + config.set_value(kwds) + + if hasattr(model, config.kkt_block_name): + raise ValueError( + f"""model already has an attribute with the + specified kkt_block_name: '{config.kkt_block_name}'""" + ) + + # we should check that all vars the user fixed are included + # in parametrize_wrt + params = config.parametrize_wrt or ComponentSet() + vars_in_cons = ComponentSet( + get_vars_from_components(model, Constraint, active=True, descend_into=True) + ) + vars_in_obj = ComponentSet( + get_vars_from_components(model, Objective, active=True, descend_into=True) + ) + vars_in_model = ComponentSet(vars_in_cons | vars_in_obj) + fixed_vars_in_model = ComponentSet(v for v in vars_in_model if v.is_fixed()) + missing = [v for v in fixed_vars_in_model if v not in params] + if missing: + raise ValueError("All fixed variables must be included in parametrize_wrt.") + + # we should also check that all vars the user passes in parametrize_wrt + # exist on an active constraint or objective within the model + unknown = [v for v in params if v not in vars_in_model] + if unknown: + raise ValueError( + "A variable passed in parametrize_wrt does not exist on an " + "active constraint or objective within the model." + ) + + kkt_block = Block() + model.add_component(config.kkt_block_name, kkt_block) + kkt_block.parametrize_wrt = params + + return self._reformulate(model, kkt_block) + + def _reformulate(self, model, kkt_block): + active_objs = list( + model.component_data_objects(Objective, active=True, descend_into=True) + ) + if len(active_objs) != 1: + raise ValueError( + f"model must have only one active objective; found {len(active_objs)}" + ) + + self._construct_lagrangean(model, kkt_block) + self._enforce_stationarity_conditions(kkt_block) + self._enforce_complementarity_conditions(model, kkt_block) + + active_objs[0].deactivate() + kkt_block.dummy_obj = Objective(expr=1) + + return model + + def _construct_lagrangean(self, model, kkt_block): + # we need to loop through the model and store the + # equality and inequality constraints + info = model.private_data() + equality_cons = [] + inequality_cons = [] + for con in model.component_data_objects( + Constraint, descend_into=True, active=True + ): + if con.has_lb() and con.has_ub() and (con.lower == con.upper): + equality_cons.append(con) + info.equality_con_to_expr[con] = con.upper - con.body + else: + if con.has_lb(): + inequality_cons.append((con, "lb")) + info.inequality_con_to_expr.setdefault(con, {})["lb"] = ( + con.lower - con.body + ) + if con.has_ub(): + inequality_cons.append((con, "ub")) + info.inequality_con_to_expr.setdefault(con, {})["ub"] = ( + con.body - con.upper + ) + # we want to keep track of the ranged constraints because the mapping between + # multipliers and ranged constraints will be a tuple (to indicate bound as well) + # instead of simply the model object + if con.has_lb() and con.has_ub() and (con.lower != con.upper): + info.ranged_constraints.add(con) + + kkt_block.equality_cons_set = Set(initialize=equality_cons, ordered=True) + kkt_block.gamma_set = RangeSet(0, len(equality_cons) - 1) + kkt_block.gamma = Var(kkt_block.gamma_set, domain=Reals) + info.equality_multiplier_index_to_con = dict( + enumerate(kkt_block.equality_cons_set.ordered_data()) + ) + kkt_block.inequality_cons_set = Set( + dimen=2, initialize=inequality_cons, ordered=True + ) + kkt_block.alpha_con_set = RangeSet(0, len(inequality_cons) - 1) + kkt_block.alpha_con = Var(kkt_block.alpha_con_set, domain=NonNegativeReals) + info.inequality_multiplier_index_to_con = dict( + enumerate(kkt_block.inequality_cons_set.ordered_data()) + ) + + # we also need to consider inequality constraints + # formed by the user specifying variable bounds + var_bound_sides = [] + vars_in_cons_all = ComponentSet( + get_vars_from_components(model, Constraint, active=True, descend_into=True) + ) + vars_in_kkt_cons = ComponentSet( + get_vars_from_components( + kkt_block, Constraint, active=True, descend_into=True + ) + ) + vars_in_cons = ComponentSet(vars_in_cons_all - vars_in_kkt_cons) + vars_in_obj = ComponentSet( + get_vars_from_components(model, Objective, active=True, descend_into=True) + ) + kkt_block.var_set = ComponentSet(vars_in_cons | vars_in_obj) + kkt_block.var_set = ComponentSet( + v for v in kkt_block.var_set if v not in kkt_block.parametrize_wrt + ) + for var in kkt_block.var_set: + if var.has_lb(): + var_bound_sides.append((var, "lb")) + if var.has_ub(): + var_bound_sides.append((var, "ub")) + kkt_block.var_bound_set = RangeSet(0, len(var_bound_sides) - 1) + kkt_block.alpha_var_bound = Var( + kkt_block.var_bound_set, domain=NonNegativeReals + ) + info.var_bound_multiplier_index_to_con = dict(enumerate(var_bound_sides)) + + # indexing the inequality constraint expressions will help + # with constructing the lagrangean later + def _var_bound_expr_rule(kkt, i): + var, side = info.var_bound_multiplier_index_to_con[i] + return (var.lb - var) if side == "lb" else (var - var.ub) + + kkt_block.var_bound_expr = Expression( + kkt_block.var_bound_set, rule=_var_bound_expr_rule + ) + + # we will construct the lagrangean by first adding the objective, + # and then looping through and adding the product of each constraint and + # the corresponding multiplier + obj = list( + model.component_data_objects(Objective, active=True, descend_into=True) + ) + # maximize is -1 and minimize is +1 + sense = obj[0].sense + if sense == maximize: + lagrangean = -obj[0].expr + elif sense == minimize: + lagrangean = obj[0].expr + + for index, con in enumerate(kkt_block.equality_cons_set.ordered_data()): + lagrangean += info.equality_con_to_expr[con] * kkt_block.gamma[index] + info.equality_con_from_multiplier[kkt_block.gamma[index]] = con + info.equality_multiplier_from_con[con] = kkt_block.gamma[index] + + for index, (con, bound) in enumerate( + kkt_block.inequality_cons_set.ordered_data() + ): + lagrangean += ( + info.inequality_con_to_expr[con][bound] * kkt_block.alpha_con[index] + ) + # mappings for ranged constraints will consider bounds as well + if con in info.ranged_constraints: + info.inequality_con_from_multiplier[kkt_block.alpha_con[index]] = ( + con, + bound, + ) + info.inequality_multiplier_from_con[(con, bound)] = kkt_block.alpha_con[ + index + ] + else: + info.inequality_con_from_multiplier[kkt_block.alpha_con[index]] = con + info.inequality_multiplier_from_con[con] = kkt_block.alpha_con[index] + + for i in kkt_block.var_bound_set: + lagrangean += kkt_block.var_bound_expr[i] * kkt_block.alpha_var_bound[i] + # mappings for ranged constraints built from variable bounds + (var, bound) = info.var_bound_multiplier_index_to_con[i] + info.inequality_con_from_multiplier[kkt_block.alpha_var_bound[i]] = ( + var, + bound, + ) + info.inequality_multiplier_from_con[(var, bound)] = ( + kkt_block.alpha_var_bound[i] + ) + + kkt_block.lagrangean = Expression(expr=lagrangean) + + def _enforce_stationarity_conditions(self, kkt_block): + deriv_lagrangean = reverse_sd(kkt_block.lagrangean.expr) + kkt_block.stationarity_conditions = ConstraintList() + for var in kkt_block.var_set: + kkt_block.stationarity_conditions.add(deriv_lagrangean[var] == 0) + + def _enforce_complementarity_conditions(self, model, kkt_block): + info = model.private_data() + kkt_block.complements = ComplementarityList() + for index, (con, bound) in enumerate( + kkt_block.inequality_cons_set.ordered_data() + ): + kkt_block.complements.add( + complements( + kkt_block.alpha_con[index] >= 0, + info.inequality_con_to_expr[con][bound] <= 0, + ) + ) + # we also need to consider the inequality constraints + # formed from the user specifying the variable bounds + for i in kkt_block.var_bound_set: + kkt_block.complements.add( + complements( + kkt_block.alpha_var_bound[i] >= 0, kkt_block.var_bound_expr[i] <= 0 + ) + ) + + def get_constraint_from_multiplier(self, model, multiplier_var): + """ + Return the constraint or variable bound corresponding to a KKT multiplier variable. + + Parameters + ---------- + model: ConcreteModel + The model on which the kkt transformation was applied + multiplier_var: Var + A KKT multiplier created by the transformation. + + Returns + ------- + Constraint or Tuple + - Constraint object for simple constraints + - (Constraint, bound) tuple for ranged constraints + - (Var, bound) tuple for variable bounds + """ + + info = model.private_data() + if multiplier_var in info.equality_con_from_multiplier.keys(): + return info.equality_con_from_multiplier[multiplier_var] + if multiplier_var in info.inequality_con_from_multiplier.keys(): + # if this multiplier var maps to a ranged constraint, we will return a tuple + # so that we can indicate which bound the multplier var maps to + return info.inequality_con_from_multiplier[multiplier_var] + raise ValueError( + f"The KKT multiplier: {multiplier_var.name}, does not exist on {model.name}." + ) + + def get_multiplier_from_constraint(self, model, constraint=None, variable=None): + """ + Return the multiplier variable corresponding to a constraint or variable bound. + + Parameters + ---------- + model: ConcreteModel + The model on which the kkt transformation was applied to + constraint: Constraint or Tuple, optional + - A primal Constraint object for simple constraints, OR + - A tuple (constraint, 'lb'|'ub') for ranged constraints + Mutually exclusive with variables. + Variable: Tuple, optional + A tuple (Var, 'lb'|'ub') for variable bounds. + Mutually exclusive with constraint. + + Returns + ------- + Var + The KKT multiplier variable corresponding to the constraint or variable bound. + """ + + if (constraint is None) and (variable is None): + raise ValueError( + "Must provide 'constraint' or 'variable'." + ) + if (constraint is not None) and (variable is not None): + raise ValueError( + "Cannot provide both 'constraint' and 'variable'. " "Provide only one." + ) + + info = model.private_data() + if constraint is not None: + if isinstance(constraint, tuple): + if len(constraint) != 2: + raise ValueError( + f"constraint tuple must be (Constraint, bound), " + f"got tuple of length {len(constraint)}" + ) + con_obj, bound = constraint + if bound not in ['lb', 'ub']: + raise ValueError(f"Bound must be 'lb' or 'ub', got: '{bound}'") + key = (con_obj, bound) + if key in info.inequality_multiplier_from_con: + return info.inequality_multiplier_from_con[key] + raise ValueError( + f"Ranged constraint '{con_obj.name}' with bound='{bound}' " + f"does not exist on {model.name}." + ) + + # simple constraints are much easier to deal with + else: + if constraint in info.equality_multiplier_from_con: + return info.equality_multiplier_from_con[constraint] + if constraint in info.inequality_multiplier_from_con: + return info.inequality_multiplier_from_con[constraint] + # may be a ranged constraint + is_ranged = constraint in info.ranged_constraints + if is_ranged: + raise ValueError( + f"Constraint '{constraint.name}' is a ranged constraint. " + f"Provide as tuple: constraint=(constraint_obj, 'lb'|'ub')." + ) + raise ValueError( + f"Constraint '{constraint.name}' does not exist on {model.name}." + ) + + # we need to deal with the case that the user wants multipliers associated with variable bounds + if variable is not None: + # variable bounds must be provided as tuple + if not isinstance(variable, tuple): + raise ValueError( + "variable must be a tuple (Var, 'lb'|'ub'), " + f"got: {type(variable).__name__}" + ) + if len(variable) != 2: + raise ValueError( + f"variable tuple must be (Var, bound), " + f"got tuple of length {len(variable)}" + ) + var_obj, bound = variable + if bound not in ['lb', 'ub']: + raise ValueError(f"Bound must be 'lb' or 'ub', got: '{bound}'") + key = (var_obj, bound) + if key in info.inequality_multiplier_from_con: + return info.inequality_multiplier_from_con[key] + raise ValueError( + f"Variable bound {var_obj.name} (bound='{bound}') " + f"does not exist on {model.name}. " + f"The variable may not have a {bound} bound defined." + ) From 32de8cd52bd24c1717f24c52010f8e5f03d99271 Mon Sep 17 00:00:00 2001 From: Chris Laliwala Date: Tue, 24 Feb 2026 12:21:36 -0500 Subject: [PATCH 03/35] created a test file for the kkt transformation. --- pyomo/core/tests/unit/test_kkt.py | 464 ++++++++++++++++++++++++++++++ 1 file changed, 464 insertions(+) create mode 100644 pyomo/core/tests/unit/test_kkt.py diff --git a/pyomo/core/tests/unit/test_kkt.py b/pyomo/core/tests/unit/test_kkt.py new file mode 100644 index 00000000000..1196f8229ad --- /dev/null +++ b/pyomo/core/tests/unit/test_kkt.py @@ -0,0 +1,464 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2025 +# 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.dependencies import scipy_available +from pyomo.common.numeric_types import value +import pyomo.common.unittest as unittest +from pyomo.common.autoslots import AutoSlots +from pyomo.common.collections import ComponentMap, ComponentSet +from pyomo.common.config import ConfigDict, ConfigValue +from pyomo.environ import ( + ConcreteModel, + Reals, + Block, + Constraint, + ConstraintList, + Expression, + NonNegativeReals, + Objective, + RangeSet, + Reals, + Set, + TransformationFactory, + Var, + maximize, + minimize, + SolverFactory, + TerminationCondition, +) +from pyomo.core.base.suffix import Suffix +from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd +from pyomo.mpec import ComplementarityList, complements +from pyomo.util.vars_from_expressions import get_vars_from_components +from pyomo.util.config_domains import ComponentDataSet +from pyomo.core.expr.compare import ( + assertExpressionsEqual, + assertExpressionsStructurallyEqual, +) + + +@unittest.skipUnless(scipy_available, "Scipy not available") +class TestKKT(unittest.TestCase): + def check_primal_kkt_transformation_solns(self, m, m_reform): + kkt = TransformationFactory('core.kkt') + + m.dual = Suffix(direction=Suffix.IMPORT) + + opt = SolverFactory('ipopt', options={"tol": 1e-8}) + results = opt.solve(m) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.optimal + ) + results = opt.solve(m_reform) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.optimal + ) + + for cons in [ + (m.c1, m_reform.c1), + (m.c2, m_reform.c2), + (m.c3, m_reform.c3), + (m.c4, m_reform.c4), + ]: + primal_con, kkt_reform_con = cons + self.assertAlmostEqual( + value( + abs(kkt.get_multiplier_from_constraint(m_reform, kkt_reform_con)) + ), + value(abs(m.dual[primal_con])), + delta=1e-6, + ) + + for v in [(m.x, m_reform.x), (m.y, m_reform.y)]: + primal_var, kkt_reform_var = v + self.assertAlmostEqual(value(primal_var), value(kkt_reform_var)) + + def test_kkt_solve(self): + m = ConcreteModel() + m.x = Var(domain=Reals) + m.y = Var(domain=Reals) + m.z = Var(domain=Reals) + + m.obj = Objective( + expr=(m.x - 3) ** 2 + (m.y - 2) ** 2 + (m.z - 1) ** 2, sense=minimize + ) + m.c1 = Constraint(expr=m.x**2 + m.y**2 <= 9) + m.c2 = Constraint(expr=m.x + m.y + m.z == 5) + m.c3 = Constraint(expr=m.z >= 1) + m.c4 = Constraint(expr=2 * m.x - m.y <= 4) + + m_reform = m.clone() + TransformationFactory('core.kkt').apply_to(m_reform) + TransformationFactory("mpec.simple_nonlinear").apply_to(m_reform) + + self.check_primal_kkt_transformation_solns(m, m_reform) + + def test_kkt(self): + m = ConcreteModel() + m.x = Var(domain=Reals) + m.y = Var(domain=Reals) + m.z = Var(domain=Reals) + + m.obj = Objective( + expr=(m.x - 3) ** 2 + (m.y - 2) ** 2 + (m.z - 1) ** 2, sense=minimize + ) + m.c1 = Constraint(expr=m.x**2 + m.y**2 <= 9) + m.c2 = Constraint(expr=m.x + m.y + m.z == 5) + m.c3 = Constraint(expr=m.z >= 1) + m.c4 = Constraint(expr=2 * m.x - m.y <= 4) + + kkt = TransformationFactory('core.kkt') + kkt.apply_to(m) + + gamma0 = kkt.get_multiplier_from_constraint(m, m.c2) + alpha_con0 = kkt.get_multiplier_from_constraint(m, m.c1) + alpha_con1 = kkt.get_multiplier_from_constraint(m, m.c3) + alpha_con2 = kkt.get_multiplier_from_constraint(m, m.c4) + + self.assertIs(kkt.get_constraint_from_multiplier(m, gamma0), m.c2) + self.assertIs(kkt.get_constraint_from_multiplier(m, alpha_con0), m.c1) + self.assertIs(kkt.get_constraint_from_multiplier(m, alpha_con1), m.c3) + self.assertIs(kkt.get_constraint_from_multiplier(m, alpha_con2), m.c4) + + c2 = kkt.get_constraint_from_multiplier(m, gamma0) + c1 = kkt.get_constraint_from_multiplier(m, alpha_con0) + c3 = kkt.get_constraint_from_multiplier(m, alpha_con1) + c4 = kkt.get_constraint_from_multiplier(m, alpha_con2) + + self.assertIs(kkt.get_multiplier_from_constraint(m, c2), gamma0) + self.assertIs(kkt.get_multiplier_from_constraint(m, c1), alpha_con0) + self.assertIs(kkt.get_multiplier_from_constraint(m, c3), alpha_con1) + self.assertIs(kkt.get_multiplier_from_constraint(m, c4), alpha_con2) + + self.assertIs(gamma0.ctype, Var) + self.assertEqual(gamma0.domain, Reals) + self.assertIsNone(gamma0.ub) + self.assertIsNone(gamma0.lb) + self.assertIs(alpha_con0.ctype, Var) + self.assertEqual(alpha_con0.domain, NonNegativeReals) + self.assertIsNone(alpha_con0.ub) + self.assertEqual(alpha_con1.lb, 0) + self.assertIs(alpha_con1.ctype, Var) + self.assertEqual(alpha_con1.domain, NonNegativeReals) + self.assertIsNone(alpha_con1.ub) + self.assertEqual(alpha_con2.lb, 0) + self.assertIs(alpha_con2.ctype, Var) + self.assertEqual(alpha_con2.domain, NonNegativeReals) + self.assertIsNone(alpha_con2.ub) + self.assertEqual(alpha_con2.lb, 0) + + self.assertIs(c1.ctype, Constraint) + self.assertIs(c2.ctype, Constraint) + self.assertIs(c3.ctype, Constraint) + self.assertIs(c4.ctype, Constraint) + + # test Lagrangean expression + assertExpressionsStructurallyEqual( + self, + m.kkt.lagrangean.expr, + (m.x - 3) ** 2 + + (m.y - 2) ** 2 + + (m.z - 1) ** 2 + + (5.0 - (m.x + m.y + m.z)) * gamma0 + + (m.x**2 + m.y**2 - 9.0) * alpha_con0 + + (1.0 - m.z) * alpha_con1 + + (2 * m.x - m.y - 4.0) * alpha_con2, + ) + + # test stationarity conditions + assertExpressionsStructurallyEqual( + self, + m.kkt.stationarity_conditions[1].expr, + 2 * alpha_con2 + 2 * alpha_con0 * m.x - gamma0 + 2 * (m.x - 3) == 0, + ) + assertExpressionsStructurallyEqual( + self, + m.kkt.stationarity_conditions[2].expr, + -alpha_con2 + 2 * alpha_con0 * m.y - gamma0 + 2 * (m.y - 2) == 0, + ) + assertExpressionsStructurallyEqual( + self, + m.kkt.stationarity_conditions[3].expr, + -alpha_con1 - gamma0 + 2 * (m.z - 1) == 0, + ) + + # test complementarity constraints + assertExpressionsStructurallyEqual( + self, m.kkt.complements[1]._args[0], 0 <= alpha_con0 + ) + assertExpressionsStructurallyEqual( + self, m.kkt.complements[1]._args[1], m.x**2 + m.y**2 - 9.0 <= 0 + ) + + assertExpressionsStructurallyEqual( + self, m.kkt.complements[2]._args[0], 0 <= alpha_con1 + ) + assertExpressionsStructurallyEqual( + self, m.kkt.complements[2]._args[1], 1.0 - m.z <= 0 + ) + + assertExpressionsStructurallyEqual( + self, m.kkt.complements[3]._args[0], 0 <= alpha_con2 + ) + assertExpressionsStructurallyEqual( + self, m.kkt.complements[3]._args[1], 2 * m.x - m.y - 4.0 <= 0 + ) + + self.assertFalse(m.obj.active) + + self.assertTrue(m.kkt.dummy_obj.active) + self.assertEqual(m.kkt.dummy_obj.expr, 1.0) + + def get_bilevel_model(self): + m = ConcreteModel(name='bilevel') + + m.outer1 = Var(domain=Reals) + m.outer2 = Var(domain=Reals) + + # Inner (follower) variables - decision variables for KKT conditions + m.x = Var(domain=Reals) + m.y = Var(domain=Reals) + m.z = Var(domain=Reals) + + # Inner problem objective (depends on outer variables) + m.obj = Objective( + expr=(m.x - m.outer1) ** 2 + (m.y - 2) ** 2 + (m.z - m.outer2) ** 2, + sense=minimize, + ) + + # Inner problem constraints (some depend on outer variables) + m.c1 = Constraint(expr=m.x**2 + m.y**2 <= 9 + m.outer1) + m.c2 = Constraint(expr=m.x + m.y + m.z == 5 + m.outer2) + m.c3 = Constraint(expr=m.z >= 1) + m.c4 = Constraint(expr=2 * m.x - m.y <= 4 + 0.5 * m.outer1) + + return m + + def test_parametrized_kkt(self): + m = self.get_bilevel_model() + + kkt = TransformationFactory('core.kkt') + kkt.apply_to(m, parametrize_wrt=[m.outer1, m.outer2]) + TransformationFactory("mpec.simple_nonlinear").apply_to(m) + + gamma0 = kkt.get_multiplier_from_constraint(m, m.c2) + alpha_con0 = kkt.get_multiplier_from_constraint(m, m.c1) + alpha_con1 = kkt.get_multiplier_from_constraint(m, m.c3) + alpha_con2 = kkt.get_multiplier_from_constraint(m, m.c4) + + self.assertIs(kkt.get_constraint_from_multiplier(m, gamma0), m.c2) + self.assertIs(kkt.get_constraint_from_multiplier(m, alpha_con0), m.c1) + self.assertIs(kkt.get_constraint_from_multiplier(m, alpha_con1), m.c3) + self.assertIs(kkt.get_constraint_from_multiplier(m, alpha_con2), m.c4) + + c2 = kkt.get_constraint_from_multiplier(m, gamma0) + c1 = kkt.get_constraint_from_multiplier(m, alpha_con0) + c3 = kkt.get_constraint_from_multiplier(m, alpha_con1) + c4 = kkt.get_constraint_from_multiplier(m, alpha_con2) + + self.assertIs(kkt.get_multiplier_from_constraint(m, c2), gamma0) + self.assertIs(kkt.get_multiplier_from_constraint(m, c1), alpha_con0) + self.assertIs(kkt.get_multiplier_from_constraint(m, c3), alpha_con1) + self.assertIs(kkt.get_multiplier_from_constraint(m, c4), alpha_con2) + + self.assertIs(gamma0.ctype, Var) + self.assertEqual(gamma0.domain, Reals) + self.assertIsNone(gamma0.ub) + self.assertIsNone(gamma0.lb) + self.assertIs(alpha_con0.ctype, Var) + self.assertEqual(alpha_con0.domain, NonNegativeReals) + self.assertIsNone(alpha_con0.ub) + self.assertEqual(alpha_con1.lb, 0) + self.assertIs(alpha_con1.ctype, Var) + self.assertEqual(alpha_con1.domain, NonNegativeReals) + self.assertIsNone(alpha_con1.ub) + self.assertEqual(alpha_con2.lb, 0) + self.assertIs(alpha_con2.ctype, Var) + self.assertEqual(alpha_con2.domain, NonNegativeReals) + self.assertIsNone(alpha_con2.ub) + self.assertEqual(alpha_con2.lb, 0) + + self.assertIs(c1.ctype, Constraint) + self.assertIs(c2.ctype, Constraint) + self.assertIs(c3.ctype, Constraint) + self.assertIs(c4.ctype, Constraint) + + # test Lagrangean expression + assertExpressionsStructurallyEqual( + self, + m.kkt.lagrangean.expr, + (m.x - m.outer1) ** 2 + + (m.y - 2) ** 2 + + (m.z - m.outer2) ** 2 + + (-(m.x + m.y + m.z - (5 + m.outer2))) * gamma0 + + (m.x**2 + m.y**2 - (9 + m.outer1)) * alpha_con0 + + (1.0 - m.z) * alpha_con1 + + (2 * m.x - m.y - (4 + 0.5 * m.outer1)) * alpha_con2, + ) + + # test stationarity conditions + assertExpressionsStructurallyEqual( + self, + m.kkt.stationarity_conditions[1].expr, + 2 * alpha_con2 + 2 * alpha_con0 * m.x - gamma0 + 2 * (m.x - m.outer1) == 0, + ) + assertExpressionsStructurallyEqual( + self, + m.kkt.stationarity_conditions[2].expr, + -alpha_con2 + 2 * alpha_con0 * m.y - gamma0 + 2 * (m.y - 2) == 0, + ) + assertExpressionsStructurallyEqual( + self, + m.kkt.stationarity_conditions[3].expr, + -alpha_con1 - gamma0 + 2 * (m.z - m.outer2) == 0, + ) + + # test complementarity constraints + assertExpressionsStructurallyEqual( + self, m.kkt.complements[1]._args[0], 0 <= alpha_con0 + ) + assertExpressionsStructurallyEqual( + self, m.kkt.complements[1]._args[1], m.x**2 + m.y**2 - (9 + m.outer1) <= 0 + ) + assertExpressionsStructurallyEqual( + self, m.kkt.complements[2]._args[0], 0 <= alpha_con1 + ) + assertExpressionsStructurallyEqual( + self, m.kkt.complements[2]._args[1], 1.0 - m.z <= 0 + ) + assertExpressionsStructurallyEqual( + self, m.kkt.complements[3]._args[0], 0 <= alpha_con2 + ) + assertExpressionsStructurallyEqual( + self, + m.kkt.complements[3]._args[1], + 2 * m.x - m.y - (4 + 0.5 * m.outer1) <= 0, + ) + + self.assertFalse(m.obj.active) + + self.assertTrue(m.kkt.dummy_obj.active) + self.assertEqual(m.kkt.dummy_obj.expr, 1.0) + + def test_solve_parametrized_kkt(self): + m = self.get_bilevel_model() + + # test with a few values + m.outer1.fix(1) + m.outer2.fix(1) + + m_reform = m.clone() + TransformationFactory('core.kkt').apply_to( + m_reform, parametrize_wrt=[m_reform.outer1, m_reform.outer2] + ) + TransformationFactory("mpec.simple_nonlinear").apply_to(m_reform) + + self.check_primal_kkt_transformation_solns(m, m_reform) + + m.outer1.fix(1) + m.outer2.fix(5) + + m_reform = m.clone() + TransformationFactory('core.kkt').apply_to( + m_reform, parametrize_wrt=[m_reform.outer1, m_reform.outer2] + ) + TransformationFactory("mpec.simple_nonlinear").apply_to(m_reform) + + self.check_primal_kkt_transformation_solns(m, m_reform) + + m.outer1.fix(3) + m.outer2.fix(3) + + m_reform = m.clone() + TransformationFactory('core.kkt').apply_to( + m_reform, parametrize_wrt=[m_reform.outer1, m_reform.outer2] + ) + TransformationFactory("mpec.simple_nonlinear").apply_to(m_reform) + + self.check_primal_kkt_transformation_solns(m, m_reform) + + def test_multipe_obj_error(self): + m = self.get_bilevel_model() + m.obj.deactivate() + kkt = TransformationFactory('core.kkt') + + with self.assertRaisesRegex( + ValueError, f"model must have only one active objective; found 0" + ): + kkt.apply_to(m) + + def test_kkt_block_name_error(self): + m = ConcreteModel() + m.x = Var(domain=Reals) + m.y = Var(domain=Reals) + m.obj = Objective(expr=(m.x - 3) ** 2, sense=minimize) + m.c1 = Constraint(expr=m.x**2 + m.y**2 <= 9) + m.b1 = Block() + kkt = TransformationFactory('core.kkt') + + with self.assertRaisesRegex( + ValueError, + f"""model already has an attribute with the + specified kkt_block_name: 'b1'""", + ): + kkt.apply_to(m, kkt_block_name='b1') + + def test_parametrize_wrt_unknown_error(self): + m = ConcreteModel() + m.x = Var(domain=Reals) + m.y = Var(domain=Reals) + m.obj = Objective(expr=(m.x - 3) ** 2, sense=minimize) + m.c1 = Constraint(expr=m.x**2 + m.y**2 <= 9) + m.b1 = Block() + m.b1.x1 = Var(domain=Reals) + m.b1.deactivate() + kkt = TransformationFactory('core.kkt') + + with self.assertRaisesRegex( + ValueError, + "A variable passed in parametrize_wrt does not exist on an " + "active constraint or objective within the model.", + ): + kkt.apply_to(m, parametrize_wrt=[m.b1.x1]) + + def test_get_constraint_from_multiplier_error(self): + m = ConcreteModel(name="model") + m.x = Var(domain=Reals) + m.y = Var(domain=Reals) + m.obj = Objective(expr=(m.x - 3) ** 2, sense=minimize) + m.c1 = Constraint(expr=m.x**2 + m.y**2 <= 9) + kkt = TransformationFactory('core.kkt') + kkt.apply_to(m) + + m2 = ConcreteModel() + m2.gamma = Var(domain=Reals) + + with self.assertRaisesRegex( + ValueError, f"The KKT multiplier: {m2.gamma}, does not exist on model." + ): + kkt.get_constraint_from_multiplier(m, m2.gamma) + + # def test_getmultiplier_from_constraint_error(self): + # m = ConcreteModel(name="model") + # m.x = Var(domain=Reals) + # m.y = Var(domain=Reals) + # m.obj = Objective( + # expr=(m.x - 3) ** 2, sense=minimize + # ) + # m.c1 = Constraint(expr=m.x**2 + m.y**2 <= 9) + # kkt = TransformationFactory('core.kkt') + # kkt.apply_to(m) + + # with self.assertRaisesRegex( + # ValueError, + # f"The KKT multiplier: {m2.gamma}, does not exist on model." + # ): + # kkt.get_constraint_from_multiplier(m, m2.gamma) From a4537f04c03701daffffbcd4637d969f0603028f Mon Sep 17 00:00:00 2001 From: Claliwal <136067410+ChrisLaliwala@users.noreply.github.com> Date: Wed, 25 Feb 2026 12:29:32 -0500 Subject: [PATCH 04/35] Apply suggestions from code review Co-authored-by: Bethany Nicholson --- pyomo/core/plugins/transform/kkt.py | 16 +++++++--------- pyomo/core/tests/unit/test_kkt.py | 16 +++++++--------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index aa857cec240..89dedca4cf7 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -1,13 +1,11 @@ -# ___________________________________________________________________________ +# ____________________________________________________________________________________ # -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2025 -# 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. -# ___________________________________________________________________________ +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2026 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.autoslots import AutoSlots diff --git a/pyomo/core/tests/unit/test_kkt.py b/pyomo/core/tests/unit/test_kkt.py index 1196f8229ad..b78c05b9216 100644 --- a/pyomo/core/tests/unit/test_kkt.py +++ b/pyomo/core/tests/unit/test_kkt.py @@ -1,13 +1,11 @@ -# ___________________________________________________________________________ +# ____________________________________________________________________________________ # -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2025 -# 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. -# ___________________________________________________________________________ +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2026 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.dependencies import scipy_available from pyomo.common.numeric_types import value From 57c94bbb4d65a9b53928f4c791fdc63bc7f04d1e Mon Sep 17 00:00:00 2001 From: Chris Laliwala Date: Wed, 25 Feb 2026 12:57:24 -0500 Subject: [PATCH 05/35] formatting. --- pyomo/core/plugins/transform/kkt.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index 89dedca4cf7..10698a110a0 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -366,9 +366,7 @@ def get_multiplier_from_constraint(self, model, constraint=None, variable=None): """ if (constraint is None) and (variable is None): - raise ValueError( - "Must provide 'constraint' or 'variable'." - ) + raise ValueError("Must provide 'constraint' or 'variable'.") if (constraint is not None) and (variable is not None): raise ValueError( "Cannot provide both 'constraint' and 'variable'. " "Provide only one." @@ -404,7 +402,7 @@ def get_multiplier_from_constraint(self, model, constraint=None, variable=None): if is_ranged: raise ValueError( f"Constraint '{constraint.name}' is a ranged constraint. " - f"Provide as tuple: constraint=(constraint_obj, 'lb'|'ub')." + "Provide as tuple: constraint=(constraint_obj, 'lb'|'ub')." ) raise ValueError( f"Constraint '{constraint.name}' does not exist on {model.name}." From 6cf46c66a0f11d794ac0f8ce7a29fa29b4e456b0 Mon Sep 17 00:00:00 2001 From: Chris Laliwala Date: Wed, 25 Feb 2026 12:57:42 -0500 Subject: [PATCH 06/35] finished adding tests. --- pyomo/core/tests/unit/test_kkt.py | 84 +++++++++++++++++++++++++------ 1 file changed, 68 insertions(+), 16 deletions(-) diff --git a/pyomo/core/tests/unit/test_kkt.py b/pyomo/core/tests/unit/test_kkt.py index b78c05b9216..a0e7db2763d 100644 --- a/pyomo/core/tests/unit/test_kkt.py +++ b/pyomo/core/tests/unit/test_kkt.py @@ -444,19 +444,71 @@ def test_get_constraint_from_multiplier_error(self): ): kkt.get_constraint_from_multiplier(m, m2.gamma) - # def test_getmultiplier_from_constraint_error(self): - # m = ConcreteModel(name="model") - # m.x = Var(domain=Reals) - # m.y = Var(domain=Reals) - # m.obj = Objective( - # expr=(m.x - 3) ** 2, sense=minimize - # ) - # m.c1 = Constraint(expr=m.x**2 + m.y**2 <= 9) - # kkt = TransformationFactory('core.kkt') - # kkt.apply_to(m) - - # with self.assertRaisesRegex( - # ValueError, - # f"The KKT multiplier: {m2.gamma}, does not exist on model." - # ): - # kkt.get_constraint_from_multiplier(m, m2.gamma) + def test_get_multiplier_from_constraint_error(self): + m = ConcreteModel(name="model") + m.x = Var(domain=Reals, bounds=(0, 10)) + m.y = Var(domain=Reals) + m.obj = Objective(expr=(m.x - 3) ** 2, sense=minimize) + m.c1 = Constraint(expr=m.x**2 + m.y**2 <= 9) + m.c2 = Constraint(expr=(0, m.y, 10)) + kkt = TransformationFactory('core.kkt') + kkt.apply_to(m) + + with self.assertRaisesRegex( + ValueError, "Must provide 'constraint' or 'variable'." + ): + kkt.get_multiplier_from_constraint(m, constraint=None, variable=None) + + with self.assertRaisesRegex( + ValueError, + "Cannot provide both 'constraint' and 'variable'. " "Provide only one", + ): + kkt.get_multiplier_from_constraint(m, constraint=m.c1, variable=(m.x, "ub")) + + with self.assertRaisesRegex( + ValueError, + r"constraint tuple must be \(Constraint, bound\), " "got tuple of length 1", + ): + kkt.get_multiplier_from_constraint(m, constraint=(m.c2,)) + + with self.assertRaisesRegex( + ValueError, "Bound must be 'lb' or 'ub', got: 'no bound'" + ): + kkt.get_multiplier_from_constraint(m, constraint=(m.c2, 'no bound')) + + with self.assertRaisesRegex( + ValueError, + "Ranged constraint 'c1' with bound='ub' " "does not exist on model.", + ): + kkt.get_multiplier_from_constraint(m, constraint=(m.c1, 'ub')) + + with self.assertRaisesRegex( + ValueError, + "Constraint 'c2' is a ranged constraint. " + r"Provide as tuple: constraint=\(constraint_obj, 'lb'|'ub'\).", + ): + kkt.get_multiplier_from_constraint(m, constraint=m.c2) + + with self.assertRaisesRegex( + ValueError, r"variable must be a tuple \(Var, 'lb'|'ub'\), " f"got: x" + ): + kkt.get_multiplier_from_constraint(m, variable=m.x) + + with self.assertRaisesRegex( + ValueError, + r"variable tuple must be \(Var, bound\), " "got tuple of length 1", + ): + kkt.get_multiplier_from_constraint(m, variable=(m.x,)) + + with self.assertRaisesRegex( + ValueError, f"Bound must be 'lb' or 'ub', got: 'no bound'" + ): + kkt.get_multiplier_from_constraint(m, variable=(m.x, 'no bound')) + + with self.assertRaisesRegex( + ValueError, + r"Variable bound y \(bound='ub'\) " + "does not exist on model. " + "The variable may not have a ub bound defined.", + ): + kkt.get_multiplier_from_constraint(m, variable=(m.y, 'ub')) From a41c5ee5d8aa726bef056bef7ba899af81767c44 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Fri, 6 Mar 2026 09:16:22 -0700 Subject: [PATCH 07/35] Run black --- pyomo/core/plugins/transform/kkt.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index 10698a110a0..bd7d8659b8b 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -98,10 +98,8 @@ def apply_to(self, model, **kwds): config.set_value(kwds) if hasattr(model, config.kkt_block_name): - raise ValueError( - f"""model already has an attribute with the - specified kkt_block_name: '{config.kkt_block_name}'""" - ) + raise ValueError(f"""model already has an attribute with the + specified kkt_block_name: '{config.kkt_block_name}'""") # we should check that all vars the user fixed are included # in parametrize_wrt @@ -275,7 +273,7 @@ def _var_bound_expr_rule(kkt, i): for i in kkt_block.var_bound_set: lagrangean += kkt_block.var_bound_expr[i] * kkt_block.alpha_var_bound[i] # mappings for ranged constraints built from variable bounds - (var, bound) = info.var_bound_multiplier_index_to_con[i] + var, bound = info.var_bound_multiplier_index_to_con[i] info.inequality_con_from_multiplier[kkt_block.alpha_var_bound[i]] = ( var, bound, From a9149b16d9328e772d6d848fa6dbf3c34b610756 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Fri, 6 Mar 2026 09:55:02 -0700 Subject: [PATCH 08/35] Fix typos --- pyomo/core/plugins/transform/kkt.py | 2 +- pyomo/core/tests/unit/test_kkt.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index bd7d8659b8b..7eafc7eb271 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -335,7 +335,7 @@ def get_constraint_from_multiplier(self, model, multiplier_var): return info.equality_con_from_multiplier[multiplier_var] if multiplier_var in info.inequality_con_from_multiplier.keys(): # if this multiplier var maps to a ranged constraint, we will return a tuple - # so that we can indicate which bound the multplier var maps to + # so that we can indicate which bound the multiplier var maps to return info.inequality_con_from_multiplier[multiplier_var] raise ValueError( f"The KKT multiplier: {multiplier_var.name}, does not exist on {model.name}." diff --git a/pyomo/core/tests/unit/test_kkt.py b/pyomo/core/tests/unit/test_kkt.py index a0e7db2763d..0025f6c6c2c 100644 --- a/pyomo/core/tests/unit/test_kkt.py +++ b/pyomo/core/tests/unit/test_kkt.py @@ -383,7 +383,7 @@ def test_solve_parametrized_kkt(self): self.check_primal_kkt_transformation_solns(m, m_reform) - def test_multipe_obj_error(self): + def test_multiple_obj_error(self): m = self.get_bilevel_model() m.obj.deactivate() kkt = TransformationFactory('core.kkt') From 74638a75f5117917e9e50ce77f7bcc0c9e9966f6 Mon Sep 17 00:00:00 2001 From: Claliwal <136067410+ChrisLaliwala@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:44:23 -0400 Subject: [PATCH 09/35] Update pyomo/core/plugins/transform/kkt.py Co-authored-by: John Siirola --- pyomo/core/plugins/transform/kkt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index 7eafc7eb271..5053b26a6d8 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -110,7 +110,7 @@ def apply_to(self, model, **kwds): vars_in_obj = ComponentSet( get_vars_from_components(model, Objective, active=True, descend_into=True) ) - vars_in_model = ComponentSet(vars_in_cons | vars_in_obj) + vars_in_model = vars_in_cons | vars_in_obj fixed_vars_in_model = ComponentSet(v for v in vars_in_model if v.is_fixed()) missing = [v for v in fixed_vars_in_model if v not in params] if missing: From 115019d6f4ad16c14cc8f189b3ec67da2a294b3d Mon Sep 17 00:00:00 2001 From: Claliwal <136067410+ChrisLaliwala@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:45:17 -0400 Subject: [PATCH 10/35] Update pyomo/core/plugins/transform/kkt.py Co-authored-by: John Siirola --- pyomo/core/plugins/transform/kkt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index 5053b26a6d8..ab07ed0048d 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -118,7 +118,7 @@ def apply_to(self, model, **kwds): # we should also check that all vars the user passes in parametrize_wrt # exist on an active constraint or objective within the model - unknown = [v for v in params if v not in vars_in_model] + unknown = params - vars_in_model if unknown: raise ValueError( "A variable passed in parametrize_wrt does not exist on an " From cc4ebdeb4ab0b0c9cb1567d6d9b0aa128354e75b Mon Sep 17 00:00:00 2001 From: Claliwal <136067410+ChrisLaliwala@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:02:41 -0400 Subject: [PATCH 11/35] Update pyomo/core/plugins/transform/kkt.py Co-authored-by: John Siirola --- pyomo/core/plugins/transform/kkt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index ab07ed0048d..d36ad1d3cf1 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -103,7 +103,7 @@ def apply_to(self, model, **kwds): # we should check that all vars the user fixed are included # in parametrize_wrt - params = config.parametrize_wrt or ComponentSet() + params = config.parametrize_wrt vars_in_cons = ComponentSet( get_vars_from_components(model, Constraint, active=True, descend_into=True) ) From 4f3b7f716f0f7b6a40b62dd3d897adeaefa3d43c Mon Sep 17 00:00:00 2001 From: Chris Laliwala Date: Tue, 17 Mar 2026 11:05:57 -0400 Subject: [PATCH 12/35] changed parametrize_wrt default value to [] --- pyomo/core/plugins/transform/kkt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index d36ad1d3cf1..be92689709b 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -81,7 +81,7 @@ class NonLinearProgrammingKKT: CONFIG.declare( 'parametrize_wrt', ConfigValue( - default=None, + default=[], domain=ComponentDataSet(Var), description='Vars to treat as data for the purposes of generating KKT reformulation', doc=""" From cff252fe8d0de36772b1638c89fcaf1c88306563 Mon Sep 17 00:00:00 2001 From: Claliwal <136067410+ChrisLaliwala@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:10:20 -0400 Subject: [PATCH 13/35] Update pyomo/core/plugins/transform/kkt.py Co-authored-by: John Siirola --- pyomo/core/plugins/transform/kkt.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index be92689709b..080654e6df7 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -125,11 +125,11 @@ def apply_to(self, model, **kwds): "active constraint or objective within the model." ) - kkt_block = Block() - model.add_component(config.kkt_block_name, kkt_block) + kkt_block = Block(concrete=True) kkt_block.parametrize_wrt = params - - return self._reformulate(model, kkt_block) + self._reformulate(model, kkt_block) + model.add_component(config.kkt_block_name, kkt_block) + return model def _reformulate(self, model, kkt_block): active_objs = list( From c10d5a7f2e51948b22afa9a9dd851f2c9b6d35f5 Mon Sep 17 00:00:00 2001 From: Chris Laliwala Date: Tue, 17 Mar 2026 11:31:23 -0400 Subject: [PATCH 14/35] cleaned up ComponentSet declarations. --- pyomo/core/plugins/transform/kkt.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index 080654e6df7..9d863deb908 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -112,7 +112,7 @@ def apply_to(self, model, **kwds): ) vars_in_model = vars_in_cons | vars_in_obj fixed_vars_in_model = ComponentSet(v for v in vars_in_model if v.is_fixed()) - missing = [v for v in fixed_vars_in_model if v not in params] + missing = fixed_vars_in_model - params if missing: raise ValueError("All fixed variables must be included in parametrize_wrt.") @@ -204,14 +204,12 @@ def _construct_lagrangean(self, model, kkt_block): kkt_block, Constraint, active=True, descend_into=True ) ) - vars_in_cons = ComponentSet(vars_in_cons_all - vars_in_kkt_cons) + vars_in_cons = vars_in_cons_all - vars_in_kkt_cons vars_in_obj = ComponentSet( get_vars_from_components(model, Objective, active=True, descend_into=True) ) - kkt_block.var_set = ComponentSet(vars_in_cons | vars_in_obj) - kkt_block.var_set = ComponentSet( - v for v in kkt_block.var_set if v not in kkt_block.parametrize_wrt - ) + kkt_block.var_set = vars_in_cons | vars_in_obj + kkt_block.var_set = kkt_block.var_set - kkt_block.parametrize_wrt for var in kkt_block.var_set: if var.has_lb(): var_bound_sides.append((var, "lb")) From f5ea2b009397312258c86fa9864ef2a5b74814bf Mon Sep 17 00:00:00 2001 From: Claliwal <136067410+ChrisLaliwala@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:54:32 -0400 Subject: [PATCH 15/35] Update pyomo/core/plugins/transform/kkt.py Co-authored-by: John Siirola --- pyomo/core/plugins/transform/kkt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index 9d863deb908..64052e34884 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -329,7 +329,7 @@ def get_constraint_from_multiplier(self, model, multiplier_var): """ info = model.private_data() - if multiplier_var in info.equality_con_from_multiplier.keys(): + if multiplier_var in info.equality_con_from_multiplier: return info.equality_con_from_multiplier[multiplier_var] if multiplier_var in info.inequality_con_from_multiplier.keys(): # if this multiplier var maps to a ranged constraint, we will return a tuple From 7cf29ff8c05593cde279e4d62db0ec271ef1664c Mon Sep 17 00:00:00 2001 From: Chris Laliwala Date: Tue, 17 Mar 2026 11:59:29 -0400 Subject: [PATCH 16/35] removed call to .keys() --- pyomo/core/plugins/transform/kkt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index 64052e34884..63a1e561c0e 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -331,7 +331,7 @@ def get_constraint_from_multiplier(self, model, multiplier_var): info = model.private_data() if multiplier_var in info.equality_con_from_multiplier: return info.equality_con_from_multiplier[multiplier_var] - if multiplier_var in info.inequality_con_from_multiplier.keys(): + if multiplier_var in info.inequality_con_from_multiplier: # if this multiplier var maps to a ranged constraint, we will return a tuple # so that we can indicate which bound the multiplier var maps to return info.inequality_con_from_multiplier[multiplier_var] From cfa0555d8c3d9f2de37ec3e55f7137e2f53c8f9f Mon Sep 17 00:00:00 2001 From: Chris Laliwala Date: Tue, 17 Mar 2026 13:17:46 -0400 Subject: [PATCH 17/35] fixed logic for collecting and sorting constraints. --- pyomo/core/plugins/transform/kkt.py | 35 +++++++++++++++-------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index 63a1e561c0e..3e0ee405d6f 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -98,8 +98,10 @@ def apply_to(self, model, **kwds): config.set_value(kwds) if hasattr(model, config.kkt_block_name): - raise ValueError(f"""model already has an attribute with the - specified kkt_block_name: '{config.kkt_block_name}'""") + raise ValueError( + f"""model already has an attribute with the + specified kkt_block_name: '{config.kkt_block_name}'""" + ) # we should check that all vars the user fixed are included # in parametrize_wrt @@ -158,25 +160,24 @@ def _construct_lagrangean(self, model, kkt_block): for con in model.component_data_objects( Constraint, descend_into=True, active=True ): - if con.has_lb() and con.has_ub() and (con.lower == con.upper): + lower, body, upper = con.to_bounded_expression() + if con.equality: equality_cons.append(con) - info.equality_con_to_expr[con] = con.upper - con.body + info.equality_con_to_expr[con] = upper - body else: - if con.has_lb(): + if lower is not None: inequality_cons.append((con, "lb")) - info.inequality_con_to_expr.setdefault(con, {})["lb"] = ( - con.lower - con.body - ) - if con.has_ub(): + info.inequality_con_to_expr.setdefault(con, {})["lb"] = lower - body + if upper is not None: inequality_cons.append((con, "ub")) - info.inequality_con_to_expr.setdefault(con, {})["ub"] = ( - con.body - con.upper - ) - # we want to keep track of the ranged constraints because the mapping between - # multipliers and ranged constraints will be a tuple (to indicate bound as well) - # instead of simply the model object - if con.has_lb() and con.has_ub() and (con.lower != con.upper): - info.ranged_constraints.add(con) + info.inequality_con_to_expr.setdefault(con, {})["ub"] = body - upper + + # lower is not None and upper is not None -> ranged constraint + if lower is not None: + # we want to keep track of the ranged constraints because the mapping between + # multipliers and ranged constraints will be a tuple (to indicate bound as well) + # instead of simply the model object + info.ranged_constraints.add(con) kkt_block.equality_cons_set = Set(initialize=equality_cons, ordered=True) kkt_block.gamma_set = RangeSet(0, len(equality_cons) - 1) From e96154604d38b20ab28de29febb1bc4c15f2ea51 Mon Sep 17 00:00:00 2001 From: Chris Laliwala Date: Tue, 17 Mar 2026 14:20:20 -0400 Subject: [PATCH 18/35] converted unecessary Sets to Lists. Also, removed the unecessary Set 'vars_in_kkt_cons'. --- pyomo/core/plugins/transform/kkt.py | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index 3e0ee405d6f..01af319992b 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -179,33 +179,25 @@ def _construct_lagrangean(self, model, kkt_block): # instead of simply the model object info.ranged_constraints.add(con) - kkt_block.equality_cons_set = Set(initialize=equality_cons, ordered=True) + kkt_block.equality_cons_list = list(equality_cons) kkt_block.gamma_set = RangeSet(0, len(equality_cons) - 1) kkt_block.gamma = Var(kkt_block.gamma_set, domain=Reals) info.equality_multiplier_index_to_con = dict( - enumerate(kkt_block.equality_cons_set.ordered_data()) - ) - kkt_block.inequality_cons_set = Set( - dimen=2, initialize=inequality_cons, ordered=True + enumerate(kkt_block.equality_cons_list) ) + kkt_block.inequality_cons_list = list(inequality_cons) kkt_block.alpha_con_set = RangeSet(0, len(inequality_cons) - 1) kkt_block.alpha_con = Var(kkt_block.alpha_con_set, domain=NonNegativeReals) info.inequality_multiplier_index_to_con = dict( - enumerate(kkt_block.inequality_cons_set.ordered_data()) + enumerate(kkt_block.inequality_cons_list) ) # we also need to consider inequality constraints # formed by the user specifying variable bounds var_bound_sides = [] - vars_in_cons_all = ComponentSet( + vars_in_cons = ComponentSet( get_vars_from_components(model, Constraint, active=True, descend_into=True) ) - vars_in_kkt_cons = ComponentSet( - get_vars_from_components( - kkt_block, Constraint, active=True, descend_into=True - ) - ) - vars_in_cons = vars_in_cons_all - vars_in_kkt_cons vars_in_obj = ComponentSet( get_vars_from_components(model, Objective, active=True, descend_into=True) ) @@ -220,6 +212,7 @@ def _construct_lagrangean(self, model, kkt_block): kkt_block.alpha_var_bound = Var( kkt_block.var_bound_set, domain=NonNegativeReals ) + info.var_bound_multiplier_index_to_con = dict(enumerate(var_bound_sides)) # indexing the inequality constraint expressions will help @@ -245,14 +238,12 @@ def _var_bound_expr_rule(kkt, i): elif sense == minimize: lagrangean = obj[0].expr - for index, con in enumerate(kkt_block.equality_cons_set.ordered_data()): + for index, con in enumerate(kkt_block.equality_cons_list): lagrangean += info.equality_con_to_expr[con] * kkt_block.gamma[index] info.equality_con_from_multiplier[kkt_block.gamma[index]] = con info.equality_multiplier_from_con[con] = kkt_block.gamma[index] - for index, (con, bound) in enumerate( - kkt_block.inequality_cons_set.ordered_data() - ): + for index, (con, bound) in enumerate(kkt_block.inequality_cons_list): lagrangean += ( info.inequality_con_to_expr[con][bound] * kkt_block.alpha_con[index] ) @@ -292,9 +283,7 @@ def _enforce_stationarity_conditions(self, kkt_block): def _enforce_complementarity_conditions(self, model, kkt_block): info = model.private_data() kkt_block.complements = ComplementarityList() - for index, (con, bound) in enumerate( - kkt_block.inequality_cons_set.ordered_data() - ): + for index, (con, bound) in enumerate(kkt_block.inequality_cons_list): kkt_block.complements.add( complements( kkt_block.alpha_con[index] >= 0, From 9df14019e289ebb5fdb37bd73fde5a15941ce732 Mon Sep 17 00:00:00 2001 From: Claliwal <136067410+ChrisLaliwala@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:28:39 -0400 Subject: [PATCH 19/35] Update pyomo/core/plugins/transform/kkt.py Co-authored-by: John Siirola --- pyomo/core/plugins/transform/kkt.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index 01af319992b..8e80157ce65 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -231,12 +231,8 @@ def _var_bound_expr_rule(kkt, i): obj = list( model.component_data_objects(Objective, active=True, descend_into=True) ) - # maximize is -1 and minimize is +1 - sense = obj[0].sense - if sense == maximize: - lagrangean = -obj[0].expr - elif sense == minimize: - lagrangean = obj[0].expr + # Note: maximize is -1 and minimize is +1 + lagrangean = obj[0].sense * obj[0].expr for index, con in enumerate(kkt_block.equality_cons_list): lagrangean += info.equality_con_to_expr[con] * kkt_block.gamma[index] From 2de227e129380b7b3548cb4b2744b08cf1a329ef Mon Sep 17 00:00:00 2001 From: Chris Laliwala Date: Tue, 17 Mar 2026 16:57:50 -0400 Subject: [PATCH 20/35] simplified API for getting multipliers from model components. --- pyomo/core/plugins/transform/kkt.py | 100 ++++++++-------------------- pyomo/core/tests/unit/test_kkt.py | 59 ++-------------- 2 files changed, 36 insertions(+), 123 deletions(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index 8e80157ce65..badd191d0ab 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -26,6 +26,8 @@ maximize, minimize, ) +from pyomo.core.base.constraint import ConstraintData +from pyomo.core.base.var import VarData from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd from pyomo.mpec import ComplementarityList, complements from pyomo.util.vars_from_expressions import get_vars_from_components @@ -325,92 +327,48 @@ def get_constraint_from_multiplier(self, model, multiplier_var): f"The KKT multiplier: {multiplier_var.name}, does not exist on {model.name}." ) - def get_multiplier_from_constraint(self, model, constraint=None, variable=None): + def get_multiplier_from_constraint(self, model, component): """ - Return the multiplier variable corresponding to a constraint or variable bound. + Return the multiplier for the constraint. Parameters ---------- model: ConcreteModel The model on which the kkt transformation was applied to - constraint: Constraint or Tuple, optional - - A primal Constraint object for simple constraints, OR - - A tuple (constraint, 'lb'|'ub') for ranged constraints - Mutually exclusive with variables. - Variable: Tuple, optional - A tuple (Var, 'lb'|'ub') for variable bounds. - Mutually exclusive with constraint. + component: Constraint or Variable Returns ------- - Var - The KKT multiplier variable corresponding to the constraint or variable bound. + VarData | tuple[VarData | None, VarData | None] + The KKT multiplier(s) corresponding to the component. + For ranged constraints/variables, returns (lb_mult, ub_mult), + where an entry is None if that bound doesn't exist. """ - - if (constraint is None) and (variable is None): - raise ValueError("Must provide 'constraint' or 'variable'.") - if (constraint is not None) and (variable is not None): - raise ValueError( - "Cannot provide both 'constraint' and 'variable'. " "Provide only one." - ) - info = model.private_data() - if constraint is not None: - if isinstance(constraint, tuple): - if len(constraint) != 2: - raise ValueError( - f"constraint tuple must be (Constraint, bound), " - f"got tuple of length {len(constraint)}" - ) - con_obj, bound = constraint - if bound not in ['lb', 'ub']: - raise ValueError(f"Bound must be 'lb' or 'ub', got: '{bound}'") - key = (con_obj, bound) - if key in info.inequality_multiplier_from_con: - return info.inequality_multiplier_from_con[key] - raise ValueError( - f"Ranged constraint '{con_obj.name}' with bound='{bound}' " - f"does not exist on {model.name}." - ) - - # simple constraints are much easier to deal with + if isinstance(component, ConstraintData): + con = component + if con in info.equality_multiplier_from_con: + return info.equality_multiplier_from_con[con] + elif con in info.inequality_multiplier_from_con: + return info.inequality_multiplier_from_con[con] + elif con in info.ranged_constraints: + lb_mult = info.inequality_multiplier_from_con.get((con, 'lb')) + ub_mult = info.inequality_multiplier_from_con.get((con, 'ub')) + return (lb_mult, ub_mult) else: - if constraint in info.equality_multiplier_from_con: - return info.equality_multiplier_from_con[constraint] - if constraint in info.inequality_multiplier_from_con: - return info.inequality_multiplier_from_con[constraint] - # may be a ranged constraint - is_ranged = constraint in info.ranged_constraints - if is_ranged: - raise ValueError( - f"Constraint '{constraint.name}' is a ranged constraint. " - "Provide as tuple: constraint=(constraint_obj, 'lb'|'ub')." - ) - raise ValueError( - f"Constraint '{constraint.name}' does not exist on {model.name}." - ) - - # we need to deal with the case that the user wants multipliers associated with variable bounds - if variable is not None: - # variable bounds must be provided as tuple - if not isinstance(variable, tuple): raise ValueError( - "variable must be a tuple (Var, 'lb'|'ub'), " - f"got: {type(variable).__name__}" + f"Constraint '{con.name}' does not exist on {model.name}." ) - if len(variable) != 2: + elif isinstance(component, VarData): + var = component + lb_mult = info.inequality_multiplier_from_con.get((var, 'lb')) + ub_mult = info.inequality_multiplier_from_con.get((var, 'ub')) + if lb_mult is None and ub_mult is None: raise ValueError( - f"variable tuple must be (Var, bound), " - f"got tuple of length {len(variable)}" + f"No multipliers exist for variable '{var.name}' on {model.name}." ) - var_obj, bound = variable - if bound not in ['lb', 'ub']: - raise ValueError(f"Bound must be 'lb' or 'ub', got: '{bound}'") - key = (var_obj, bound) - if key in info.inequality_multiplier_from_con: - return info.inequality_multiplier_from_con[key] + return (lb_mult, ub_mult) + else: raise ValueError( - f"Variable bound {var_obj.name} (bound='{bound}') " - f"does not exist on {model.name}. " - f"The variable may not have a {bound} bound defined." + f"Component '{component.name}' does not exist on {model.name}." ) diff --git a/pyomo/core/tests/unit/test_kkt.py b/pyomo/core/tests/unit/test_kkt.py index 0025f6c6c2c..80f44c78654 100644 --- a/pyomo/core/tests/unit/test_kkt.py +++ b/pyomo/core/tests/unit/test_kkt.py @@ -454,61 +454,16 @@ def test_get_multiplier_from_constraint_error(self): kkt = TransformationFactory('core.kkt') kkt.apply_to(m) - with self.assertRaisesRegex( - ValueError, "Must provide 'constraint' or 'variable'." - ): - kkt.get_multiplier_from_constraint(m, constraint=None, variable=None) - - with self.assertRaisesRegex( - ValueError, - "Cannot provide both 'constraint' and 'variable'. " "Provide only one", - ): - kkt.get_multiplier_from_constraint(m, constraint=m.c1, variable=(m.x, "ub")) - - with self.assertRaisesRegex( - ValueError, - r"constraint tuple must be \(Constraint, bound\), " "got tuple of length 1", - ): - kkt.get_multiplier_from_constraint(m, constraint=(m.c2,)) - - with self.assertRaisesRegex( - ValueError, "Bound must be 'lb' or 'ub', got: 'no bound'" - ): - kkt.get_multiplier_from_constraint(m, constraint=(m.c2, 'no bound')) - - with self.assertRaisesRegex( - ValueError, - "Ranged constraint 'c1' with bound='ub' " "does not exist on model.", - ): - kkt.get_multiplier_from_constraint(m, constraint=(m.c1, 'ub')) - - with self.assertRaisesRegex( - ValueError, - "Constraint 'c2' is a ranged constraint. " - r"Provide as tuple: constraint=\(constraint_obj, 'lb'|'ub'\).", - ): - kkt.get_multiplier_from_constraint(m, constraint=m.c2) - - with self.assertRaisesRegex( - ValueError, r"variable must be a tuple \(Var, 'lb'|'ub'\), " f"got: x" - ): - kkt.get_multiplier_from_constraint(m, variable=m.x) - - with self.assertRaisesRegex( - ValueError, - r"variable tuple must be \(Var, bound\), " "got tuple of length 1", - ): - kkt.get_multiplier_from_constraint(m, variable=(m.x,)) + m2 = ConcreteModel() + m2.z = Var(bounds=(1, 10)) + m2.new_con = Constraint(expr=m.x <= 5) with self.assertRaisesRegex( - ValueError, f"Bound must be 'lb' or 'ub', got: 'no bound'" + ValueError, "Constraint 'new_con' does not exist on model." ): - kkt.get_multiplier_from_constraint(m, variable=(m.x, 'no bound')) + kkt.get_multiplier_from_constraint(m, component=m2.new_con) with self.assertRaisesRegex( - ValueError, - r"Variable bound y \(bound='ub'\) " - "does not exist on model. " - "The variable may not have a ub bound defined.", + ValueError, "No multipliers exist for variable 'z' on model." ): - kkt.get_multiplier_from_constraint(m, variable=(m.y, 'ub')) + kkt.get_multiplier_from_constraint(m, component=m2.z) From 10c7e10ff95f88c424f6bb75f127936c1fe974ac Mon Sep 17 00:00:00 2001 From: Chris Laliwala Date: Mon, 23 Mar 2026 23:56:47 -0400 Subject: [PATCH 21/35] refactored code to be more efficient. --- pyomo/core/plugins/transform/kkt.py | 373 +++++++++++----------------- 1 file changed, 151 insertions(+), 222 deletions(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index badd191d0ab..4a36fc15abf 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -23,6 +23,7 @@ Set, TransformationFactory, Var, + VarList, maximize, minimize, ) @@ -32,35 +33,15 @@ from pyomo.mpec import ComplementarityList, complements from pyomo.util.vars_from_expressions import get_vars_from_components from pyomo.util.config_domains import ComponentDataSet +from pyomo.core.expr.visitor import identify_variables class _KKTReformulationData(AutoSlots.Mixin): - __slots__ = ( - "equality_multiplier_from_con", - "equality_con_from_multiplier", - "inequality_multiplier_from_con", - "inequality_con_from_multiplier", - "var_bound_multiplier_index_to_con", - "equality_multiplier_index_to_con", - "inequality_multiplier_index_to_con", - "equality_con_to_expr", - "inequality_con_to_expr", - "ranged_constraints", - ) + __slots__ = ("obj_dual_map", "dual_obj_map") def __init__(self): - self.equality_multiplier_from_con = ComponentMap() - self.equality_con_from_multiplier = ComponentMap() - self.inequality_multiplier_from_con = ComponentMap() - self.inequality_con_from_multiplier = ComponentMap() - - self.var_bound_multiplier_index_to_con = {} - self.equality_multiplier_index_to_con = {} - self.inequality_multiplier_index_to_con = {} - self.equality_con_to_expr = ComponentMap() - self.inequality_con_to_expr = ComponentMap() - - self.ranged_constraints = ComponentSet() + self.obj_dual_map = ComponentMap() + self.dual_obj_map = ComponentMap() Block.register_private_data_initializer(_KKTReformulationData) @@ -108,34 +89,21 @@ def apply_to(self, model, **kwds): # we should check that all vars the user fixed are included # in parametrize_wrt params = config.parametrize_wrt - vars_in_cons = ComponentSet( - get_vars_from_components(model, Constraint, active=True, descend_into=True) - ) - vars_in_obj = ComponentSet( - get_vars_from_components(model, Objective, active=True, descend_into=True) - ) - vars_in_model = vars_in_cons | vars_in_obj - fixed_vars_in_model = ComponentSet(v for v in vars_in_model if v.is_fixed()) - missing = fixed_vars_in_model - params - if missing: - raise ValueError("All fixed variables must be included in parametrize_wrt.") - - # we should also check that all vars the user passes in parametrize_wrt - # exist on an active constraint or objective within the model - unknown = params - vars_in_model - if unknown: - raise ValueError( - "A variable passed in parametrize_wrt does not exist on an " - "active constraint or objective within the model." - ) kkt_block = Block(concrete=True) kkt_block.parametrize_wrt = params - self._reformulate(model, kkt_block) + self._reformulate(model, kkt_block, params) model.add_component(config.kkt_block_name, kkt_block) return model - def _reformulate(self, model, kkt_block): + def _reformulate(self, model, kkt_block, params): + # initialize lagrangean + info = model.private_data() + lagrangean = 0 + var_set = ComponentSet() + fixed_var_set = ComponentSet() + + # collect the active Objectives active_objs = list( model.component_data_objects(Objective, active=True, descend_into=True) ) @@ -143,163 +111,146 @@ def _reformulate(self, model, kkt_block): raise ValueError( f"model must have only one active objective; found {len(active_objs)}" ) + # collect vars from active objective + obj = active_objs[0] + for v in identify_variables(obj.expr, include_fixed=True): + if v.is_fixed(): + fixed_var_set.add(v) + else: + var_set.add(v) + # add objective to lagrangean + lagrangean += obj.sense * obj.expr + + # list of equality multipliers + kkt_block.gamma = VarList() + # list of inequality multipliers + kkt_block.alpha1 = VarList(domain=NonNegativeReals) + kkt_block.alpha2 = VarList(domain=NonNegativeReals) + # define inequality complements + kkt_block.complements = ComplementarityList() - self._construct_lagrangean(model, kkt_block) - self._enforce_stationarity_conditions(kkt_block) - self._enforce_complementarity_conditions(model, kkt_block) - - active_objs[0].deactivate() - kkt_block.dummy_obj = Objective(expr=1) - - return model - - def _construct_lagrangean(self, model, kkt_block): - # we need to loop through the model and store the - # equality and inequality constraints - info = model.private_data() - equality_cons = [] - inequality_cons = [] for con in model.component_data_objects( Constraint, descend_into=True, active=True ): lower, body, upper = con.to_bounded_expression() if con.equality: - equality_cons.append(con) - info.equality_con_to_expr[con] = upper - body + # create multiplier + gamma_i = kkt_block.gamma.add() + # add expression to lagrangean + lagrangean += (upper - body) * gamma_i + # create mappings + info.obj_dual_map[con] = gamma_i + info.dual_obj_map[gamma_i] = con + # collect variables in constraint + for v in identify_variables(body, include_fixed=True): + if v.is_fixed(): + fixed_var_set.add(v) + else: + var_set.add(v) + for v in identify_variables(upper, include_fixed=True): + if v.is_fixed(): + fixed_var_set.add(v) + else: + var_set.add(v) + else: + alpha_l = None if lower is not None: - inequality_cons.append((con, "lb")) - info.inequality_con_to_expr.setdefault(con, {})["lb"] = lower - body + # create multiplier + alpha_l = kkt_block.alpha1.add() + # add expression to lagrangean + con_expr = lower - body + lagrangean += con_expr * alpha_l + # create complement + kkt_block.complements.add(complements(alpha_l >= 0, con_expr <= 0)) + # create dual -> con mapping + info.dual_obj_map[alpha_l] = con + # collect variables in constraint + for v in identify_variables(lower, include_fixed=True): + if v.is_fixed(): + fixed_var_set.add(v) + else: + var_set.add(v) + + alpha_u = None if upper is not None: - inequality_cons.append((con, "ub")) - info.inequality_con_to_expr.setdefault(con, {})["ub"] = body - upper - - # lower is not None and upper is not None -> ranged constraint - if lower is not None: - # we want to keep track of the ranged constraints because the mapping between - # multipliers and ranged constraints will be a tuple (to indicate bound as well) - # instead of simply the model object - info.ranged_constraints.add(con) - - kkt_block.equality_cons_list = list(equality_cons) - kkt_block.gamma_set = RangeSet(0, len(equality_cons) - 1) - kkt_block.gamma = Var(kkt_block.gamma_set, domain=Reals) - info.equality_multiplier_index_to_con = dict( - enumerate(kkt_block.equality_cons_list) - ) - kkt_block.inequality_cons_list = list(inequality_cons) - kkt_block.alpha_con_set = RangeSet(0, len(inequality_cons) - 1) - kkt_block.alpha_con = Var(kkt_block.alpha_con_set, domain=NonNegativeReals) - info.inequality_multiplier_index_to_con = dict( - enumerate(kkt_block.inequality_cons_list) - ) + # create multiplier + alpha_u = kkt_block.alpha2.add() + # add expression to lagrangean + con_expr = body - upper + lagrangean += con_expr * alpha_u + # create complement + kkt_block.complements.add(complements(alpha_u >= 0, con_expr <= 0)) + # create dual -> con mapping + info.dual_obj_map[alpha_u] = con + # collect variables in constraint + for v in identify_variables(upper, include_fixed=True): + if v.is_fixed(): + fixed_var_set.add(v) + else: + var_set.add(v) + + # create con -> dual mapping. Will return None if a bound doesn't exist. + info.obj_dual_map[con] = (alpha_l, alpha_u) + + # do error checking on parametrize_wrt + missing = fixed_var_set - params + if missing: + raise ValueError("All fixed variables must be included in parametrize_wrt.") - # we also need to consider inequality constraints - # formed by the user specifying variable bounds - var_bound_sides = [] - vars_in_cons = ComponentSet( - get_vars_from_components(model, Constraint, active=True, descend_into=True) - ) - vars_in_obj = ComponentSet( - get_vars_from_components(model, Objective, active=True, descend_into=True) - ) - kkt_block.var_set = vars_in_cons | vars_in_obj - kkt_block.var_set = kkt_block.var_set - kkt_block.parametrize_wrt - for var in kkt_block.var_set: + all_vars_set = fixed_var_set | var_set + if not (params <= all_vars_set): + raise ValueError( + "A variable passed in parametrize_wrt does not exist on an " + "active constraint or objective within the model." + ) + + # loop through variables, add terms to Lagrangean and add mappings + var_set = var_set - params + for var in var_set: + alpha_l = None if var.has_lb(): - var_bound_sides.append((var, "lb")) + # create multiplier + alpha_l = kkt_block.alpha1.add() + # add expression to lagrangean + con_expr = var.lb - var + lagrangean += con_expr * alpha_l + # create complement + kkt_block.complements.add(complements(alpha_l >= 0, con_expr <= 0)) + # create dual -> con mapping + info.dual_obj_map[alpha_l] = var + + alpha_u = None if var.has_ub(): - var_bound_sides.append((var, "ub")) - kkt_block.var_bound_set = RangeSet(0, len(var_bound_sides) - 1) - kkt_block.alpha_var_bound = Var( - kkt_block.var_bound_set, domain=NonNegativeReals - ) - - info.var_bound_multiplier_index_to_con = dict(enumerate(var_bound_sides)) - - # indexing the inequality constraint expressions will help - # with constructing the lagrangean later - def _var_bound_expr_rule(kkt, i): - var, side = info.var_bound_multiplier_index_to_con[i] - return (var.lb - var) if side == "lb" else (var - var.ub) - - kkt_block.var_bound_expr = Expression( - kkt_block.var_bound_set, rule=_var_bound_expr_rule - ) - - # we will construct the lagrangean by first adding the objective, - # and then looping through and adding the product of each constraint and - # the corresponding multiplier - obj = list( - model.component_data_objects(Objective, active=True, descend_into=True) - ) - # Note: maximize is -1 and minimize is +1 - lagrangean = obj[0].sense * obj[0].expr - - for index, con in enumerate(kkt_block.equality_cons_list): - lagrangean += info.equality_con_to_expr[con] * kkt_block.gamma[index] - info.equality_con_from_multiplier[kkt_block.gamma[index]] = con - info.equality_multiplier_from_con[con] = kkt_block.gamma[index] - - for index, (con, bound) in enumerate(kkt_block.inequality_cons_list): - lagrangean += ( - info.inequality_con_to_expr[con][bound] * kkt_block.alpha_con[index] - ) - # mappings for ranged constraints will consider bounds as well - if con in info.ranged_constraints: - info.inequality_con_from_multiplier[kkt_block.alpha_con[index]] = ( - con, - bound, - ) - info.inequality_multiplier_from_con[(con, bound)] = kkt_block.alpha_con[ - index - ] - else: - info.inequality_con_from_multiplier[kkt_block.alpha_con[index]] = con - info.inequality_multiplier_from_con[con] = kkt_block.alpha_con[index] - - for i in kkt_block.var_bound_set: - lagrangean += kkt_block.var_bound_expr[i] * kkt_block.alpha_var_bound[i] - # mappings for ranged constraints built from variable bounds - var, bound = info.var_bound_multiplier_index_to_con[i] - info.inequality_con_from_multiplier[kkt_block.alpha_var_bound[i]] = ( - var, - bound, - ) - info.inequality_multiplier_from_con[(var, bound)] = ( - kkt_block.alpha_var_bound[i] - ) + # create multiplier + alpha_u = kkt_block.alpha2.add() + # add expression to lagrangean + con_expr = var - var.ub + lagrangean += con_expr * alpha_u + # create complement + kkt_block.complements.add(complements(alpha_u >= 0, con_expr <= 0)) + # create dual -> con mapping + info.dual_obj_map[alpha_u] = var + + # create var -> dual mapping. Will return None if a var bound doesn't exist. + info.obj_dual_map[var] = (alpha_l, alpha_u) kkt_block.lagrangean = Expression(expr=lagrangean) - def _enforce_stationarity_conditions(self, kkt_block): + # enforce stationarity condtiions deriv_lagrangean = reverse_sd(kkt_block.lagrangean.expr) kkt_block.stationarity_conditions = ConstraintList() - for var in kkt_block.var_set: + for var in var_set: kkt_block.stationarity_conditions.add(deriv_lagrangean[var] == 0) - def _enforce_complementarity_conditions(self, model, kkt_block): - info = model.private_data() - kkt_block.complements = ComplementarityList() - for index, (con, bound) in enumerate(kkt_block.inequality_cons_list): - kkt_block.complements.add( - complements( - kkt_block.alpha_con[index] >= 0, - info.inequality_con_to_expr[con][bound] <= 0, - ) - ) - # we also need to consider the inequality constraints - # formed from the user specifying the variable bounds - for i in kkt_block.var_bound_set: - kkt_block.complements.add( - complements( - kkt_block.alpha_var_bound[i] >= 0, kkt_block.var_bound_expr[i] <= 0 - ) - ) + active_objs[0].deactivate() + kkt_block.dummy_obj = Objective(expr=1) - def get_constraint_from_multiplier(self, model, multiplier_var): + def get_object_from_multiplier(self, model, multiplier_var): """ - Return the constraint or variable bound corresponding to a KKT multiplier variable. + Return the constraint corresponding to a KKT multiplier variable. If the + multiplier corresponds to an inequality formed by a variable bound, the variable + is returned. Parameters ---------- @@ -310,26 +261,24 @@ def get_constraint_from_multiplier(self, model, multiplier_var): Returns ------- - Constraint or Tuple - - Constraint object for simple constraints - - (Constraint, bound) tuple for ranged constraints - - (Var, bound) tuple for variable bounds + Object + - Constraint object + - Variable """ info = model.private_data() - if multiplier_var in info.equality_con_from_multiplier: - return info.equality_con_from_multiplier[multiplier_var] - if multiplier_var in info.inequality_con_from_multiplier: - # if this multiplier var maps to a ranged constraint, we will return a tuple - # so that we can indicate which bound the multiplier var maps to - return info.inequality_con_from_multiplier[multiplier_var] - raise ValueError( - f"The KKT multiplier: {multiplier_var.name}, does not exist on {model.name}." - ) + if multiplier_var in info.dual_obj_map: + return info.dual_obj_map[multiplier_var] + else: + raise ValueError( + f"The KKT multiplier: {multiplier_var.name}, does not exist on {model.name}." + ) - def get_multiplier_from_constraint(self, model, component): + def get_multiplier_from_object(self, model, component): """ - Return the multiplier for the constraint. + Return the multiplier for the object. If the object is a normal constraint, a single + multiplier is returned. If the object is a ranged constraint or a variable, a tuple + containing the lower and upper bound multipliers is returned. Parameters ---------- @@ -342,33 +291,13 @@ def get_multiplier_from_constraint(self, model, component): VarData | tuple[VarData | None, VarData | None] The KKT multiplier(s) corresponding to the component. For ranged constraints/variables, returns (lb_mult, ub_mult), - where an entry is None if that bound doesn't exist. + where an entry is 'None' if that bound doesn't exist. """ + info = model.private_data() - if isinstance(component, ConstraintData): - con = component - if con in info.equality_multiplier_from_con: - return info.equality_multiplier_from_con[con] - elif con in info.inequality_multiplier_from_con: - return info.inequality_multiplier_from_con[con] - elif con in info.ranged_constraints: - lb_mult = info.inequality_multiplier_from_con.get((con, 'lb')) - ub_mult = info.inequality_multiplier_from_con.get((con, 'ub')) - return (lb_mult, ub_mult) - else: - raise ValueError( - f"Constraint '{con.name}' does not exist on {model.name}." - ) - elif isinstance(component, VarData): - var = component - lb_mult = info.inequality_multiplier_from_con.get((var, 'lb')) - ub_mult = info.inequality_multiplier_from_con.get((var, 'ub')) - if lb_mult is None and ub_mult is None: - raise ValueError( - f"No multipliers exist for variable '{var.name}' on {model.name}." - ) - return (lb_mult, ub_mult) + if component in info.obj_dual_map: + return info.obj_dual_map[component] else: raise ValueError( - f"Component '{component.name}' does not exist on {model.name}." + f"The component '{component.name}' either does not exist on '{model.name}', or is not associated with a multiplier." ) From 7583fd45c9312296451178986373650db025b892 Mon Sep 17 00:00:00 2001 From: Chris Laliwala Date: Mon, 23 Mar 2026 23:57:00 -0400 Subject: [PATCH 22/35] updated tests to work for refactored code. --- pyomo/core/tests/unit/test_kkt.py | 270 +++++++++++++++++++----------- 1 file changed, 168 insertions(+), 102 deletions(-) diff --git a/pyomo/core/tests/unit/test_kkt.py b/pyomo/core/tests/unit/test_kkt.py index 80f44c78654..9bcc8a2a77d 100644 --- a/pyomo/core/tests/unit/test_kkt.py +++ b/pyomo/core/tests/unit/test_kkt.py @@ -43,7 +43,6 @@ ) -@unittest.skipUnless(scipy_available, "Scipy not available") class TestKKT(unittest.TestCase): def check_primal_kkt_transformation_solns(self, m, m_reform): kkt = TransformationFactory('core.kkt') @@ -60,20 +59,35 @@ def check_primal_kkt_transformation_solns(self, m, m_reform): results.solver.termination_condition, TerminationCondition.optimal ) - for cons in [ - (m.c1, m_reform.c1), - (m.c2, m_reform.c2), - (m.c3, m_reform.c3), - (m.c4, m_reform.c4), - ]: - primal_con, kkt_reform_con = cons - self.assertAlmostEqual( - value( - abs(kkt.get_multiplier_from_constraint(m_reform, kkt_reform_con)) - ), - value(abs(m.dual[primal_con])), - delta=1e-6, - ) + # equality constraint + self.assertAlmostEqual( + value(abs(kkt.get_multiplier_from_object(m_reform, m_reform.c2))), + value(abs(m.dual[m.c2])), + delta=1e-6, + ) + + # inequality constraints + lower_bound_mult, upper_bound_mult = kkt.get_multiplier_from_object( + m_reform, m_reform.c1 + ) + self.assertIsNone(lower_bound_mult) + self.assertAlmostEqual( + value(abs(upper_bound_mult)), value(abs(m.dual[m.c1])), delta=1e-6 + ) + lower_bound_mult, upper_bound_mult = kkt.get_multiplier_from_object( + m_reform, m_reform.c3 + ) + self.assertAlmostEqual( + value(abs(lower_bound_mult)), value(abs(m.dual[m.c3])), delta=1e-6 + ) + self.assertIsNone(upper_bound_mult) + lower_bound_mult, upper_bound_mult = kkt.get_multiplier_from_object( + m_reform, m_reform.c4 + ) + self.assertIsNone(lower_bound_mult) + self.assertAlmostEqual( + value(abs(upper_bound_mult)), value(abs(m.dual[m.c4])), delta=1e-6 + ) for v in [(m.x, m_reform.x), (m.y, m_reform.y)]: primal_var, kkt_reform_var = v @@ -108,50 +122,73 @@ def test_kkt(self): m.obj = Objective( expr=(m.x - 3) ** 2 + (m.y - 2) ** 2 + (m.z - 1) ** 2, sense=minimize ) + # upper bounded constraint m.c1 = Constraint(expr=m.x**2 + m.y**2 <= 9) + # equality constraint m.c2 = Constraint(expr=m.x + m.y + m.z == 5) + # lower bounded constraint m.c3 = Constraint(expr=m.z >= 1) + # upper bounded constraint m.c4 = Constraint(expr=2 * m.x - m.y <= 4) kkt = TransformationFactory('core.kkt') kkt.apply_to(m) - gamma0 = kkt.get_multiplier_from_constraint(m, m.c2) - alpha_con0 = kkt.get_multiplier_from_constraint(m, m.c1) - alpha_con1 = kkt.get_multiplier_from_constraint(m, m.c3) - alpha_con2 = kkt.get_multiplier_from_constraint(m, m.c4) + # equality constraint + gamma0 = kkt.get_multiplier_from_object(m, m.c2) - self.assertIs(kkt.get_constraint_from_multiplier(m, gamma0), m.c2) - self.assertIs(kkt.get_constraint_from_multiplier(m, alpha_con0), m.c1) - self.assertIs(kkt.get_constraint_from_multiplier(m, alpha_con1), m.c3) - self.assertIs(kkt.get_constraint_from_multiplier(m, alpha_con2), m.c4) + self.assertIs(kkt.get_object_from_multiplier(m, gamma0), m.c2) - c2 = kkt.get_constraint_from_multiplier(m, gamma0) - c1 = kkt.get_constraint_from_multiplier(m, alpha_con0) - c3 = kkt.get_constraint_from_multiplier(m, alpha_con1) - c4 = kkt.get_constraint_from_multiplier(m, alpha_con2) + # upper bounded constraint + alpha_con0_mults = kkt.get_multiplier_from_object(m, m.c1) + alpha_con0_lower_mult = alpha_con0_mults[0] # None + alpha_con0_upper_mult = alpha_con0_mults[1] - self.assertIs(kkt.get_multiplier_from_constraint(m, c2), gamma0) - self.assertIs(kkt.get_multiplier_from_constraint(m, c1), alpha_con0) - self.assertIs(kkt.get_multiplier_from_constraint(m, c3), alpha_con1) - self.assertIs(kkt.get_multiplier_from_constraint(m, c4), alpha_con2) + self.assertIsNone(alpha_con0_lower_mult) + self.assertIs(kkt.get_object_from_multiplier(m, alpha_con0_upper_mult), m.c1) + + # lower bounded constraint + alpha_con1_mults = kkt.get_multiplier_from_object(m, m.c3) + alpha_con1_lower_mult = alpha_con1_mults[0] + alpha_con1_upper_mult = alpha_con1_mults[1] # None + + self.assertIs(kkt.get_object_from_multiplier(m, alpha_con1_lower_mult), m.c3) + self.assertIsNone(alpha_con1_upper_mult) + + # upper bounded constraint + alpha_con2_mults = kkt.get_multiplier_from_object(m, m.c4) + alpha_con2_lower_mult = alpha_con2_mults[0] # None + alpha_con2_upper_mult = alpha_con2_mults[1] + + self.assertIsNone(alpha_con2_lower_mult) + self.assertIs(kkt.get_object_from_multiplier(m, alpha_con2_upper_mult), m.c4) + + c2 = kkt.get_object_from_multiplier(m, gamma0) + c1 = kkt.get_object_from_multiplier(m, alpha_con0_upper_mult) + c3 = kkt.get_object_from_multiplier(m, alpha_con1_lower_mult) + c4 = kkt.get_object_from_multiplier(m, alpha_con2_upper_mult) + + self.assertIs(kkt.get_multiplier_from_object(m, c2), gamma0) + self.assertIs(kkt.get_multiplier_from_object(m, c1), alpha_con0_mults) + self.assertIs(kkt.get_multiplier_from_object(m, c3), alpha_con1_mults) + self.assertIs(kkt.get_multiplier_from_object(m, c4), alpha_con2_mults) self.assertIs(gamma0.ctype, Var) self.assertEqual(gamma0.domain, Reals) self.assertIsNone(gamma0.ub) self.assertIsNone(gamma0.lb) - self.assertIs(alpha_con0.ctype, Var) - self.assertEqual(alpha_con0.domain, NonNegativeReals) - self.assertIsNone(alpha_con0.ub) - self.assertEqual(alpha_con1.lb, 0) - self.assertIs(alpha_con1.ctype, Var) - self.assertEqual(alpha_con1.domain, NonNegativeReals) - self.assertIsNone(alpha_con1.ub) - self.assertEqual(alpha_con2.lb, 0) - self.assertIs(alpha_con2.ctype, Var) - self.assertEqual(alpha_con2.domain, NonNegativeReals) - self.assertIsNone(alpha_con2.ub) - self.assertEqual(alpha_con2.lb, 0) + + self.assertIs(alpha_con0_upper_mult.ctype, Var) + self.assertEqual(alpha_con0_upper_mult.domain, NonNegativeReals) + self.assertIsNone(alpha_con0_upper_mult.ub) + + self.assertIs(alpha_con1_lower_mult.ctype, Var) + self.assertEqual(alpha_con1_lower_mult.domain, NonNegativeReals) + self.assertIsNone(alpha_con1_lower_mult.ub) + + self.assertIs(alpha_con2_upper_mult.ctype, Var) + self.assertEqual(alpha_con2_upper_mult.domain, NonNegativeReals) + self.assertIsNone(alpha_con2_upper_mult.ub) self.assertIs(c1.ctype, Constraint) self.assertIs(c2.ctype, Constraint) @@ -165,46 +202,54 @@ def test_kkt(self): (m.x - 3) ** 2 + (m.y - 2) ** 2 + (m.z - 1) ** 2 - + (5.0 - (m.x + m.y + m.z)) * gamma0 - + (m.x**2 + m.y**2 - 9.0) * alpha_con0 - + (1.0 - m.z) * alpha_con1 - + (2 * m.x - m.y - 4.0) * alpha_con2, + + (m.x**2 + m.y**2 - 9) * alpha_con0_upper_mult + + (5 - (m.x + m.y + m.z)) * gamma0 + + (1 - m.z) * alpha_con1_lower_mult + + (2 * m.x - m.y - 4) * alpha_con2_upper_mult, ) # test stationarity conditions assertExpressionsStructurallyEqual( self, m.kkt.stationarity_conditions[1].expr, - 2 * alpha_con2 + 2 * alpha_con0 * m.x - gamma0 + 2 * (m.x - 3) == 0, + 2 * alpha_con2_upper_mult + - gamma0 + + 2 * alpha_con0_upper_mult * m.x + + 2 * (m.x - 3) + == 0, ) assertExpressionsStructurallyEqual( self, m.kkt.stationarity_conditions[2].expr, - -alpha_con2 + 2 * alpha_con0 * m.y - gamma0 + 2 * (m.y - 2) == 0, + -alpha_con2_upper_mult + - gamma0 + + 2 * alpha_con0_upper_mult * m.y + + 2 * (m.y - 2) + == 0, ) assertExpressionsStructurallyEqual( self, m.kkt.stationarity_conditions[3].expr, - -alpha_con1 - gamma0 + 2 * (m.z - 1) == 0, + -alpha_con1_lower_mult - gamma0 + 2 * (m.z - 1) == 0, ) # test complementarity constraints assertExpressionsStructurallyEqual( - self, m.kkt.complements[1]._args[0], 0 <= alpha_con0 + self, m.kkt.complements[1]._args[0], 0 <= alpha_con0_upper_mult ) assertExpressionsStructurallyEqual( self, m.kkt.complements[1]._args[1], m.x**2 + m.y**2 - 9.0 <= 0 ) assertExpressionsStructurallyEqual( - self, m.kkt.complements[2]._args[0], 0 <= alpha_con1 + self, m.kkt.complements[2]._args[0], 0 <= alpha_con1_lower_mult ) assertExpressionsStructurallyEqual( self, m.kkt.complements[2]._args[1], 1.0 - m.z <= 0 ) assertExpressionsStructurallyEqual( - self, m.kkt.complements[3]._args[0], 0 <= alpha_con2 + self, m.kkt.complements[3]._args[0], 0 <= alpha_con2_upper_mult ) assertExpressionsStructurallyEqual( self, m.kkt.complements[3]._args[1], 2 * m.x - m.y - 4.0 <= 0 @@ -247,42 +292,61 @@ def test_parametrized_kkt(self): kkt.apply_to(m, parametrize_wrt=[m.outer1, m.outer2]) TransformationFactory("mpec.simple_nonlinear").apply_to(m) - gamma0 = kkt.get_multiplier_from_constraint(m, m.c2) - alpha_con0 = kkt.get_multiplier_from_constraint(m, m.c1) - alpha_con1 = kkt.get_multiplier_from_constraint(m, m.c3) - alpha_con2 = kkt.get_multiplier_from_constraint(m, m.c4) + # equality constraint + gamma0 = kkt.get_multiplier_from_object(m, m.c2) + + self.assertIs(kkt.get_object_from_multiplier(m, gamma0), m.c2) + + # upper bounded constraint + alpha_con0_mults = kkt.get_multiplier_from_object(m, m.c1) + alpha_con0_lower_mult = alpha_con0_mults[0] # None + alpha_con0_upper_mult = alpha_con0_mults[1] + + self.assertIsNone(alpha_con0_lower_mult) + self.assertIs(kkt.get_object_from_multiplier(m, alpha_con0_upper_mult), m.c1) - self.assertIs(kkt.get_constraint_from_multiplier(m, gamma0), m.c2) - self.assertIs(kkt.get_constraint_from_multiplier(m, alpha_con0), m.c1) - self.assertIs(kkt.get_constraint_from_multiplier(m, alpha_con1), m.c3) - self.assertIs(kkt.get_constraint_from_multiplier(m, alpha_con2), m.c4) + # lower bounded constraint + alpha_con1_mults = kkt.get_multiplier_from_object(m, m.c3) + alpha_con1_lower_mult = alpha_con1_mults[0] + alpha_con1_upper_mult = alpha_con1_mults[1] # None - c2 = kkt.get_constraint_from_multiplier(m, gamma0) - c1 = kkt.get_constraint_from_multiplier(m, alpha_con0) - c3 = kkt.get_constraint_from_multiplier(m, alpha_con1) - c4 = kkt.get_constraint_from_multiplier(m, alpha_con2) + self.assertIs(kkt.get_object_from_multiplier(m, alpha_con1_lower_mult), m.c3) + self.assertIsNone(alpha_con1_upper_mult) - self.assertIs(kkt.get_multiplier_from_constraint(m, c2), gamma0) - self.assertIs(kkt.get_multiplier_from_constraint(m, c1), alpha_con0) - self.assertIs(kkt.get_multiplier_from_constraint(m, c3), alpha_con1) - self.assertIs(kkt.get_multiplier_from_constraint(m, c4), alpha_con2) + # upper bounded constraint + alpha_con2_mults = kkt.get_multiplier_from_object(m, m.c4) + alpha_con2_lower_mult = alpha_con2_mults[0] # None + alpha_con2_upper_mult = alpha_con2_mults[1] + + self.assertIsNone(alpha_con2_lower_mult) + self.assertIs(kkt.get_object_from_multiplier(m, alpha_con2_upper_mult), m.c4) + + c2 = kkt.get_object_from_multiplier(m, gamma0) + c1 = kkt.get_object_from_multiplier(m, alpha_con0_upper_mult) + c3 = kkt.get_object_from_multiplier(m, alpha_con1_lower_mult) + c4 = kkt.get_object_from_multiplier(m, alpha_con2_upper_mult) + + self.assertIs(kkt.get_multiplier_from_object(m, c2), gamma0) + self.assertIs(kkt.get_multiplier_from_object(m, c1), alpha_con0_mults) + self.assertIs(kkt.get_multiplier_from_object(m, c3), alpha_con1_mults) + self.assertIs(kkt.get_multiplier_from_object(m, c4), alpha_con2_mults) self.assertIs(gamma0.ctype, Var) self.assertEqual(gamma0.domain, Reals) self.assertIsNone(gamma0.ub) self.assertIsNone(gamma0.lb) - self.assertIs(alpha_con0.ctype, Var) - self.assertEqual(alpha_con0.domain, NonNegativeReals) - self.assertIsNone(alpha_con0.ub) - self.assertEqual(alpha_con1.lb, 0) - self.assertIs(alpha_con1.ctype, Var) - self.assertEqual(alpha_con1.domain, NonNegativeReals) - self.assertIsNone(alpha_con1.ub) - self.assertEqual(alpha_con2.lb, 0) - self.assertIs(alpha_con2.ctype, Var) - self.assertEqual(alpha_con2.domain, NonNegativeReals) - self.assertIsNone(alpha_con2.ub) - self.assertEqual(alpha_con2.lb, 0) + + self.assertIs(alpha_con0_upper_mult.ctype, Var) + self.assertEqual(alpha_con0_upper_mult.domain, NonNegativeReals) + self.assertIsNone(alpha_con0_upper_mult.ub) + + self.assertIs(alpha_con1_lower_mult.ctype, Var) + self.assertEqual(alpha_con1_lower_mult.domain, NonNegativeReals) + self.assertIsNone(alpha_con1_lower_mult.ub) + + self.assertIs(alpha_con2_upper_mult.ctype, Var) + self.assertEqual(alpha_con2_upper_mult.domain, NonNegativeReals) + self.assertIsNone(alpha_con2_upper_mult.ub) self.assertIs(c1.ctype, Constraint) self.assertIs(c2.ctype, Constraint) @@ -296,44 +360,51 @@ def test_parametrized_kkt(self): (m.x - m.outer1) ** 2 + (m.y - 2) ** 2 + (m.z - m.outer2) ** 2 + + (m.x**2 + m.y**2 - (9 + m.outer1)) * alpha_con0_upper_mult + (-(m.x + m.y + m.z - (5 + m.outer2))) * gamma0 - + (m.x**2 + m.y**2 - (9 + m.outer1)) * alpha_con0 - + (1.0 - m.z) * alpha_con1 - + (2 * m.x - m.y - (4 + 0.5 * m.outer1)) * alpha_con2, + + (1 - m.z) * alpha_con1_lower_mult + + (2 * m.x - m.y - (4 + 0.5 * m.outer1)) * alpha_con2_upper_mult, ) # test stationarity conditions assertExpressionsStructurallyEqual( self, m.kkt.stationarity_conditions[1].expr, - 2 * alpha_con2 + 2 * alpha_con0 * m.x - gamma0 + 2 * (m.x - m.outer1) == 0, + (2 * alpha_con2_upper_mult - gamma0) + + 2 * alpha_con0_upper_mult * m.x + + 2 * (m.x - m.outer1) + == 0, ) assertExpressionsStructurallyEqual( self, m.kkt.stationarity_conditions[2].expr, - -alpha_con2 + 2 * alpha_con0 * m.y - gamma0 + 2 * (m.y - 2) == 0, + -alpha_con2_upper_mult + - gamma0 + + 2 * alpha_con0_upper_mult * m.y + + 2 * (m.y - 2) + == 0, ) assertExpressionsStructurallyEqual( self, m.kkt.stationarity_conditions[3].expr, - -alpha_con1 - gamma0 + 2 * (m.z - m.outer2) == 0, + -alpha_con1_lower_mult - gamma0 + 2 * (m.z - m.outer2) == 0, ) # test complementarity constraints assertExpressionsStructurallyEqual( - self, m.kkt.complements[1]._args[0], 0 <= alpha_con0 + self, m.kkt.complements[1]._args[0], 0 <= alpha_con0_upper_mult ) assertExpressionsStructurallyEqual( self, m.kkt.complements[1]._args[1], m.x**2 + m.y**2 - (9 + m.outer1) <= 0 ) assertExpressionsStructurallyEqual( - self, m.kkt.complements[2]._args[0], 0 <= alpha_con1 + self, m.kkt.complements[2]._args[0], 0 <= alpha_con1_lower_mult ) assertExpressionsStructurallyEqual( - self, m.kkt.complements[2]._args[1], 1.0 - m.z <= 0 + self, m.kkt.complements[2]._args[1], 1 - m.z <= 0 ) assertExpressionsStructurallyEqual( - self, m.kkt.complements[3]._args[0], 0 <= alpha_con2 + self, m.kkt.complements[3]._args[0], 0 <= alpha_con2_upper_mult ) assertExpressionsStructurallyEqual( self, @@ -427,7 +498,7 @@ def test_parametrize_wrt_unknown_error(self): ): kkt.apply_to(m, parametrize_wrt=[m.b1.x1]) - def test_get_constraint_from_multiplier_error(self): + def test_get_object_from_multiplier_error(self): m = ConcreteModel(name="model") m.x = Var(domain=Reals) m.y = Var(domain=Reals) @@ -442,9 +513,9 @@ def test_get_constraint_from_multiplier_error(self): with self.assertRaisesRegex( ValueError, f"The KKT multiplier: {m2.gamma}, does not exist on model." ): - kkt.get_constraint_from_multiplier(m, m2.gamma) + kkt.get_object_from_multiplier(m, m2.gamma) - def test_get_multiplier_from_constraint_error(self): + def test_get_multiplier_from_object_error(self): m = ConcreteModel(name="model") m.x = Var(domain=Reals, bounds=(0, 10)) m.y = Var(domain=Reals) @@ -455,15 +526,10 @@ def test_get_multiplier_from_constraint_error(self): kkt.apply_to(m) m2 = ConcreteModel() - m2.z = Var(bounds=(1, 10)) m2.new_con = Constraint(expr=m.x <= 5) with self.assertRaisesRegex( - ValueError, "Constraint 'new_con' does not exist on model." - ): - kkt.get_multiplier_from_constraint(m, component=m2.new_con) - - with self.assertRaisesRegex( - ValueError, "No multipliers exist for variable 'z' on model." + ValueError, + "The component 'new_con' either does not exist on 'model', or is not associated with a multiplier.", ): - kkt.get_multiplier_from_constraint(m, component=m2.z) + kkt.get_multiplier_from_object(m, component=m2.new_con) From 77e926a317b2ebfe13ecfde240ef653084366c00 Mon Sep 17 00:00:00 2001 From: Chris Laliwala Date: Tue, 24 Mar 2026 00:10:53 -0400 Subject: [PATCH 23/35] fixed collection of variables in constraints --- pyomo/core/plugins/transform/kkt.py | 42 ++++++++--------------------- 1 file changed, 11 insertions(+), 31 deletions(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index 4a36fc15abf..45a5a65f12d 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -18,20 +18,12 @@ Expression, NonNegativeReals, Objective, - RangeSet, - Reals, - Set, TransformationFactory, Var, VarList, - maximize, - minimize, ) -from pyomo.core.base.constraint import ConstraintData -from pyomo.core.base.var import VarData from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd from pyomo.mpec import ComplementarityList, complements -from pyomo.util.vars_from_expressions import get_vars_from_components from pyomo.util.config_domains import ComponentDataSet from pyomo.core.expr.visitor import identify_variables @@ -133,6 +125,17 @@ def _reformulate(self, model, kkt_block, params): Constraint, descend_into=True, active=True ): lower, body, upper = con.to_bounded_expression() + + # collect variables in constraint + for expr in (lower, body, upper): + if expr is None: + continue + for v in identify_variables(expr, include_fixed=True): + if v.is_fixed(): + fixed_var_set.add(v) + else: + var_set.add(v) + if con.equality: # create multiplier gamma_i = kkt_block.gamma.add() @@ -141,17 +144,6 @@ def _reformulate(self, model, kkt_block, params): # create mappings info.obj_dual_map[con] = gamma_i info.dual_obj_map[gamma_i] = con - # collect variables in constraint - for v in identify_variables(body, include_fixed=True): - if v.is_fixed(): - fixed_var_set.add(v) - else: - var_set.add(v) - for v in identify_variables(upper, include_fixed=True): - if v.is_fixed(): - fixed_var_set.add(v) - else: - var_set.add(v) else: alpha_l = None @@ -165,12 +157,6 @@ def _reformulate(self, model, kkt_block, params): kkt_block.complements.add(complements(alpha_l >= 0, con_expr <= 0)) # create dual -> con mapping info.dual_obj_map[alpha_l] = con - # collect variables in constraint - for v in identify_variables(lower, include_fixed=True): - if v.is_fixed(): - fixed_var_set.add(v) - else: - var_set.add(v) alpha_u = None if upper is not None: @@ -183,12 +169,6 @@ def _reformulate(self, model, kkt_block, params): kkt_block.complements.add(complements(alpha_u >= 0, con_expr <= 0)) # create dual -> con mapping info.dual_obj_map[alpha_u] = con - # collect variables in constraint - for v in identify_variables(upper, include_fixed=True): - if v.is_fixed(): - fixed_var_set.add(v) - else: - var_set.add(v) # create con -> dual mapping. Will return None if a bound doesn't exist. info.obj_dual_map[con] = (alpha_l, alpha_u) From 8eec4a279e8419fa1c92a15e56e59e49123a79e3 Mon Sep 17 00:00:00 2001 From: Chris Laliwala Date: Tue, 24 Mar 2026 12:40:48 -0400 Subject: [PATCH 24/35] formatted with black. --- pyomo/core/plugins/transform/kkt.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index 45a5a65f12d..236a9a457af 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -73,10 +73,8 @@ def apply_to(self, model, **kwds): config.set_value(kwds) if hasattr(model, config.kkt_block_name): - raise ValueError( - f"""model already has an attribute with the - specified kkt_block_name: '{config.kkt_block_name}'""" - ) + raise ValueError(f"""model already has an attribute with the + specified kkt_block_name: '{config.kkt_block_name}'""") # we should check that all vars the user fixed are included # in parametrize_wrt From 42f9b49e6eda11a856cf85e347d7979eb056206a Mon Sep 17 00:00:00 2001 From: Chris Laliwala Date: Tue, 24 Mar 2026 12:45:23 -0400 Subject: [PATCH 25/35] fixed typos --- pyomo/core/plugins/transform/kkt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index 236a9a457af..7be2d991af6 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -215,7 +215,7 @@ def _reformulate(self, model, kkt_block, params): kkt_block.lagrangean = Expression(expr=lagrangean) - # enforce stationarity condtiions + # enforce stationarity conditions deriv_lagrangean = reverse_sd(kkt_block.lagrangean.expr) kkt_block.stationarity_conditions = ConstraintList() for var in var_set: From 4a994c0e06f2d1a0a3cba28ca2f6ecac38b1fe54 Mon Sep 17 00:00:00 2001 From: Claliwal <136067410+ChrisLaliwala@users.noreply.github.com> Date: Sat, 28 Mar 2026 14:00:02 -0400 Subject: [PATCH 26/35] Update pyomo/core/plugins/transform/kkt.py Co-authored-by: John Siirola --- pyomo/core/plugins/transform/kkt.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index 7be2d991af6..0ba07e19615 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -103,11 +103,7 @@ def _reformulate(self, model, kkt_block, params): ) # collect vars from active objective obj = active_objs[0] - for v in identify_variables(obj.expr, include_fixed=True): - if v.is_fixed(): - fixed_var_set.add(v) - else: - var_set.add(v) + all_vars_set.update(identify_variables(obj.expr, include_fixed=True)) # add objective to lagrangean lagrangean += obj.sense * obj.expr From d7f91d92d63d5d616ed94bf4e0f069a143dc1044 Mon Sep 17 00:00:00 2001 From: Chris Laliwala Date: Sat, 28 Mar 2026 14:24:21 -0400 Subject: [PATCH 27/35] made variable collection more efficient. --- pyomo/core/plugins/transform/kkt.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index 0ba07e19615..75ef19b69bd 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -90,8 +90,7 @@ def _reformulate(self, model, kkt_block, params): # initialize lagrangean info = model.private_data() lagrangean = 0 - var_set = ComponentSet() - fixed_var_set = ComponentSet() + all_vars_set = ComponentSet() # collect the active Objectives active_objs = list( @@ -124,11 +123,7 @@ def _reformulate(self, model, kkt_block, params): for expr in (lower, body, upper): if expr is None: continue - for v in identify_variables(expr, include_fixed=True): - if v.is_fixed(): - fixed_var_set.add(v) - else: - var_set.add(v) + all_vars_set.update(identify_variables(expr=expr, include_fixed=True)) if con.equality: # create multiplier @@ -167,12 +162,15 @@ def _reformulate(self, model, kkt_block, params): # create con -> dual mapping. Will return None if a bound doesn't exist. info.obj_dual_map[con] = (alpha_l, alpha_u) + fixed_vars = ComponentSet(v for v in all_vars_set if v.is_fixed()) + var_set = ComponentSet(all_vars_set) + var_set -= fixed_vars + # do error checking on parametrize_wrt - missing = fixed_var_set - params + missing = fixed_vars - params if missing: raise ValueError("All fixed variables must be included in parametrize_wrt.") - all_vars_set = fixed_var_set | var_set if not (params <= all_vars_set): raise ValueError( "A variable passed in parametrize_wrt does not exist on an " From deb48d734e0ffe2aca6bd36a72778df674981f55 Mon Sep 17 00:00:00 2001 From: Chris Laliwala Date: Sat, 28 Mar 2026 14:29:48 -0400 Subject: [PATCH 28/35] combined upper and lower inequality multiplier VarList into single VarList --- pyomo/core/plugins/transform/kkt.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index 75ef19b69bd..9e95f2e203b 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -109,8 +109,7 @@ def _reformulate(self, model, kkt_block, params): # list of equality multipliers kkt_block.gamma = VarList() # list of inequality multipliers - kkt_block.alpha1 = VarList(domain=NonNegativeReals) - kkt_block.alpha2 = VarList(domain=NonNegativeReals) + kkt_block.alpha = VarList(domain=NonNegativeReals) # define inequality complements kkt_block.complements = ComplementarityList() @@ -138,7 +137,7 @@ def _reformulate(self, model, kkt_block, params): alpha_l = None if lower is not None: # create multiplier - alpha_l = kkt_block.alpha1.add() + alpha_l = kkt_block.alpha.add() # add expression to lagrangean con_expr = lower - body lagrangean += con_expr * alpha_l @@ -150,7 +149,7 @@ def _reformulate(self, model, kkt_block, params): alpha_u = None if upper is not None: # create multiplier - alpha_u = kkt_block.alpha2.add() + alpha_u = kkt_block.alpha.add() # add expression to lagrangean con_expr = body - upper lagrangean += con_expr * alpha_u @@ -183,7 +182,7 @@ def _reformulate(self, model, kkt_block, params): alpha_l = None if var.has_lb(): # create multiplier - alpha_l = kkt_block.alpha1.add() + alpha_l = kkt_block.alpha.add() # add expression to lagrangean con_expr = var.lb - var lagrangean += con_expr * alpha_l @@ -195,7 +194,7 @@ def _reformulate(self, model, kkt_block, params): alpha_u = None if var.has_ub(): # create multiplier - alpha_u = kkt_block.alpha2.add() + alpha_u = kkt_block.alpha.add() # add expression to lagrangean con_expr = var - var.ub lagrangean += con_expr * alpha_u From 253dede2e889cbe1adc619d02484ca8b67140e22 Mon Sep 17 00:00:00 2001 From: Chris Laliwala Date: Sat, 28 Mar 2026 14:35:42 -0400 Subject: [PATCH 29/35] removed unecessary dummy objective from transformation. updated tests to reflect this. --- pyomo/core/plugins/transform/kkt.py | 8 +++++--- pyomo/core/tests/unit/test_kkt.py | 6 ------ 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index 9e95f2e203b..671e54fd8f5 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -73,8 +73,10 @@ def apply_to(self, model, **kwds): config.set_value(kwds) if hasattr(model, config.kkt_block_name): - raise ValueError(f"""model already has an attribute with the - specified kkt_block_name: '{config.kkt_block_name}'""") + raise ValueError( + f"""model already has an attribute with the + specified kkt_block_name: '{config.kkt_block_name}'""" + ) # we should check that all vars the user fixed are included # in parametrize_wrt @@ -215,7 +217,7 @@ def _reformulate(self, model, kkt_block, params): kkt_block.stationarity_conditions.add(deriv_lagrangean[var] == 0) active_objs[0].deactivate() - kkt_block.dummy_obj = Objective(expr=1) + # kkt_block.dummy_obj = Objective(expr=1) def get_object_from_multiplier(self, model, multiplier_var): """ diff --git a/pyomo/core/tests/unit/test_kkt.py b/pyomo/core/tests/unit/test_kkt.py index 9bcc8a2a77d..ab606fdfa05 100644 --- a/pyomo/core/tests/unit/test_kkt.py +++ b/pyomo/core/tests/unit/test_kkt.py @@ -257,9 +257,6 @@ def test_kkt(self): self.assertFalse(m.obj.active) - self.assertTrue(m.kkt.dummy_obj.active) - self.assertEqual(m.kkt.dummy_obj.expr, 1.0) - def get_bilevel_model(self): m = ConcreteModel(name='bilevel') @@ -414,9 +411,6 @@ def test_parametrized_kkt(self): self.assertFalse(m.obj.active) - self.assertTrue(m.kkt.dummy_obj.active) - self.assertEqual(m.kkt.dummy_obj.expr, 1.0) - def test_solve_parametrized_kkt(self): m = self.get_bilevel_model() From 09e08ebd7737aa5fa49baa095ef12991621aa683 Mon Sep 17 00:00:00 2001 From: Chris Laliwala Date: Sat, 28 Mar 2026 14:51:03 -0400 Subject: [PATCH 30/35] cleaned up code comments, broke up strings that were too long. --- pyomo/core/plugins/transform/kkt.py | 47 +++++++---------------------- 1 file changed, 11 insertions(+), 36 deletions(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index 671e54fd8f5..39346af836d 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -74,8 +74,8 @@ def apply_to(self, model, **kwds): if hasattr(model, config.kkt_block_name): raise ValueError( - f"""model already has an attribute with the - specified kkt_block_name: '{config.kkt_block_name}'""" + "model already has an attribute with the" + f"specified kkt_block_name: '{config.kkt_block_name}'" ) # we should check that all vars the user fixed are included @@ -89,7 +89,7 @@ def apply_to(self, model, **kwds): return model def _reformulate(self, model, kkt_block, params): - # initialize lagrangean + # initialize info = model.private_data() lagrangean = 0 all_vars_set = ComponentSet() @@ -105,7 +105,6 @@ def _reformulate(self, model, kkt_block, params): # collect vars from active objective obj = active_objs[0] all_vars_set.update(identify_variables(obj.expr, include_fixed=True)) - # add objective to lagrangean lagrangean += obj.sense * obj.expr # list of equality multipliers @@ -127,40 +126,28 @@ def _reformulate(self, model, kkt_block, params): all_vars_set.update(identify_variables(expr=expr, include_fixed=True)) if con.equality: - # create multiplier gamma_i = kkt_block.gamma.add() - # add expression to lagrangean lagrangean += (upper - body) * gamma_i - # create mappings info.obj_dual_map[con] = gamma_i info.dual_obj_map[gamma_i] = con else: alpha_l = None if lower is not None: - # create multiplier alpha_l = kkt_block.alpha.add() - # add expression to lagrangean con_expr = lower - body lagrangean += con_expr * alpha_l - # create complement kkt_block.complements.add(complements(alpha_l >= 0, con_expr <= 0)) - # create dual -> con mapping info.dual_obj_map[alpha_l] = con alpha_u = None if upper is not None: - # create multiplier alpha_u = kkt_block.alpha.add() - # add expression to lagrangean con_expr = body - upper lagrangean += con_expr * alpha_u - # create complement kkt_block.complements.add(complements(alpha_u >= 0, con_expr <= 0)) - # create dual -> con mapping info.dual_obj_map[alpha_u] = con - # create con -> dual mapping. Will return None if a bound doesn't exist. info.obj_dual_map[con] = (alpha_l, alpha_u) fixed_vars = ComponentSet(v for v in all_vars_set if v.is_fixed()) @@ -172,40 +159,30 @@ def _reformulate(self, model, kkt_block, params): if missing: raise ValueError("All fixed variables must be included in parametrize_wrt.") - if not (params <= all_vars_set): + if not params <= all_vars_set: raise ValueError( "A variable passed in parametrize_wrt does not exist on an " "active constraint or objective within the model." ) - # loop through variables, add terms to Lagrangean and add mappings var_set = var_set - params for var in var_set: alpha_l = None if var.has_lb(): - # create multiplier alpha_l = kkt_block.alpha.add() - # add expression to lagrangean con_expr = var.lb - var lagrangean += con_expr * alpha_l - # create complement kkt_block.complements.add(complements(alpha_l >= 0, con_expr <= 0)) - # create dual -> con mapping info.dual_obj_map[alpha_l] = var alpha_u = None if var.has_ub(): - # create multiplier alpha_u = kkt_block.alpha.add() - # add expression to lagrangean con_expr = var - var.ub lagrangean += con_expr * alpha_u - # create complement kkt_block.complements.add(complements(alpha_u >= 0, con_expr <= 0)) - # create dual -> con mapping info.dual_obj_map[alpha_u] = var - # create var -> dual mapping. Will return None if a var bound doesn't exist. info.obj_dual_map[var] = (alpha_l, alpha_u) kkt_block.lagrangean = Expression(expr=lagrangean) @@ -217,7 +194,6 @@ def _reformulate(self, model, kkt_block, params): kkt_block.stationarity_conditions.add(deriv_lagrangean[var] == 0) active_objs[0].deactivate() - # kkt_block.dummy_obj = Objective(expr=1) def get_object_from_multiplier(self, model, multiplier_var): """ @@ -242,10 +218,9 @@ def get_object_from_multiplier(self, model, multiplier_var): info = model.private_data() if multiplier_var in info.dual_obj_map: return info.dual_obj_map[multiplier_var] - else: - raise ValueError( - f"The KKT multiplier: {multiplier_var.name}, does not exist on {model.name}." - ) + raise ValueError( + f"The KKT multiplier: {multiplier_var.name}, does not exist on {model.name}." + ) def get_multiplier_from_object(self, model, component): """ @@ -270,7 +245,7 @@ def get_multiplier_from_object(self, model, component): info = model.private_data() if component in info.obj_dual_map: return info.obj_dual_map[component] - else: - raise ValueError( - f"The component '{component.name}' either does not exist on '{model.name}', or is not associated with a multiplier." - ) + raise ValueError( + f"The component '{component.name}' either does not exist on " + f"'{model.name}', or is not associated with a multiplier." + ) From ceb0fdabb50d4223c7108eb459ea2a0b5f43bdf7 Mon Sep 17 00:00:00 2001 From: Chris Laliwala Date: Sat, 28 Mar 2026 15:08:40 -0400 Subject: [PATCH 31/35] formatted code, removed unnecessary imports, etc., also added check for ipopt to relevant tests. --- pyomo/core/plugins/transform/kkt.py | 4 +-- pyomo/core/tests/unit/test_kkt.py | 42 +++++++++++------------------ 2 files changed, 17 insertions(+), 29 deletions(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index 39346af836d..87ebdbf10ab 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -23,9 +23,9 @@ VarList, ) from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd +from pyomo.core.expr.visitor import identify_variables from pyomo.mpec import ComplementarityList, complements from pyomo.util.config_domains import ComponentDataSet -from pyomo.core.expr.visitor import identify_variables class _KKTReformulationData(AutoSlots.Mixin): @@ -74,7 +74,7 @@ def apply_to(self, model, **kwds): if hasattr(model, config.kkt_block_name): raise ValueError( - "model already has an attribute with the" + "model already has an attribute with the " f"specified kkt_block_name: '{config.kkt_block_name}'" ) diff --git a/pyomo/core/tests/unit/test_kkt.py b/pyomo/core/tests/unit/test_kkt.py index ab606fdfa05..177bd8163c8 100644 --- a/pyomo/core/tests/unit/test_kkt.py +++ b/pyomo/core/tests/unit/test_kkt.py @@ -7,40 +7,26 @@ # software. This software is distributed under the 3-clause BSD License. # ____________________________________________________________________________________ -from pyomo.common.dependencies import scipy_available +from pyomo.common import unittest from pyomo.common.numeric_types import value -import pyomo.common.unittest as unittest -from pyomo.common.autoslots import AutoSlots -from pyomo.common.collections import ComponentMap, ComponentSet -from pyomo.common.config import ConfigDict, ConfigValue +from pyomo.core.base.suffix import Suffix +from pyomo.core.expr.compare import assertExpressionsStructurallyEqual from pyomo.environ import ( - ConcreteModel, - Reals, Block, + ConcreteModel, Constraint, - ConstraintList, - Expression, NonNegativeReals, Objective, - RangeSet, Reals, - Set, + SolverFactory, + TerminationCondition, TransformationFactory, Var, - maximize, minimize, - SolverFactory, - TerminationCondition, -) -from pyomo.core.base.suffix import Suffix -from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd -from pyomo.mpec import ComplementarityList, complements -from pyomo.util.vars_from_expressions import get_vars_from_components -from pyomo.util.config_domains import ComponentDataSet -from pyomo.core.expr.compare import ( - assertExpressionsEqual, - assertExpressionsStructurallyEqual, ) +from pyomo.opt import check_available_solvers + +solvers = check_available_solvers('ipopt') class TestKKT(unittest.TestCase): @@ -93,6 +79,7 @@ def check_primal_kkt_transformation_solns(self, m, m_reform): primal_var, kkt_reform_var = v self.assertAlmostEqual(value(primal_var), value(kkt_reform_var)) + @unittest.skipIf('ipopt' not in solvers, "ipopt solver is not available") def test_kkt_solve(self): m = ConcreteModel() m.x = Var(domain=Reals) @@ -411,6 +398,7 @@ def test_parametrized_kkt(self): self.assertFalse(m.obj.active) + @unittest.skipIf('ipopt' not in solvers, "ipopt solver is not available") def test_solve_parametrized_kkt(self): m = self.get_bilevel_model() @@ -454,7 +442,7 @@ def test_multiple_obj_error(self): kkt = TransformationFactory('core.kkt') with self.assertRaisesRegex( - ValueError, f"model must have only one active objective; found 0" + ValueError, "model must have only one active objective; found 0" ): kkt.apply_to(m) @@ -469,8 +457,7 @@ def test_kkt_block_name_error(self): with self.assertRaisesRegex( ValueError, - f"""model already has an attribute with the - specified kkt_block_name: 'b1'""", + "model already has an attribute with the " "specified kkt_block_name: 'b1'", ): kkt.apply_to(m, kkt_block_name='b1') @@ -524,6 +511,7 @@ def test_get_multiplier_from_object_error(self): with self.assertRaisesRegex( ValueError, - "The component 'new_con' either does not exist on 'model', or is not associated with a multiplier.", + "The component 'new_con' either does not exist on 'model', " + "or is not associated with a multiplier.", ): kkt.get_multiplier_from_object(m, component=m2.new_con) From 44570937d2665bba3b05abe7706b5a845a0d4d16 Mon Sep 17 00:00:00 2001 From: Claliwal <136067410+ChrisLaliwala@users.noreply.github.com> Date: Tue, 28 Apr 2026 12:56:07 -0400 Subject: [PATCH 32/35] Update pyomo/core/plugins/transform/kkt.py Co-authored-by: Emma Johnson <12833636+emma58@users.noreply.github.com> --- pyomo/core/plugins/transform/kkt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index 87ebdbf10ab..1c04a498a46 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -78,8 +78,8 @@ def apply_to(self, model, **kwds): f"specified kkt_block_name: '{config.kkt_block_name}'" ) - # we should check that all vars the user fixed are included - # in parametrize_wrt +# We will check below that all vars the user fixed are included in +# parameterize_wrt params = config.parametrize_wrt kkt_block = Block(concrete=True) From e8aca52a70440c77cec6bfd88ee45c86d6fcdc05 Mon Sep 17 00:00:00 2001 From: Chris Laliwala Date: Tue, 28 Apr 2026 13:02:46 -0400 Subject: [PATCH 33/35] updated error messages to be more accurate. changed naming of 'parametrize_wrt' to 'parameterize_wrt' to ensure consistency with rest of Pyomo --- pyomo/core/plugins/transform/kkt.py | 14 +++++++------- pyomo/core/tests/unit/test_kkt.py | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index 1c04a498a46..d7be8487b62 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -54,7 +54,7 @@ class NonLinearProgrammingKKT: ), ) CONFIG.declare( - 'parametrize_wrt', + 'parameterize_wrt', ConfigValue( default=[], domain=ComponentDataSet(Var), @@ -80,10 +80,10 @@ def apply_to(self, model, **kwds): # We will check below that all vars the user fixed are included in # parameterize_wrt - params = config.parametrize_wrt + params = config.parameterize_wrt kkt_block = Block(concrete=True) - kkt_block.parametrize_wrt = params + kkt_block.parameterize_wrt = params self._reformulate(model, kkt_block, params) model.add_component(config.kkt_block_name, kkt_block) return model @@ -100,7 +100,7 @@ def _reformulate(self, model, kkt_block, params): ) if len(active_objs) != 1: raise ValueError( - f"model must have only one active objective; found {len(active_objs)}" + f"model must have exactly active objective; found {len(active_objs)}" ) # collect vars from active objective obj = active_objs[0] @@ -154,14 +154,14 @@ def _reformulate(self, model, kkt_block, params): var_set = ComponentSet(all_vars_set) var_set -= fixed_vars - # do error checking on parametrize_wrt + # do error checking on parameterize_wrt missing = fixed_vars - params if missing: - raise ValueError("All fixed variables must be included in parametrize_wrt.") + raise ValueError("All fixed variables must be included in parameterize_wrt.") if not params <= all_vars_set: raise ValueError( - "A variable passed in parametrize_wrt does not exist on an " + "A variable passed in parameterize_wrt does not exist on an " "active constraint or objective within the model." ) diff --git a/pyomo/core/tests/unit/test_kkt.py b/pyomo/core/tests/unit/test_kkt.py index 177bd8163c8..f15406f3f5b 100644 --- a/pyomo/core/tests/unit/test_kkt.py +++ b/pyomo/core/tests/unit/test_kkt.py @@ -273,7 +273,7 @@ def test_parametrized_kkt(self): m = self.get_bilevel_model() kkt = TransformationFactory('core.kkt') - kkt.apply_to(m, parametrize_wrt=[m.outer1, m.outer2]) + kkt.apply_to(m, parameterize_wrt=[m.outer1, m.outer2]) TransformationFactory("mpec.simple_nonlinear").apply_to(m) # equality constraint @@ -408,7 +408,7 @@ def test_solve_parametrized_kkt(self): m_reform = m.clone() TransformationFactory('core.kkt').apply_to( - m_reform, parametrize_wrt=[m_reform.outer1, m_reform.outer2] + m_reform, parameterize_wrt=[m_reform.outer1, m_reform.outer2] ) TransformationFactory("mpec.simple_nonlinear").apply_to(m_reform) @@ -419,7 +419,7 @@ def test_solve_parametrized_kkt(self): m_reform = m.clone() TransformationFactory('core.kkt').apply_to( - m_reform, parametrize_wrt=[m_reform.outer1, m_reform.outer2] + m_reform, parameterize_wrt=[m_reform.outer1, m_reform.outer2] ) TransformationFactory("mpec.simple_nonlinear").apply_to(m_reform) @@ -430,7 +430,7 @@ def test_solve_parametrized_kkt(self): m_reform = m.clone() TransformationFactory('core.kkt').apply_to( - m_reform, parametrize_wrt=[m_reform.outer1, m_reform.outer2] + m_reform, parameterize_wrt=[m_reform.outer1, m_reform.outer2] ) TransformationFactory("mpec.simple_nonlinear").apply_to(m_reform) @@ -461,7 +461,7 @@ def test_kkt_block_name_error(self): ): kkt.apply_to(m, kkt_block_name='b1') - def test_parametrize_wrt_unknown_error(self): + def test_parameterize_wrt_unknown_error(self): m = ConcreteModel() m.x = Var(domain=Reals) m.y = Var(domain=Reals) @@ -474,10 +474,10 @@ def test_parametrize_wrt_unknown_error(self): with self.assertRaisesRegex( ValueError, - "A variable passed in parametrize_wrt does not exist on an " + "A variable passed in parameterize_wrt does not exist on an " "active constraint or objective within the model.", ): - kkt.apply_to(m, parametrize_wrt=[m.b1.x1]) + kkt.apply_to(m, parameterize_wrt=[m.b1.x1]) def test_get_object_from_multiplier_error(self): m = ConcreteModel(name="model") From 1ce03dc76c40285bda8941b299e77e3fcf229444 Mon Sep 17 00:00:00 2001 From: Chris Laliwala Date: Tue, 28 Apr 2026 13:05:15 -0400 Subject: [PATCH 34/35] minor updates to documentation and error messages. --- pyomo/core/plugins/transform/kkt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index d7be8487b62..423049a9a0c 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -100,7 +100,7 @@ def _reformulate(self, model, kkt_block, params): ) if len(active_objs) != 1: raise ValueError( - f"model must have exactly active objective; found {len(active_objs)}" + f"model must have exactly one active objective; found {len(active_objs)}" ) # collect vars from active objective obj = active_objs[0] @@ -161,7 +161,7 @@ def _reformulate(self, model, kkt_block, params): if not params <= all_vars_set: raise ValueError( - "A variable passed in parameterize_wrt does not exist on an " + "A variable passed in parameterize_wrt does not exist in an " "active constraint or objective within the model." ) @@ -231,7 +231,7 @@ def get_multiplier_from_object(self, model, component): Parameters ---------- model: ConcreteModel - The model on which the kkt transformation was applied to + The model to which the kkt transformation was applied to component: Constraint or Variable Returns From 1bec2ccf66d10179b1aba36e5e223ce66a3d36c5 Mon Sep 17 00:00:00 2001 From: Chris Laliwala Date: Tue, 28 Apr 2026 13:39:57 -0400 Subject: [PATCH 35/35] updated error checking on parameterize_wrt --- pyomo/core/plugins/transform/kkt.py | 15 ++++++++++----- pyomo/core/tests/unit/test_kkt.py | 7 ++++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/pyomo/core/plugins/transform/kkt.py b/pyomo/core/plugins/transform/kkt.py index 423049a9a0c..cc56a235316 100644 --- a/pyomo/core/plugins/transform/kkt.py +++ b/pyomo/core/plugins/transform/kkt.py @@ -78,8 +78,8 @@ def apply_to(self, model, **kwds): f"specified kkt_block_name: '{config.kkt_block_name}'" ) -# We will check below that all vars the user fixed are included in -# parameterize_wrt + # We will check below that all vars the user fixed are included in + # parameterize_wrt params = config.parameterize_wrt kkt_block = Block(concrete=True) @@ -157,12 +157,17 @@ def _reformulate(self, model, kkt_block, params): # do error checking on parameterize_wrt missing = fixed_vars - params if missing: - raise ValueError("All fixed variables must be included in parameterize_wrt.") + raise ValueError( + "All fixed variables must be included in parameterize_wrt. " + "Missing variables:\n\t" + "\n\t".join(v.name for v in missing) + ) - if not params <= all_vars_set: + extra = params - all_vars_set + if extra: raise ValueError( "A variable passed in parameterize_wrt does not exist in an " - "active constraint or objective within the model." + "active constraint or objective within the model. " + "Invalid variables:\n\t" + "\n\t".join(v.name for v in extra) ) var_set = var_set - params diff --git a/pyomo/core/tests/unit/test_kkt.py b/pyomo/core/tests/unit/test_kkt.py index f15406f3f5b..6d8eca50507 100644 --- a/pyomo/core/tests/unit/test_kkt.py +++ b/pyomo/core/tests/unit/test_kkt.py @@ -442,7 +442,7 @@ def test_multiple_obj_error(self): kkt = TransformationFactory('core.kkt') with self.assertRaisesRegex( - ValueError, "model must have only one active objective; found 0" + ValueError, "model must have exactly one active objective; found 0" ): kkt.apply_to(m) @@ -474,8 +474,9 @@ def test_parameterize_wrt_unknown_error(self): with self.assertRaisesRegex( ValueError, - "A variable passed in parameterize_wrt does not exist on an " - "active constraint or objective within the model.", + "A variable passed in parameterize_wrt does not exist in an " + "active constraint or objective within the model. " + "Invalid variables:\n\t" + "b1.x1", ): kkt.apply_to(m, parameterize_wrt=[m.b1.x1])