-
Notifications
You must be signed in to change notification settings - Fork 573
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 all 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,9 @@ | |
| # software. This software is distributed under the 3-clause BSD License. | ||
| # ____________________________________________________________________________________ | ||
|
|
||
| from collections import defaultdict | ||
| from operator import attrgetter | ||
|
|
||
| from pyomo.core import ( | ||
| TransformationFactory, | ||
| Var, | ||
|
|
@@ -15,14 +18,30 @@ | |
| Objective, | ||
| Block, | ||
| value, | ||
| Expression, | ||
| Param, | ||
| Suffix, | ||
| ) | ||
|
|
||
| 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.core.base import ComponentUID, SortComponents | ||
| from pyomo.common.deprecation import deprecation_warning | ||
|
|
||
| from pyomo.repn.util import categorize_valid_components | ||
|
|
||
| ### FIXME: Remove the following as soon as non-active components no | ||
| ### longer report active==True | ||
| from pyomo.network import Port | ||
| from pyomo.core.base import RangeSet, Set | ||
|
|
||
| import logging | ||
|
|
||
| logger = logging.getLogger('pyomo.core') | ||
|
|
||
|
|
||
| def target_list(x): | ||
| deprecation_msg = ( | ||
|
|
@@ -64,9 +83,16 @@ def target_list(x): | |
| ) | ||
|
|
||
|
|
||
| import logging | ||
| class _AddSlackVariablesData(AutoSlots.Mixin): | ||
| __slots__ = ('slack_variables', 'relaxed_constraint', 'summed_slacks_expr') | ||
|
|
||
| def __init__(self): | ||
| self.slack_variables = defaultdict(list) | ||
| self.relaxed_constraint = ComponentMap() | ||
| self.summed_slacks_expr = None | ||
|
|
||
| logger = logging.getLogger('pyomo.core') | ||
|
|
||
| Block.register_private_data_initializer(_AddSlackVariablesData) | ||
|
|
||
|
|
||
| @TransformationFactory.register( | ||
|
|
@@ -90,6 +116,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,10 +145,11 @@ 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 | ||
| ) | ||
| constraintDatas = self._get_all_constraint_datas(instance) | ||
|
|
||
| else: | ||
| constraintDatas = [] | ||
| for t in targets: | ||
|
|
@@ -126,10 +169,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 +200,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 +212,141 @@ 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) | ||
|
|
||
| trans_info.summed_slacks_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_all_constraint_datas(self, model): | ||
| components, unknown = categorize_valid_components( | ||
| model, | ||
| active=True, | ||
| sort=SortComponents.deterministic, | ||
| valid={ | ||
| Block, | ||
| Expression, | ||
| Var, | ||
| Param, | ||
| Suffix, | ||
| Objective, | ||
| # FIXME: Non-active components should not report as Active | ||
| Set, | ||
| RangeSet, | ||
| Port, | ||
| }, | ||
| targets={Constraint}, | ||
| ) | ||
| if unknown: | ||
| raise ValueError( | ||
| "The model ('%s') contains the following active components " | ||
| "that the 'core.add_slack_variables' transformation does not " | ||
| "know how to process:\n\t%s\nIf these components are Block-like " | ||
| "(e.g., Disjuncts) and the intent is to add slacks on them, call " | ||
| "the transformation on them directly." | ||
| % ( | ||
| model.name, | ||
| "\n\t".join( | ||
| sorted( | ||
| "%s:\n\t\t%s" | ||
| % (k, "\n\t\t".join(sorted(map(attrgetter('name'), v)))) | ||
| for k, v in unknown.items() | ||
| ) | ||
| ), | ||
| ) | ||
| ) | ||
| if components[Constraint]: | ||
| for block in components[Constraint]: | ||
| for cons in block.component_data_objects( | ||
| Constraint, | ||
| active=True, | ||
| descend_into=False, | ||
| sort=SortComponents.deterministic, | ||
| ): | ||
| yield cons | ||
|
|
||
| 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}." | ||
| ) | ||
|
|
||
| def get_summed_slacks_expr(self, model): | ||
| """Return an expression summing all the slacks added to the model during the | ||
| transformation. This would most commonly be used to add a penalty on non-zero | ||
| slacks to an existing objective. | ||
|
|
||
| Returns | ||
| ------- | ||
| Expression | ||
|
|
||
| Parameters | ||
| ---------- | ||
| model: ConcreteModel | ||
| A model, having had the 'core.add_slack_variables' transformation | ||
| applied to it | ||
| """ | ||
| expr = model.private_data().summed_slacks_expr | ||
| if expr is None: | ||
| raise ValueError( | ||
| f"It does not appear that {model.name} is a model that was transformed " | ||
| f"by the 'core.add_slack_variables' transformation." | ||
| ) | ||
| return expr | ||
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.