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

parallel snstop #420

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .github/test_real.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ cd tests
# we have to copy over the coveragerc file to make sure it's in the
# same directory where codecov is run
cp ../.coveragerc .
testflo --pre_announce --disallow_deprecations -v --coverage --coverpkg pyoptsparse $EXTRA_FLAGS
testflo --pre_announce --disallow_deprecations -v --coverage --coverpkg pyoptsparse $EXTRA_FLAGS --timeout 60
2 changes: 1 addition & 1 deletion .github/windows.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,5 @@ jobs:

- script: |
cd tests
testflo -n 1 .
testflo -n 1 --timeout 60 .
displayName: Run tests
2 changes: 1 addition & 1 deletion .github/workflows/windows-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ jobs:
run: |
conda activate pyos-build
cd tests
testflo --pre_announce -v -n 1 .
testflo --pre_announce -v -n 1 --timeout 60 .
20 changes: 12 additions & 8 deletions pyoptsparse/pyOpt_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -704,15 +704,19 @@

# Receive mode and quit if mode is -1:
mode = self.optProb.comm.bcast(mode, root=0)
if mode == -1:
# mode = 0 call masterfunc2 as broadcast by root in masterfunc
if mode == 0:

Check warning on line 708 in pyoptsparse/pyOpt_optimizer.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pyOpt_optimizer.py#L708

Added line #L708 was not covered by tests
# Receive info from shell function
info = self.optProb.comm.bcast(info, root=0)

Check warning on line 710 in pyoptsparse/pyOpt_optimizer.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pyOpt_optimizer.py#L710

Added line #L710 was not covered by tests

# Call the generic internal function. We don't care
# about return values on these procs
self._masterFunc2(*info)

Check warning on line 714 in pyoptsparse/pyOpt_optimizer.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pyOpt_optimizer.py#L714

Added line #L714 was not covered by tests
# mode = -1 exit wait loop
elif mode == -1:

Check warning on line 716 in pyoptsparse/pyOpt_optimizer.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pyOpt_optimizer.py#L716

Added line #L716 was not covered by tests
break

# Otherwise receive info from shell function
info = self.optProb.comm.bcast(info, root=0)

# Call the generic internal function. We don't care
# about return values on these procs
self._masterFunc2(*info)
else:
raise Error("Wait loop recieved code %d must be -1 or 0" % mode)

Check warning on line 719 in pyoptsparse/pyOpt_optimizer.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pyOpt_optimizer.py#L719

Added line #L719 was not covered by tests

def _setInitialCacheValues(self):
"""
Expand Down
44 changes: 44 additions & 0 deletions pyoptsparse/pySNOPT/pySNOPT.py
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,46 @@
else:
return commSol

def _waitLoop(self):
"""Non-root processors go into this waiting loop while the
root proc does all the work in the optimization algorithm

This function overwrites the namesake in the Optimizer class to add a new mode enabling parallel snstop function
"""

mode = None
info = None
while True:
# * Note*: No checks for MPI here since this code is
# * only run in parallel, which assumes mpi4py is working

# Receive mode and quit if mode is -1:
mode = self.optProb.comm.bcast(mode, root=0)

# mode = 0 call masterfunc2 as broadcast by root in masterfunc
if mode == 0:
# Receive info from shell function
info = self.optProb.comm.bcast(info, root=0)

# Call the generic internal function. We don't care
# about return values on these procs
self._masterFunc2(*info)

# mode = -1 exit wait loop
elif mode == -1:
break

# mode = 1 call user snSTOP function
elif mode == 1:
# Receive function arguments from root
info = self.optProb.comm.bcast(info, root=0)
# Get function handle and make call
snstop_handle = self.getOption("snSTOP function handle")
if snstop_handle is not None:
snstop_handle(*info)
else:
raise Error("Wait loop recieved code %d must be -1, 0, or 1 " % mode)

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

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pySNOPT/pySNOPT.py#L577

Added line #L577 was not covered by tests

def _userfg_wrap(self, mode, nnJac, x, fobj, gobj, fcon, gcon, nState, cu, iu, ru):
"""
The snopt user function. This is what is actually called from snopt.
Expand Down Expand Up @@ -703,6 +743,10 @@

if not self.storeHistory:
raise Error("snSTOP function handle must be used with storeHistory=True")

# Broadcasting flag to call user snstop function
self.optProb.comm.bcast(1, root=0)
self.optProb.comm.bcast(snstopArgs, root=0)
iabort = snstop_handle(*snstopArgs)
# write iterDict again if anything was inserted
if self.storeHistory and callCounter is not None:
Expand Down
139 changes: 139 additions & 0 deletions tests/test_hs015_parallel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"""Test solution of problem HS15 from the Hock & Schittkowski collection"""

# Standard Python modules
import unittest

# External modules
import numpy as np

try:
HAS_MPI = True
# External modules
from mpi4py import MPI

# Setting up MPI communicators
comm = MPI.COMM_WORLD
rank = comm.Get_rank()
size = comm.Get_size()

except ImportError:
HAS_MPI = False

# First party modules
from pyoptsparse import Optimization

# Local modules
from testing_utils import OptTest


@unittest.skipIf(not HAS_MPI, "MPI not available")
class TestHS15(OptTest):
## Solve test problem HS15 from the Hock & Schittkowski collection.
#
# min 100 (x2 - x1^2)^2 + (1 - x1)^2
# s.t. x1 x2 >= 1
# x1 + x2^2 >= 0
# x1 <= 0.5
#
# The standard start point (-2, 1) usually converges to the standard
# minimum at (0.5, 2.0), with final objective = 306.5.
# Sometimes the solver converges to another local minimum
# at (-0.79212, -1.26243), with final objective = 360.4.
##

N_PROCS = 2 # Run case on two procs

name = "HS015"
DVs = {"xvars"}
cons = {"con"}
objs = {"obj"}
extras = {"extra1", "extra2"}
fStar = [
306.5,
360.379767,
]
xStar = [
{"xvars": (0.5, 2.0)},
{"xvars": (-0.79212322, -1.26242985)},
]
optOptions = {}

def objfunc(self, xdict):
self.nf += 1
x = xdict["xvars"]
funcs = {}
funcs["obj"] = [100 * (x[1] - x[0] ** 2) ** 2 + (1 - x[0]) ** 2]
conval = np.zeros(2, "D")
conval[0] = x[0] * x[1]
conval[1] = x[0] + x[1] ** 2
funcs["con"] = conval
# extra keys
funcs["extra1"] = 0.0
funcs["extra2"] = 1.0
fail = False
return funcs, fail

def sens(self, xdict, funcs):
self.ng += 1
x = xdict["xvars"]
funcsSens = {}
funcsSens["obj"] = {
"xvars": [2 * 100 * (x[1] - x[0] ** 2) * (-2 * x[0]) - 2 * (1 - x[0]), 2 * 100 * (x[1] - x[0] ** 2)]
}
funcsSens["con"] = {"xvars": [[x[1], x[0]], [1, 2 * x[1]]]}
fail = False
return funcsSens, fail

def setup_optProb(self):
# Optimization Object
self.optProb = Optimization("HS15 Constraint Problem", self.objfunc)

# Design Variables
lower = [-5.0, -5.0]
upper = [0.5, 5.0]
value = [-2, 1.0]
self.optProb.addVarGroup("xvars", 2, lower=lower, upper=upper, value=value)

# Constraints
lower = [1.0, 0.0]
upper = [None, None]
self.optProb.addConGroup("con", 2, lower=lower, upper=upper)

# Objective
self.optProb.addObj("obj")

@staticmethod
def my_snstop(iterDict):
"""manually terminate SNOPT after 1 major iteration if"""

return_idx = 0
if iterDict["nMajor"] == 1:
if comm.rank == 1:
comm.send(1, dest=0, tag=comm.rank)
elif comm.rank == 0:
return_idx = comm.recv(source=1)
return return_idx

def test_optimization(self):
self.optName = "SNOPT"
self.setup_optProb()
sol = self.optimize()
# Check Solution
self.assert_solution_allclose(sol, 1e-12)
# Check informs
self.assert_inform_equal(sol)

def test_snopt_snstop(self):
self.optName = "SNOPT"
self.setup_optProb()
optOptions = {
"snSTOP function handle": self.my_snstop,
}
sol = self.optimize(optOptions=optOptions, storeHistory=True)
# Check informs
# we should get 70/74
self.assert_inform_equal(sol, optInform=74)


if __name__ == "__main__":
unittest.main()
Loading