RobotLocomotion / drake

Model-based design and verification for robotics.
https://drake.mit.edu
Other
3.16k stars 1.24k forks source link

SNOPT misapprehends pseudo-valid option names like "Max iterations" #21551

Open sea-bass opened 3 weeks ago

sea-bass commented 3 weeks ago

Edit: Original title was: Changing SNOPT "Max iterations" property seems to invalidate cost minimization.


What happened?

I'm running into a weird issue where I have a trajectory optimization problem that fails pretty frequently.

It succeeds a lot more if I turn up the maximum SNOPT iterations as follows:

solver_options = SolverOptions()    
solver_options.SetOption(SnoptSolver.id(), "Max iterations", 10000)
Solve(prog, solver_options=solver_options)

... however, when I change this option (doesn't matter what value, so long as I set it) this it seems to completely invalidate my one cost function (in my case, quadratic cost on the trajectory time), and always returns the maximum allowable time limit I specify.

The fact that the number I set seems to be less important than setting the option itself indicates to me that, somehow, setting this option turns the problem from an objective function minimization to a "Feasible solution" solve.

I can put together a repro case if needed, but it will take me some time to strip down my code, so I was first wondering if there was something known here?

Version

1.29.0

What operating system are you using?

Ubuntu 24.04

What installation option are you using?

pip install drake

Relevant log output

No response

sea-bass commented 3 weeks ago

A non-reduced repro case I can provide right away:

Clone this branch of this repo: https://github.com/sea-bass/pyroboplan/tree/traj-opt-collision

git clone -b traj-opt-collision https://github.com/sea-bass/pyroboplan.git

Go into the repo root and run

pip install -e .

Run the example:

python3 examples/example_optimize_rrt_path.py

Notice that you will get quite a few optimization failures, but when there is a successful trajectory, this get right up to the max velocity/acceleration/jerk limits specified.

Now go into this line and uncomment it: https://github.com/sea-bass/pyroboplan/blob/0cddcac468b5ce35e617ed850cd461874b3f629c/src/pyroboplan/trajectory/trajectory_optimization.py#L611-L612

Trying again, you will see more successes, but for some reason the cost of minimizing the trajectory time (or segment time) is not minimized at all, and is always the upper bound.

jwnimmer-tri commented 3 weeks ago

FYI Sometimes when it seems like a nonlinear solver is misbehaving, the fault actually lies with the mathematical program's setup being either ill-formed, or too stiff -- rather than a problem with the solver. Hopefully a smaller reproducer will help either find such a fault, or rule it out.

sea-bass commented 3 weeks ago

I just got a REALLY simple repro. Check this out!

import numpy as np

from pydrake.solvers import MathematicalProgram, SolverOptions, SnoptSolver

prog = MathematicalProgram()
x = prog.NewContinuousVariables(1)

# Limit the variable, but also minimize it
prog.AddBoundingBoxConstraint(0.01, 10.0, x)
prog.AddQuadraticCost(Q=np.eye(1), b=np.zeros(1), vars=x)

# Solve the program.
solver = SnoptSolver()
solver_options = SolverOptions()
# solver_options.SetOption(SnoptSolver.id(), "Max iterations", 1000)  # Uncomment this!
result = solver.Solve(prog, solver_options=solver_options)

x_opt = result.GetSolution(x)
print(f"Solved with solver: {result.get_solver_id().name()}")
print(f"Optimal value: {x_opt}")

With the line commented out, I get an optimal value of 0.01 (minimum value possible), but if I uncomment it, the result is 10.0 (the maximum).

EDIT: I then added this line

solver_options.SetOption(SnoptSolver.id(), "Minimize", "1")  # Restore sanity in the world

and it again fixes the solution to return 0.01.

jwnimmer-tri commented 3 weeks ago

About option names, the SNOPT manual says "Some of the keywords have synonyms, and certain abbreviations are allowed, as long as there is no ambiguity.".

I think what is probably happening is that it's mapping "Max iterations" (which is not a thing that exists in SNOPT land, as far as I can tell) to "Maximize", and solving the max cost instead of min cost program.

jwnimmer-tri commented 3 weeks ago

So, I think the bug here is that SNOPT itself fails to reject typos in its option names.

Either we document that trap in the class SnoptSolver overview to help warn future users, or else we add to Drake's snopt_solver.cc a manually-collected listing of all ~80 known SNOPT option names, and warn in case the user passes a name that isn't in the hard-coded list.

jwnimmer-tri commented 3 weeks ago

Any votes on the solution here +@hongkai-dai?

sea-bass commented 3 weeks ago

Interesting! The latter option seems "right", but it will also be very annoying to keep that up to date every time the SNOPT version used in Drake is updated.

jwnimmer-tri commented 3 weeks ago

... annoying to keep that up to date every time the SNOPT version used in Drake is updated.

I had the same initial reaction, but in practice updates won't be a problem. Versions of SNOPT newer than 7.4 have enough severe regressions (for robotics problems) that we stopped upgrading more than 5 years ago. (See #10430.)

So the only real work is building the list for SNOPT 7.4 and SNOPT 7.6 (which are the only two versions we support).

hongkai-dai commented 3 weeks ago

BTW, to set the iterations limit SNOPT, you should use SetSolverOptions(SnoptSolver::id(), "major iterations limit", ITERATION_LIMIT) for the number of QPs, and `SetSolverOptions(SnoptSolver::id(), "minor iterations limit" for the total number of steps inside QPs.