Skip to content

New options and mappings in the core.add_slack_variables Transformation#3869

Open
emma58 wants to merge 7 commits intoPyomo:mainfrom
emma58:add_slacks_additions
Open

New options and mappings in the core.add_slack_variables Transformation#3869
emma58 wants to merge 7 commits intoPyomo:mainfrom
emma58:add_slacks_additions

Conversation

@emma58
Copy link
Copy Markdown
Contributor

@emma58 emma58 commented Mar 6, 2026

Fixes # .

Summary/Motivation:

We've always used the core.add_slack_variables transformation as purely a debugging tool for infeasible models. However, there are other reasons one could want to add slack variables, such as relaxing certain constraints of a model by bounded amounts. This PR adds a couple features to the add slack variables transformation to make that easier, specifically an API so that a user can get the slack variables from the constraints and adding an option to not change the objective.

Changes proposed in this PR:

  • Adds a config option to not touch the objective
  • Adds public methods for getting relaxed constraints corresponding to given slack variables and vice versa on the transformation

Legal Acknowledgement

By contributing to this software project, I have read the contribution guide and agree to the following terms and conditions for my contribution:

  1. I agree my contributions are submitted under the BSD license.
  2. I represent I am authorized to make the contributions and grant the license. If my employer has rights to intellectual property that includes these contributions, I represent that I have received permission to make contributions and grant the required license on behalf of that employer.

emma58 added 3 commits March 6, 2026 09:30
…rmation, as well as an API to retrieve slack variables and relaxed constraints from each other
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 6, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.93%. Comparing base (6ab0106) to head (40252d8).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3869   +/-   ##
=======================================
  Coverage   89.92%   89.93%           
=======================================
  Files         902      902           
  Lines      106393   106438   +45     
=======================================
+ Hits        95679    95726   +47     
+ Misses      10714    10712    -2     
Flag Coverage Δ
builders 29.20% <36.53%> (+0.01%) ⬆️
default 86.23% <100.00%> (?)
expensive 35.64% <36.53%> (?)
linux 87.38% <100.00%> (-2.04%) ⬇️
linux_other 87.38% <100.00%> (+<0.01%) ⬆️
oldsolvers 28.11% <36.53%> (+<0.01%) ⬆️
osx 82.72% <100.00%> (+<0.01%) ⬆️
win 85.82% <100.00%> (+0.01%) ⬆️
win_other 85.82% <100.00%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@emma58 emma58 requested a review from jsiirola March 10, 2026 16:55
@blnicho blnicho self-requested a review March 10, 2026 18:57
Copy link
Copy Markdown
Member

@jsiirola jsiirola left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any problems with this PR, but I do have a couple philosophical questions...

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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you get a meaningful error if get_slack_variables is called on a model that was not transformed by this Transformation?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, significantly more meaningful than I was expecting, actually:

>>> from pyomo.environ import *
>>> m = ConcreteModel()
>>> m.x = Var()
>>> m.y = Var()
>>> m.c = Constraint(expr=(3, m.x + m.y, 4))
>>> m.obj = Objective(expr= m.x - m.y)
>>> TransformationFactory('core.add_slack_variables').get_slack_variables(m, m.c)
Traceback (most recent call last):
  File "<python-input-6>", line 1, in <module>
    TransformationFactory('core.add_slack_variables').get_slack_variables(m, m.c)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^
  File "/Users/esjohn/src/pyomo/pyomo/core/plugins/transform/add_slack_vars.py", line 240, in get_slack_variables
    raise ValueError(
    ...<3 lines>...
    )
ValueError: It does not appear that c is a constraint on model unknown that was relaxed by the 'core.add_slack_variables' transformation.

We were having a very smart day when we implemented the private data Blocks... This is working because, in the scope of that file, slack_variables gets created when we get the private_data, so then it's just empty and we get a good error.

o.deactivate()

# make a new objective that minimizes sum of slack variables
xblock._slack_objective = Objective(expr=obj_expr)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a use-case for saving the obj_expr in the privae_data() and adding an API for users to get it? For example, that would make it easier to add it as a penalty to the existing Objective... Without it, a user has to search the new block for a bunch of scalar variables...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm open to this, but a user wouldn't have to go searching for the scalars: they can get them via the Constraints. But maybe since this is probably a relatively common use case, we should make it easier.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I added it.

Comment on lines +192 to +193
trans_info.slack_variables[cons].append(posSlack)
trans_info.relaxed_constraint[posSlack] = cons
Copy link
Copy Markdown
Member

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))?

Copy link
Copy Markdown
Contributor Author

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, pprint is enough for debugging... I am certainly guilty of this method.

@emma58
Copy link
Copy Markdown
Contributor Author

emma58 commented Mar 27, 2026

I decided to make this error for unknown active components. This means it will now die on an untransformed GDP, but I think that's okay because if someone wants to add slacks on a Disjunct, they can just call it on the Disjunct directly. (It just seems like that would be a rare enough use case compared to the more likely forgot-to-transform-first situation that it's okay to make it harder to accomplish.)

@emma58 emma58 requested a review from jsiirola March 27, 2026 23:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants