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

Added restartDict to snstop #404

Merged
merged 21 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions doc/optimizers/SNOPT_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,37 @@ Return work arrays:
These arrays can be used to hot start a subsequent optimization.
The SNOPT option 'Sticky parameters' will also be automatically set to 'Yes' to facilitate the hot start.

Work arrays save file:
desc: >
This option is unique to the Python wrapper.
The SNOPT work arrays will be pickled and saved to this file after each major iteration.
This file is useful if you want to restart an optimization that did not exit cleanly.
If None, the work arrays are not saved.

snSTOP function handle:
desc: >
This option is unique to the Python wrapper.
A function handle can be supplied which is called at the end of each major iteration.
The following is an example of a callback function that saves the restart dictionary
to a different file after each major iteration.

.. code-block:: python

def snstopCallback(iterDict, restartDict):
# Get the major iteration number
nMajor = iterDict["nMajor"]

# Save the restart dictionary
writePickle(f"restart_{nMajor}.pickle", restartDict)

return 0

snSTOP arguments:
desc: |
This option is unique to the Python wrapper.
It specifies a list of arguments that will be passed to the snSTOP function handle.
``iterDict`` is always passed as an argument.
Additional arguments are passed in the same order as this list.
The possible values are

- ``restartDict``
35 changes: 33 additions & 2 deletions pyoptsparse/pySNOPT/pySNOPT.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from typing import Any, Dict, Optional, Tuple

# External modules
from baseclasses.utils import CaseInsensitiveSet
from baseclasses.utils import CaseInsensitiveSet, writePickle

Check warning on line 15 in pyoptsparse/pySNOPT/pySNOPT.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pySNOPT/pySNOPT.py#L15

Added line #L15 was not covered by tests
import numpy as np
from numpy import ndarray
from pkg_resources import parse_version
Expand Down Expand Up @@ -60,7 +60,9 @@
{
"Save major iteration variables",
"Return work arrays",
"Work arrays save file",
"snSTOP function handle",
"snSTOP arguments",
}
)

Expand Down Expand Up @@ -118,7 +120,9 @@
"Total real workspace": [int, None],
"Save major iteration variables": [list, []],
"Return work arrays": [bool, False],
"Work arrays save file": [(type(None), str), None],
"snSTOP function handle": [(type(None), type(lambda: None)), None],
"snSTOP arguments": [list, []],
}
return defOpts

Expand Down Expand Up @@ -667,12 +671,39 @@
if "funcs" in self.cache.keys():
iterDict["funcs"].update(self.cache["funcs"])

# Create the restart dictionary to be passed to snstop_handle
restartDict = {
Copy link
Collaborator

Choose a reason for hiding this comment

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

TODO for the future: this should probably be a dataclass object instead of a dict... That way we can avoid some code duplication with above.

Choose a reason for hiding this comment

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

@ewu63 I can do it if you open an issue with the specification of what you would like to have ;-)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Opened #405

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for the comments. It would be nice for the function to be more flexible. I'm not sure how returning values would work though because the user does not call _snstop. It seems like the dictionaries have to be passed in.

"cw": cw,
"iw": iw,
"rw": rw,
"xs": x, # x is the same as xs; we call it x here to be consistent with the SNOPT subroutine snSTOP
"hs": hs,
"pi": pi,
}

workArraysSave = self.getOption("Work arrays save file")
if workArraysSave is not None:
# Save the restart dictionary
writePickle(workArraysSave, restartDict)

# perform callback if requested
snstop_handle = self.getOption("snSTOP function handle")
if snstop_handle is not None:

# Get the arguments to pass in to snstop_handle
# iterDict is always included
snstopArgs = [iterDict]
for snstopArg in self.getOption("snSTOP arguments"):
if snstopArg == "restartDict":
snstopArgs.append(restartDict)
else:
raise Error(f"Received unknown snSTOP argument {snstopArg}. "

Check warning on line 700 in pyoptsparse/pySNOPT/pySNOPT.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pySNOPT/pySNOPT.py#L700

Added line #L700 was not covered by tests
+ "Please see 'snSTOP arguments' option in the pyOptSparse documentation "
+ "under 'SNOPT'.")

if not self.storeHistory:
raise Error("snSTOP function handle must be used with storeHistory=True")
iabort = snstop_handle(iterDict)
iabort = snstop_handle(*snstopArgs)
# write iterDict again if anything was inserted
if self.storeHistory and callCounter is not None:
self.hist.write(callCounter, iterDict)
Expand Down
86 changes: 86 additions & 0 deletions tests/test_hs015.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
"""Test solution of problem HS15 from the Hock & Schittkowski collection"""

# Standard Python modules
import os
import unittest

# External modules
from baseclasses.utils import readPickle, writePickle
import numpy as np
from parameterized import parameterized

Expand Down Expand Up @@ -193,6 +195,90 @@ def test_snopt_snstop(self):
# we should get 70/74
self.assert_inform_equal(sol, optInform=74)

def test_snopt_snstop_restart(self):
pickleFile = "restart.pickle"

def my_snstop_restart(iterDict, restartDict):
# Save the restart dictionary
writePickle(pickleFile, restartDict)

# Exit after 5 major iterations
if iterDict["nMajor"] == 5:
return 1

return 0

# Run the optimization for 5 major iterations
self.optName = "SNOPT"
self.setup_optProb()
optOptions = {
"snSTOP function handle": my_snstop_restart,
"snSTOP arguments": ["restartDict"],
}
sol = self.optimize(optOptions=optOptions, storeHistory=True)
ewu63 marked this conversation as resolved.
Show resolved Hide resolved

# Check that the optimization exited with 74
self.assert_inform_equal(sol, optInform=74)

# Read the restart dictionary pickle file saved by snstop
restartDict = readPickle(pickleFile)

# Now optimize again but using the restart dictionary
self.setup_optProb()
opt = OPT(
marcomangano marked this conversation as resolved.
Show resolved Hide resolved
self.optName,
options={
"Start": "Hot",
"Verify level": -1,
"snSTOP function handle": my_snstop_restart,
"snSTOP arguments": ["restartDict"],
},
)
histFile = "restart.hst"
sol = opt(self.optProb, sens=self.sens, storeHistory=histFile, restartDict=restartDict)

# Check that the optimization converged in fewer than 5 more major iterations
self.assert_solution_allclose(sol, 1e-12)
self.assert_inform_equal(sol, optInform=1)

# Delete the pickle and history files
os.remove(pickleFile)
os.remove(histFile)

def test_snopt_work_arrays_save(self):
# Run the optimization for 5 major iterations
self.optName = "SNOPT"
self.setup_optProb()
pickleFile = "work_arrays_save.pickle"
optOptions = {
"snSTOP function handle": self.my_snstop,
"Work arrays save file": pickleFile,
}
sol = self.optimize(optOptions=optOptions, storeHistory=True)

# Read the restart dictionary pickle file saved by snstop
restartDict = readPickle(pickleFile)

# Now optimize again but using the restart dictionary
self.setup_optProb()
opt = OPT(
self.optName,
options={
"Start": "Hot",
"Verify level": -1,
},
)
histFile = "work_arrays_save.hst"
sol = opt(self.optProb, sens=self.sens, storeHistory=histFile, restartDict=restartDict)

# Check that the optimization converged in fewer than 5 more major iterations
marcomangano marked this conversation as resolved.
Show resolved Hide resolved
self.assert_solution_allclose(sol, 1e-12)
self.assert_inform_equal(sol, optInform=1)

# Delete the pickle and history files
os.remove(pickleFile)
os.remove(histFile)

def test_snopt_failed_initial(self):
def failed_fun(x_dict):
funcs = {"obj": 0.0, "con": [np.nan, np.nan]}
Expand Down
Loading