feat(constraint.py): Auto-convert hard to soft constraints through soften method - #904
feat(constraint.py): Auto-convert hard to soft constraints through soften method#904isanchez-ng wants to merge 22 commits into
soften method#904Conversation
…oid alternating between a bare Variable and a tuple of Variables depending on the constraint's sign. Negative is now None for inequality constraints instead of being absent from the return.
… a scalar operand) to accept ConstantLike. The narrow annotation caused mypy to flag valid code
for more information, see https://pre-commit.ci
Merging this PR will degrade performance by 22.69%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ❌ | test_to_lp[qp-n=1000] |
2 MB | 2.6 MB | -22.69% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing isanchez-ng:master (e533695) with master (b71e9a9)
Footnotes
-
181 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
|
@isanchez-ng wonderful initiative and I like the approach! let me know as soon as I should review |
…n arrays (FOR NOW)
…raints & add unit test
for more information, see https://pre-commit.ci
… and mask application
… checks Also fixes a mypy union-attr error on the hasattr-guarded penalty check by normalizing via np.asarray before reducing with np.all.
for more information, see https://pre-commit.ci
…errors Slack.positive/negative were typed as VariableLike (ScalarVariable | Variable), but add_variables() always returns Variable, never ScalarVariable.
…od is not supported for frozen constraints.
…ation (model.add_constraints) Adds a `penalty` kwarg to `Model.add_constraints` that calls `.soften()` on the newly registered constraint, saving a follow-up call. Raises ValueError when combined with freeze=True (explicit or via the model's freeze_constraints default), since soften is not supported on frozen constraints.
|
@codspeedbot fix this regression |
|
@FabianHofmann I think this is ready for review! The one thing I couldn't fix was the performance analysis. If you could guide me a little bit with this I'd be happy to fix them as well. Heads up that it's a fair amount of new code (+370 lines). Happy to hop on a quick 10-min call to walk you through it if that helps for a faster review :) |
|
@isanchez-ng great job, the changes look totally reviewable! here is what my agent found (easy to tackle): [F1] Orphan slack variable on mixed-sign constraints (correctness, should fix). positive_slack = model.add_variables(...) # side effect on the model
negative_slack = None
sign_values = pd.unique(self.sign.values.ravel())
if len(sign_values) > 1:
raise NotImplementedError(...) # too lateSo softening a mixed-sign constraint raises, but leaves a dangling {name}_pos variable registered in model.variables, with the constraint and objective untouched. That breaks fail-fast: it fails and pollutes the model. Fix: read and validate sign_values at the very top of the method, before creating any variable. Cheap and complete. [F2] penalty=0 is allowed but the docstring and message say it must be positive (minor). [F3] The add_constraints(..., penalty=...) shortcut throws away the Slack return (API gap, minor). Beyond that: [F4] should we add a Then this is good to go! |
Closes #782
Hi! This is my first contribution. Let me know if there's something I should work on!
Context: Building soft constraints requires a lot of manual work that can be avoided through a method that modifies both the variables (to add the slacks) and the objective function (to add the penalty term). This PR intents to built this new feature.
Changes proposed in this Pull Request
Implementation
I followed the proposed structure for the method, but changed some details that I mention on the next section.
Also, I changed slightly the typehints for
objective.__add__. to avoid some mypy errors that weren't actually errors.How did I test this new feature?
Open discussions:
isinstance(var_name, tuple)through their own codes. Instead of that, I proposed a NamedTuple.soften()if the model's objective hasn't been defined yet, sincesoften()adds a penalty term to the existing objective rather than replacing it.soften()run beforeadd_objective(). But doing so creates a weird condition where the linemodel.objective += penalty thingsasserts the objective is still empty, so calling a secondmodel.add_objectiveaftersoften()would raise an error telling the user to passoverwrite=True. Doing that, however, replaces the whole objective expression, and silently discards the penalty termsoften()had already added.soften()also relies onmodel.senseto pick the correct sign for the penalty term. Sincesensedefaults to"min"untilmodel.add_objective()is called with a different value, callingsoften()first risks silently penalizing in the wrong direction if the user later setssense="max".I'm open to discussion on these bullet points if someone else has a better proposal.
Questions that still have to be answered:
doc/release_notes.rstof the upcoming release is included.. I don't know exactly what they mean with this 🤔. Does this just mean to add a small phrase of what's this doing? (it's my first time colllaborating on this repo). I'd appreaciate a small guidance.To-Dos:
.softenmethodsofteninsidemodel.add_constraintChecklist
AGENTS.md).doc.