Skip to content
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

solve problem by multiple solvers #2450

Open
wants to merge 11 commits into
base: master
Choose a base branch
from

Conversation

oriyalperin
Copy link

Description

As @erelsgl suggested in discussion #1522,
in some problems, some solvers may be better than others, but they might also fail with a SolverError.
Therefore, we've added a new method that allows sending a list of solvers rather than just one.
This function tries them in the order they are provided, and return the first solution for which no SolverError is raised.
Issue link: #1529

Type of change

  • New feature (backwards compatible)
  • New feature (breaking API changes)
  • Bug fix
  • Other (Documentation, CI, ...)

Contribution checklist

  • Add our license to new files.
  • Check that your code adheres to our coding style.
  • Write unittests.
  • Run the unittests and check that they’re passing.
  • Run the benchmarks to make sure your change doesn’t introduce a regression.

@CLAassistant
Copy link

CLAassistant commented May 20, 2024

CLA assistant check
All committers have signed the CLA.

@@ -416,6 +416,50 @@ def compilation_time(self) -> float | None:
"""
return self._compilation_time

def solve_multiple_solvers(self, solvers:list[tuple[str, dict] | tuple[str] | str] = None):
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is nice functionality! I don't think we need to support an argument of None, since anyone calling this function should be giving some list of solvers.

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for the review!
I'll remove the support for None.
What about an empty list? Currently, the implementation uses the solve method without arguments in this case.
Do you think I should keep it like that?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I would say that an empty list should raise a ValueError.

Copy link
Author

Choose a reason for hiding this comment

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

Thanks! Should the other errors related to input validation (such as inner input types) also raise ValueError?
Currently, I've used ParameterError.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, these are all ValueError. Let me sync a bit with the other devs though.

Copy link
Author

Choose a reason for hiding this comment

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

Thanks, please let me know if any further changes are needed :)

Copy link
Collaborator

@PTNobel PTNobel left a comment

Choose a reason for hiding this comment

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

Overall I'm positive on this but not a huge fan of the current public API. I left my suggestions. Aditionaly, I don't like solve_multiple_solvers, could we call the method solve_via_chain, solve_with_fallback, try_solve, or try_solve_till_success? I think Stephen Boyd often uses the term solver chain for this feature; I can ask him next week.

cvxpy/problems/problem.py Outdated Show resolved Hide resolved
cvxpy/problems/problem.py Outdated Show resolved Hide resolved
cvxpy/tests/test_problem.py Outdated Show resolved Hide resolved
cvxpy/problems/problem.py Outdated Show resolved Hide resolved
cvxpy/problems/problem.py Outdated Show resolved Hide resolved
cvxpy/problems/problem.py Outdated Show resolved Hide resolved
oriyalperin and others added 3 commits May 26, 2024 08:42
Apply suggestions from code review

Co-authored-by: Parth Nobel <parthnobel@berkeley.edu>
Co-authored-by: Parth Nobel <parthnobel@berkeley.edu>
@oriyalperin
Copy link
Author

@PTNobel thank you for all your suggestions!

Overall I'm positive on this but not a huge fan of the current public API. I left my suggestions. Aditionaly, I don't like solve_multiple_solvers, could we call the method solve_via_chain, solve_with_fallback, try_solve, or try_solve_till_success? I think Stephen Boyd often uses the term solver chain for this feature; I can ask him next week.

That would be great, thanks!

@PTNobel
Copy link
Collaborator

PTNobel commented Jun 3, 2024

Stephen said we should call it a "solver path" in the documentation; he also suggested making it a keyword argument to .solve instead of a new public function (maybe .solve(path=[...]). I'm wondering if maybe we could just specifialize on if the input is a string or a list to make .solve(solver=[cp.SCS, cp.CLARABEL]) the API. Thoughts on that @SteveDiamond @phschiele ?

@rileyjmurray
Copy link
Collaborator

rileyjmurray commented Jun 3, 2024 via email

@PTNobel
Copy link
Collaborator

PTNobel commented Jun 5, 2024

I’m not sure about “path” as a keyword argument. I’d be in favor of “solver_path” or “solver_priority” for keyword arguments to prob.solve.

Fair! I don't like solver_priority since we fallback to the other solvers instead of giving up, so I think solver_path would be a good choice.

@erelsgl
Copy link

erelsgl commented Jun 5, 2024

What if both "solver" and "solver_path" are given?

@PTNobel
Copy link
Collaborator

PTNobel commented Jun 5, 2024

I think that should raise a runtime error.

@rileyjmurray
Copy link
Collaborator

rileyjmurray commented Jun 5, 2024 via email

@SteveDiamond
Copy link
Collaborator

Stephen said we should call it a "solver path" in the documentation; he also suggested making it a keyword argument to .solve instead of a new public function (maybe .solve(path=[...]). I'm wondering if maybe we could just specifialize on if the input is a string or a list to make .solve(solver=[cp.SCS, cp.CLARABEL]) the API. Thoughts on that @SteveDiamond @phschiele ?

How would this work when you have solver specific arguments to the solve method?

@phschiele
Copy link
Collaborator

Stephen said we should call it a "solver path" in the documentation; he also suggested making it a keyword argument to .solve instead of a new public function (maybe .solve(path=[...]). I'm wondering if maybe we could just specifialize on if the input is a string or a list to make .solve(solver=[cp.SCS, cp.CLARABEL]) the API. Thoughts on that @SteveDiamond @phschiele ?

How would this work when you have solver specific arguments to the solve method?

Could be .solve(solver=[(cp.SCS, scs_args), (cp.CLARABEL, clarabel_args)]) but it might be getting complicated at this point.

@oriyalperin
Copy link
Author

I've made some changes according to your suggestions. Does this meet your expectations?

@PTNobel
Copy link
Collaborator

PTNobel commented Jun 10, 2024

I like this PR with Oriya's changes. Since this is a change to the public API, let's get maintainer consensus on this proposal before writing the new docs and merging.

"""

if not solvers:
raise ValueError("Solvers list must contain at least one solver.")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
raise ValueError("Solvers list must contain at least one solver.")
raise ValueError("Solver path must contain at least one solver.")

@@ -415,6 +415,58 @@ def compilation_time(self) -> float | None:
last time it was compiled.
"""
return self._compilation_time

def _solve_solver_path(self, solve_func, solvers:list[tuple[str, dict] | tuple[str] | str],
*args, **kwargs):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
*args, **kwargs):
args, kwargs):

if solver is not None:
raise ValueError(
"Cannot specify both 'solver' and 'solver_path'. Please choose one.")
return self._solve_solver_path(solve_func,solver_path,*args,**kwargs)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
return self._solve_solver_path(solve_func,solver_path,*args,**kwargs)
return self._solve_solver_path(solve_func,solver_path, args, kwargs)

solution = solve_func(
self, *args, solver=solver_name, **solver_kwargs, **kwargs)
else:
raise ValueError("Solver tuple input must be (str, dict) or (str)")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
raise ValueError("Solver tuple input must be (str, dict) or (str)")
raise ValueError("Solver path entry must be str, tuple[str] or tuple[str, dict[str, Any]]")

raise ValueError("Solver tuple input must be (str, dict) or (str)")
else:
raise ValueError("Solver input must be one of the following:\
(str, dict) tuple, (str) tuple, or str.")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you define a constant string somewhere and use the same message on line 461 and 463?

cp.sum_squares(cp.matmul(A, cp.Variable(40)) - b))).solve(
solver_path=solvers)

## solver_path ans solver arguments
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
## solver_path ans solver arguments
## Specify both solver_path and solver as arguments

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.

None yet

7 participants