Skip to content

Commit 37084d9

Browse files
authored
Merge pull request #3881 from ChrisLaliwala/kkt-transform-refactor
KKT Transform Code Refactor
2 parents df3a6a6 + ca735e5 commit 37084d9

3 files changed

Lines changed: 775 additions & 0 deletions

File tree

pyomo/core/plugins/transform/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@
2121
scaling,
2222
logical_to_linear,
2323
lp_dual,
24+
kkt,
2425
)
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
# ____________________________________________________________________________________
2+
#
3+
# Pyomo: Python Optimization Modeling Objects
4+
# Copyright (c) 2008-2026 National Technology and Engineering Solutions of Sandia, LLC
5+
# Under the terms of Contract DE-NA0003525 with National Technology and Engineering
6+
# Solutions of Sandia, LLC, the U.S. Government retains certain rights in this
7+
# software. This software is distributed under the 3-clause BSD License.
8+
# ____________________________________________________________________________________
9+
10+
11+
from pyomo.common.autoslots import AutoSlots
12+
from pyomo.common.collections import ComponentMap, ComponentSet
13+
from pyomo.common.config import ConfigDict, ConfigValue
14+
from pyomo.core import (
15+
Block,
16+
Constraint,
17+
ConstraintList,
18+
Expression,
19+
NonNegativeReals,
20+
Objective,
21+
TransformationFactory,
22+
Var,
23+
VarList,
24+
)
25+
from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd
26+
from pyomo.core.expr.visitor import identify_variables
27+
from pyomo.mpec import ComplementarityList, complements
28+
from pyomo.util.config_domains import ComponentDataSet
29+
30+
31+
class _KKTReformulationData(AutoSlots.Mixin):
32+
__slots__ = ("obj_dual_map", "dual_obj_map")
33+
34+
def __init__(self):
35+
self.obj_dual_map = ComponentMap()
36+
self.dual_obj_map = ComponentMap()
37+
38+
39+
Block.register_private_data_initializer(_KKTReformulationData)
40+
41+
42+
@TransformationFactory.register(
43+
'core.kkt', 'Generate KKT reformulation of the given model'
44+
)
45+
class NonLinearProgrammingKKT:
46+
CONFIG = ConfigDict("core.kkt")
47+
CONFIG.declare(
48+
'kkt_block_name',
49+
ConfigValue(
50+
default='kkt',
51+
doc="""
52+
Name of the block on which the kkt variables and constraints will be stored.
53+
""",
54+
),
55+
)
56+
CONFIG.declare(
57+
'parameterize_wrt',
58+
ConfigValue(
59+
default=[],
60+
domain=ComponentDataSet(Var),
61+
description='Vars to treat as data for the purposes of generating KKT reformulation',
62+
doc="""
63+
Optional list of Vars to be treated as data while generating the KKT reformulation.
64+
""",
65+
),
66+
)
67+
68+
def apply_to(self, model, **kwds):
69+
"""
70+
Reformulate model with KKT conditions.
71+
"""
72+
config = self.CONFIG(kwds.pop('options', {}))
73+
config.set_value(kwds)
74+
75+
if hasattr(model, config.kkt_block_name):
76+
raise ValueError(
77+
"model already has an attribute with the "
78+
f"specified kkt_block_name: '{config.kkt_block_name}'"
79+
)
80+
81+
# We will check below that all vars the user fixed are included in
82+
# parameterize_wrt
83+
params = config.parameterize_wrt
84+
85+
kkt_block = Block(concrete=True)
86+
kkt_block.parameterize_wrt = params
87+
self._reformulate(model, kkt_block, params)
88+
model.add_component(config.kkt_block_name, kkt_block)
89+
return model
90+
91+
def _reformulate(self, model, kkt_block, params):
92+
# initialize
93+
info = model.private_data()
94+
lagrangean = 0
95+
all_vars_set = ComponentSet()
96+
97+
# collect the active Objectives
98+
active_objs = list(
99+
model.component_data_objects(Objective, active=True, descend_into=True)
100+
)
101+
if len(active_objs) != 1:
102+
raise ValueError(
103+
f"model must have exactly one active objective; found {len(active_objs)}"
104+
)
105+
# collect vars from active objective
106+
obj = active_objs[0]
107+
all_vars_set.update(identify_variables(obj.expr, include_fixed=True))
108+
lagrangean += obj.sense * obj.expr
109+
110+
# list of equality multipliers
111+
kkt_block.gamma = VarList()
112+
# list of inequality multipliers
113+
kkt_block.alpha = VarList(domain=NonNegativeReals)
114+
# define inequality complements
115+
kkt_block.complements = ComplementarityList()
116+
117+
for con in model.component_data_objects(
118+
Constraint, descend_into=True, active=True
119+
):
120+
lower, body, upper = con.to_bounded_expression()
121+
122+
# collect variables in constraint
123+
for expr in (lower, body, upper):
124+
if expr is None:
125+
continue
126+
all_vars_set.update(identify_variables(expr=expr, include_fixed=True))
127+
128+
if con.equality:
129+
gamma_i = kkt_block.gamma.add()
130+
lagrangean += (upper - body) * gamma_i
131+
info.obj_dual_map[con] = gamma_i
132+
info.dual_obj_map[gamma_i] = con
133+
134+
else:
135+
alpha_l = None
136+
if lower is not None:
137+
alpha_l = kkt_block.alpha.add()
138+
con_expr = lower - body
139+
lagrangean += con_expr * alpha_l
140+
kkt_block.complements.add(complements(alpha_l >= 0, con_expr <= 0))
141+
info.dual_obj_map[alpha_l] = con
142+
143+
alpha_u = None
144+
if upper is not None:
145+
alpha_u = kkt_block.alpha.add()
146+
con_expr = body - upper
147+
lagrangean += con_expr * alpha_u
148+
kkt_block.complements.add(complements(alpha_u >= 0, con_expr <= 0))
149+
info.dual_obj_map[alpha_u] = con
150+
151+
info.obj_dual_map[con] = (alpha_l, alpha_u)
152+
153+
fixed_vars = ComponentSet(v for v in all_vars_set if v.is_fixed())
154+
var_set = ComponentSet(all_vars_set)
155+
var_set -= fixed_vars
156+
157+
# do error checking on parameterize_wrt
158+
missing = fixed_vars - params
159+
if missing:
160+
raise ValueError(
161+
"All fixed variables must be included in parameterize_wrt. "
162+
"Missing variables:\n\t" + "\n\t".join(v.name for v in missing)
163+
)
164+
165+
extra = params - all_vars_set
166+
if extra:
167+
raise ValueError(
168+
"A variable passed in parameterize_wrt does not exist in an "
169+
"active constraint or objective within the model. "
170+
"Invalid variables:\n\t" + "\n\t".join(v.name for v in extra)
171+
)
172+
173+
var_set = var_set - params
174+
for var in var_set:
175+
alpha_l = None
176+
if var.has_lb():
177+
alpha_l = kkt_block.alpha.add()
178+
con_expr = var.lb - var
179+
lagrangean += con_expr * alpha_l
180+
kkt_block.complements.add(complements(alpha_l >= 0, con_expr <= 0))
181+
info.dual_obj_map[alpha_l] = var
182+
183+
alpha_u = None
184+
if var.has_ub():
185+
alpha_u = kkt_block.alpha.add()
186+
con_expr = var - var.ub
187+
lagrangean += con_expr * alpha_u
188+
kkt_block.complements.add(complements(alpha_u >= 0, con_expr <= 0))
189+
info.dual_obj_map[alpha_u] = var
190+
191+
info.obj_dual_map[var] = (alpha_l, alpha_u)
192+
193+
kkt_block.lagrangean = Expression(expr=lagrangean)
194+
195+
# enforce stationarity conditions
196+
deriv_lagrangean = reverse_sd(kkt_block.lagrangean.expr)
197+
kkt_block.stationarity_conditions = ConstraintList()
198+
for var in var_set:
199+
kkt_block.stationarity_conditions.add(deriv_lagrangean[var] == 0)
200+
201+
active_objs[0].deactivate()
202+
203+
def get_object_from_multiplier(self, model, multiplier_var):
204+
"""
205+
Return the constraint corresponding to a KKT multiplier variable. If the
206+
multiplier corresponds to an inequality formed by a variable bound, the variable
207+
is returned.
208+
209+
Parameters
210+
----------
211+
model: ConcreteModel
212+
The model on which the kkt transformation was applied
213+
multiplier_var: Var
214+
A KKT multiplier created by the transformation.
215+
216+
Returns
217+
-------
218+
Object
219+
- Constraint object
220+
- Variable
221+
"""
222+
223+
info = model.private_data()
224+
if multiplier_var in info.dual_obj_map:
225+
return info.dual_obj_map[multiplier_var]
226+
raise ValueError(
227+
f"The KKT multiplier: {multiplier_var.name}, does not exist on {model.name}."
228+
)
229+
230+
def get_multiplier_from_object(self, model, component):
231+
"""
232+
Return the multiplier for the object. If the object is a normal constraint, a single
233+
multiplier is returned. If the object is a ranged constraint or a variable, a tuple
234+
containing the lower and upper bound multipliers is returned.
235+
236+
Parameters
237+
----------
238+
model: ConcreteModel
239+
The model to which the kkt transformation was applied to
240+
component: Constraint or Variable
241+
242+
Returns
243+
-------
244+
VarData | tuple[VarData | None, VarData | None]
245+
The KKT multiplier(s) corresponding to the component.
246+
For ranged constraints/variables, returns (lb_mult, ub_mult),
247+
where an entry is 'None' if that bound doesn't exist.
248+
"""
249+
250+
info = model.private_data()
251+
if component in info.obj_dual_map:
252+
return info.obj_dual_map[component]
253+
raise ValueError(
254+
f"The component '{component.name}' either does not exist on "
255+
f"'{model.name}', or is not associated with a multiplier."
256+
)

0 commit comments

Comments
 (0)