-
Notifications
You must be signed in to change notification settings - Fork 577
New options and mappings in the core.add_slack_variables Transformation #3869
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
94050a4
41880c6
155815b
db1946c
53b2f14
4c62d85
40252d8
ba337df
31a8486
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,8 @@ | |
| # software. This software is distributed under the 3-clause BSD License. | ||
| # ____________________________________________________________________________________ | ||
|
|
||
| from collections import defaultdict | ||
|
|
||
| from pyomo.core import ( | ||
| TransformationFactory, | ||
| Var, | ||
|
|
@@ -17,12 +19,18 @@ | |
| value, | ||
| ) | ||
|
|
||
| from pyomo.common.autoslots import AutoSlots | ||
| from pyomo.common.collections import ComponentMap | ||
| from pyomo.common.modeling import unique_component_name | ||
| from pyomo.core.plugins.transform.hierarchy import NonIsomorphicTransformation | ||
| from pyomo.common.config import ConfigBlock, ConfigValue | ||
| from pyomo.core.base import ComponentUID | ||
| from pyomo.common.deprecation import deprecation_warning | ||
|
|
||
| import logging | ||
|
|
||
| logger = logging.getLogger('pyomo.core') | ||
|
|
||
|
|
||
| def target_list(x): | ||
| deprecation_msg = ( | ||
|
|
@@ -64,9 +72,15 @@ def target_list(x): | |
| ) | ||
|
|
||
|
|
||
| import logging | ||
| class _AddSlackVariablesData(AutoSlots.Mixin): | ||
| __slots__ = ('slack_variables', 'relaxed_constraint') | ||
|
|
||
| logger = logging.getLogger('pyomo.core') | ||
| def __init__(self): | ||
| self.slack_variables = defaultdict(list) | ||
| self.relaxed_constraint = ComponentMap() | ||
|
|
||
|
|
||
| Block.register_private_data_initializer(_AddSlackVariablesData) | ||
|
|
||
|
|
||
| @TransformationFactory.register( | ||
|
|
@@ -90,6 +104,22 @@ class AddSlackVariables(NonIsomorphicTransformation): | |
| doc="This specifies the list of Constraints to add slack variables to.", | ||
| ), | ||
| ) | ||
| CONFIG.declare( | ||
| 'add_slack_objective', | ||
| ConfigValue( | ||
| default=True, | ||
| domain=bool, | ||
| description="Whether or not to change the model objective to minimizing " | ||
| "the added slack variables.", | ||
| doc=""" | ||
| Whether or not to change the problem objective to minimize the added slack | ||
| variables. If True (the default), the original objective is deactivated | ||
| and the transformation adds an objective to minimize the sum of the added | ||
| (non-negative) slack variables. If False, the transformation does not | ||
| change the model objective. | ||
| """, | ||
| ), | ||
| ) | ||
|
|
||
| def __init__(self, **kwds): | ||
| kwds['name'] = "add_slack_vars" | ||
|
|
@@ -103,6 +133,8 @@ def _apply_to_impl(self, instance, **kwds): | |
| config.set_value(kwds) | ||
| targets = config.targets | ||
|
|
||
| trans_info = instance.private_data() | ||
|
|
||
| if targets is None: | ||
| constraintDatas = instance.component_data_objects( | ||
| Constraint, descend_into=True | ||
|
|
@@ -126,10 +158,6 @@ def _apply_to_impl(self, instance, **kwds): | |
| else: | ||
| constraintDatas.append(t) | ||
|
|
||
| # deactivate the objective | ||
| for o in instance.component_data_objects(Objective): | ||
| o.deactivate() | ||
|
|
||
| # create block where we can add slack variables safely | ||
| xblockname = unique_component_name(instance, "_core_add_slack_variables") | ||
| instance.add_component(xblockname, Block()) | ||
|
|
@@ -161,6 +189,8 @@ def _apply_to_impl(self, instance, **kwds): | |
| body += posSlack | ||
| # penalize slack in objective | ||
| obj_expr += posSlack | ||
| trans_info.slack_variables[cons].append(posSlack) | ||
| trans_info.relaxed_constraint[posSlack] = cons | ||
| if upper is not None: | ||
| # we subtract a positive slack variable from the body: | ||
| # declare slack | ||
|
|
@@ -171,6 +201,70 @@ def _apply_to_impl(self, instance, **kwds): | |
| body -= negSlack | ||
| # add slack to objective | ||
| obj_expr += negSlack | ||
| trans_info.slack_variables[cons].append(negSlack) | ||
| trans_info.relaxed_constraint[negSlack] = cons | ||
|
|
||
| cons.set_value((lower, body, upper)) | ||
| # make a new objective that minimizes sum of slack variables | ||
| xblock._slack_objective = Objective(expr=obj_expr) | ||
|
|
||
| if config.add_slack_objective: | ||
| # deactivate the objective | ||
| for o in instance.component_data_objects(Objective): | ||
| o.deactivate() | ||
|
|
||
| # make a new objective that minimizes sum of slack variables | ||
| xblock._slack_objective = Objective(expr=obj_expr) | ||
|
jsiirola marked this conversation as resolved.
|
||
|
|
||
| def get_slack_variables(self, model, constraint): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you need to pass the model as an argument? Why not get the model from the constraint object directly,
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
@emma58, question: would it make sense to put a |
||
| """Return the list of slack variables used to relax 'constraint.' Note | ||
| that if 'constraint' is one-sided, there will be a single variable in | ||
| the list, but if it is a ranged constraint (l <= expr <= u) or an | ||
| equality, there will be two variables. | ||
|
|
||
| Returns | ||
| ------- | ||
| List of slack variables | ||
|
|
||
| Parameters | ||
| ---------- | ||
| model: ConcreteModel | ||
| A model, having had the 'core.add_slack_variables' transformation | ||
| applied to it | ||
| constraint: Constraint | ||
| A constraint that was relaxed by the transformation (either | ||
| because no targets were specified or because it was a target) | ||
| """ | ||
| slack_variables = model.private_data().slack_variables | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you get a meaningful error if
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wow, significantly more meaningful than I was expecting, actually: We were having a very smart day when we implemented the private data Blocks... This is working because, in the scope of that file, |
||
| if constraint in slack_variables: | ||
| return slack_variables[constraint] | ||
| else: | ||
| raise ValueError( | ||
| f"It does not appear that {constraint.name} is a constraint " | ||
| f"on model {model.name} that was relaxed by the " | ||
| f"'core.add_slack_variables' transformation." | ||
| ) | ||
|
|
||
| def get_relaxed_constraint(self, model, slack_var): | ||
| """Return the constraint that 'slack_var' is used to relax. | ||
|
|
||
| Returns | ||
| ------- | ||
| Constraint | ||
|
|
||
| Parameters | ||
| ----------- | ||
| model: ConcreteModel | ||
| A model, having had the 'core.add_slack_variables' transformation | ||
| applied to it | ||
| slack_var: Var | ||
| A variable created by the 'core.add_slack_variables' transformation to | ||
| relax a constraint. | ||
| """ | ||
| relaxed_constraints = model.private_data().relaxed_constraint | ||
| if slack_var in relaxed_constraints: | ||
| return relaxed_constraints[slack_var] | ||
| else: | ||
| raise ValueError( | ||
| f"It does not appear that {slack_var.name} is a slack variable " | ||
| f"created by applying the 'core.add_slack_variables' transformation " | ||
| f"to model {model.name}." | ||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Philosophical question: now that we are providing a useful API for mapping slacks to constraints and back, does it make sense to avoid all the name munging and just make a single indexed
xblock.slacks = Var(NonNegativeIntegers, bounds=(0, None))?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maaaaybe. Though the way it is now,
pprintis enough for debugging... I am certainly guilty of this method.