From 34073edfbd06cd65c253d5cb735250d68f56b33d Mon Sep 17 00:00:00 2001 From: awccopp Date: Fri, 1 Nov 2024 16:35:31 -0400 Subject: [PATCH 01/14] parallel snstop --- pyoptsparse/pyOpt_optimizer.py | 20 +++++++++++------ pyoptsparse/pySNOPT/pySNOPT.py | 40 ++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/pyoptsparse/pyOpt_optimizer.py b/pyoptsparse/pyOpt_optimizer.py index 2ae6dca0..9f589f24 100644 --- a/pyoptsparse/pyOpt_optimizer.py +++ b/pyoptsparse/pyOpt_optimizer.py @@ -704,15 +704,21 @@ def _waitLoop(self): # 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: + # 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 + else: + raise Error("Wait loop recieved code %d must be -1 or 0"%mode) + - # 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) def _setInitialCacheValues(self): """ diff --git a/pyoptsparse/pySNOPT/pySNOPT.py b/pyoptsparse/pySNOPT/pySNOPT.py index 5354f78e..03e0d697 100644 --- a/pyoptsparse/pySNOPT/pySNOPT.py +++ b/pyoptsparse/pySNOPT/pySNOPT.py @@ -536,6 +536,43 @@ def __call__( 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""" + + 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) + 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. @@ -703,6 +740,9 @@ def _snstop(self, ktcond, mjrprtlvl, minimize, n, nncon, nnobj, ns, itn, nmajor, if not self.storeHistory: raise Error("snSTOP function handle must be used with storeHistory=True") + + 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: From 62041ddffd70754f547a4868b6999423795ce899 Mon Sep 17 00:00:00 2001 From: Marco Mangano Date: Wed, 27 Nov 2024 16:00:18 -0600 Subject: [PATCH 02/14] formatting fixes --- pyoptsparse/pyOpt_optimizer.py | 6 ++---- pyoptsparse/pySNOPT/pySNOPT.py | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/pyoptsparse/pyOpt_optimizer.py b/pyoptsparse/pyOpt_optimizer.py index 9f589f24..4816f6f2 100644 --- a/pyoptsparse/pyOpt_optimizer.py +++ b/pyoptsparse/pyOpt_optimizer.py @@ -704,7 +704,7 @@ def _waitLoop(self): # 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 + # 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) @@ -716,9 +716,7 @@ def _waitLoop(self): elif mode == -1: break else: - raise Error("Wait loop recieved code %d must be -1 or 0"%mode) - - + raise Error("Wait loop recieved code %d must be -1 or 0" % mode) def _setInitialCacheValues(self): """ diff --git a/pyoptsparse/pySNOPT/pySNOPT.py b/pyoptsparse/pySNOPT/pySNOPT.py index 03e0d697..65847524 100644 --- a/pyoptsparse/pySNOPT/pySNOPT.py +++ b/pyoptsparse/pySNOPT/pySNOPT.py @@ -569,9 +569,9 @@ def _waitLoop(self): # Get function handle and make call snstop_handle = self.getOption("snSTOP function handle") if snstop_handle is not None: - snstop_handle(*info) + snstop_handle(*info) else: - raise Error("Wait loop recieved code %d must be -1, 0, or 1 "%mode) + raise Error("Wait loop recieved code %d must be -1, 0, or 1 " % mode) def _userfg_wrap(self, mode, nnJac, x, fobj, gobj, fcon, gcon, nState, cu, iu, ru): """ From b028be45b11d7f7eeff69e727510cba8ed9e43e5 Mon Sep 17 00:00:00 2001 From: Marco Mangano Date: Wed, 27 Nov 2024 16:42:10 -0600 Subject: [PATCH 03/14] added comments --- pyoptsparse/pySNOPT/pySNOPT.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyoptsparse/pySNOPT/pySNOPT.py b/pyoptsparse/pySNOPT/pySNOPT.py index 65847524..8b95b21e 100644 --- a/pyoptsparse/pySNOPT/pySNOPT.py +++ b/pyoptsparse/pySNOPT/pySNOPT.py @@ -538,7 +538,10 @@ def __call__( def _waitLoop(self): """Non-root processors go into this waiting loop while the - root proc does all the work in the optimization algorithm""" + 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 @@ -741,6 +744,7 @@ def _snstop(self, ktcond, mjrprtlvl, minimize, n, nncon, nnobj, ns, itn, nmajor, 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) From 95b7150d4aa446e1bc94e250b8e02b47033fcf62 Mon Sep 17 00:00:00 2001 From: Marco Mangano Date: Mon, 16 Dec 2024 16:55:28 -0500 Subject: [PATCH 04/14] added test - does it make sense? --- tests/test_hs015_parallel.py | 142 +++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 tests/test_hs015_parallel.py diff --git a/tests/test_hs015_parallel.py b/tests/test_hs015_parallel.py new file mode 100644 index 00000000..5cf33836 --- /dev/null +++ b/tests/test_hs015_parallel.py @@ -0,0 +1,142 @@ +"""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 + from mpi4py import MPI + + # import sabani +except ImportError: + HAS_MPI = False + +# First party modules +from pyoptsparse import Optimization + +# Local modules +from testing_utils import OptTest + + +comm = MPI.COMM_WORLD +rank = comm.Get_rank() +size = comm.Get_size() + + +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. + ## + + if not HAS_MPI: + raise unittest.SkipTest("MPI not available") + + 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""" + # print("Iteration", iterDict["nMajor"]) + # print("Rank", rank) + return_idx = 0 + if rank == 1 and iterDict["nMajor"] == 1: + return_idx = comm.bcast(1, root=1) + return_idx = comm.bcast(return_idx, root=1) + # comm.Barrier() + # print(f"return_iDX on {rank}", return_idx) + 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() From b982ef4206d7f83779a68bf42c31a0d236d0cbbe Mon Sep 17 00:00:00 2001 From: Marco Mangano Date: Tue, 17 Dec 2024 10:14:02 -0500 Subject: [PATCH 05/14] fixed MPI check --- tests/test_hs015_parallel.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/test_hs015_parallel.py b/tests/test_hs015_parallel.py index 5cf33836..58c9897d 100644 --- a/tests/test_hs015_parallel.py +++ b/tests/test_hs015_parallel.py @@ -21,11 +21,6 @@ from testing_utils import OptTest -comm = MPI.COMM_WORLD -rank = comm.Get_rank() -size = comm.Get_size() - - class TestHS15(OptTest): ## Solve test problem HS15 from the Hock & Schittkowski collection. # @@ -43,6 +38,10 @@ class TestHS15(OptTest): if not HAS_MPI: raise unittest.SkipTest("MPI not available") + comm = MPI.COMM_WORLD + rank = comm.Get_rank() + size = comm.Get_size() + N_PROCS = 2 # Run case on two procs name = "HS015" From c5d3cf656c6ab7aef779b82355f8927215b33de6 Mon Sep 17 00:00:00 2001 From: Marco Mangano Date: Tue, 17 Dec 2024 10:17:42 -0500 Subject: [PATCH 06/14] actually fixing MPI check --- tests/test_hs015_parallel.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/test_hs015_parallel.py b/tests/test_hs015_parallel.py index 58c9897d..4323ca86 100644 --- a/tests/test_hs015_parallel.py +++ b/tests/test_hs015_parallel.py @@ -13,6 +13,7 @@ # import sabani except ImportError: HAS_MPI = False + raise unittest.SkipTest("MPI not available") # First party modules from pyoptsparse import Optimization @@ -21,6 +22,11 @@ from testing_utils import OptTest +comm = MPI.COMM_WORLD +rank = comm.Get_rank() +size = comm.Get_size() + + class TestHS15(OptTest): ## Solve test problem HS15 from the Hock & Schittkowski collection. # @@ -35,13 +41,6 @@ class TestHS15(OptTest): # at (-0.79212, -1.26243), with final objective = 360.4. ## - if not HAS_MPI: - raise unittest.SkipTest("MPI not available") - - comm = MPI.COMM_WORLD - rank = comm.Get_rank() - size = comm.Get_size() - N_PROCS = 2 # Run case on two procs name = "HS015" From 513a4c8c004f8ea4eea61d017fd770fda499dcdd Mon Sep 17 00:00:00 2001 From: Marco Mangano Date: Tue, 17 Dec 2024 10:22:21 -0500 Subject: [PATCH 07/14] iSort fix --- tests/test_hs015_parallel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_hs015_parallel.py b/tests/test_hs015_parallel.py index 4323ca86..900eea88 100644 --- a/tests/test_hs015_parallel.py +++ b/tests/test_hs015_parallel.py @@ -8,6 +8,7 @@ try: HAS_MPI = True + # External modules from mpi4py import MPI # import sabani @@ -21,7 +22,6 @@ # Local modules from testing_utils import OptTest - comm = MPI.COMM_WORLD rank = comm.Get_rank() size = comm.Get_size() From 92777afed13665ee362d6ebfb2f45102594acc3e Mon Sep 17 00:00:00 2001 From: Marco Mangano Date: Tue, 17 Dec 2024 10:30:29 -0500 Subject: [PATCH 08/14] maybe this time the test will be skipped? --- tests/test_hs015_parallel.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_hs015_parallel.py b/tests/test_hs015_parallel.py index 900eea88..824e3a28 100644 --- a/tests/test_hs015_parallel.py +++ b/tests/test_hs015_parallel.py @@ -10,11 +10,8 @@ HAS_MPI = True # External modules from mpi4py import MPI - - # import sabani except ImportError: HAS_MPI = False - raise unittest.SkipTest("MPI not available") # First party modules from pyoptsparse import Optimization @@ -22,6 +19,9 @@ # Local modules from testing_utils import OptTest +if not HAS_MPI: + raise unittest.SkipTest("MPI not available") + comm = MPI.COMM_WORLD rank = comm.Get_rank() size = comm.Get_size() From 0395130cdf7b05f6400440a577c381597a9960d5 Mon Sep 17 00:00:00 2001 From: Marco Mangano Date: Tue, 17 Dec 2024 10:38:48 -0500 Subject: [PATCH 09/14] maybe like this? --- tests/test_hs015_parallel.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_hs015_parallel.py b/tests/test_hs015_parallel.py index 824e3a28..f2fcad6c 100644 --- a/tests/test_hs015_parallel.py +++ b/tests/test_hs015_parallel.py @@ -10,6 +10,10 @@ HAS_MPI = True # External modules from mpi4py import MPI + + comm = MPI.COMM_WORLD + rank = comm.Get_rank() + size = comm.Get_size() except ImportError: HAS_MPI = False @@ -19,13 +23,6 @@ # Local modules from testing_utils import OptTest -if not HAS_MPI: - raise unittest.SkipTest("MPI not available") - -comm = MPI.COMM_WORLD -rank = comm.Get_rank() -size = comm.Get_size() - class TestHS15(OptTest): ## Solve test problem HS15 from the Hock & Schittkowski collection. @@ -41,6 +38,9 @@ class TestHS15(OptTest): # at (-0.79212, -1.26243), with final objective = 360.4. ## + if not HAS_MPI: + raise unittest.SkipTest("MPI not available") + N_PROCS = 2 # Run case on two procs name = "HS015" From 78d8fd532caeb44f6e239dd454bf66506273fe0c Mon Sep 17 00:00:00 2001 From: Marco Mangano Date: Tue, 17 Dec 2024 11:24:21 -0500 Subject: [PATCH 10/14] what about this --- tests/test_hs015_parallel.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_hs015_parallel.py b/tests/test_hs015_parallel.py index f2fcad6c..c9a2c6f1 100644 --- a/tests/test_hs015_parallel.py +++ b/tests/test_hs015_parallel.py @@ -24,6 +24,7 @@ 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. # @@ -38,8 +39,8 @@ class TestHS15(OptTest): # at (-0.79212, -1.26243), with final objective = 360.4. ## - if not HAS_MPI: - raise unittest.SkipTest("MPI not available") + # if not HAS_MPI: + # raise unittest.SkipTest("MPI not available") N_PROCS = 2 # Run case on two procs From b17012b7d6a49bfb93400fb7a0e2bc18e4068cbd Mon Sep 17 00:00:00 2001 From: Marco Mangano Date: Tue, 17 Dec 2024 14:12:28 -0500 Subject: [PATCH 11/14] cleanup --- tests/test_hs015_parallel.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/test_hs015_parallel.py b/tests/test_hs015_parallel.py index c9a2c6f1..5fb426c5 100644 --- a/tests/test_hs015_parallel.py +++ b/tests/test_hs015_parallel.py @@ -11,6 +11,7 @@ # External modules from mpi4py import MPI + # Setting up MPI communicators comm = MPI.COMM_WORLD rank = comm.Get_rank() size = comm.Get_size() @@ -39,9 +40,6 @@ class TestHS15(OptTest): # at (-0.79212, -1.26243), with final objective = 360.4. ## - # if not HAS_MPI: - # raise unittest.SkipTest("MPI not available") - N_PROCS = 2 # Run case on two procs name = "HS015" @@ -106,14 +104,12 @@ def setup_optProb(self): @staticmethod def my_snstop(iterDict): """manually terminate SNOPT after 1 major iteration if""" - # print("Iteration", iterDict["nMajor"]) - # print("Rank", rank) + return_idx = 0 if rank == 1 and iterDict["nMajor"] == 1: return_idx = comm.bcast(1, root=1) return_idx = comm.bcast(return_idx, root=1) - # comm.Barrier() - # print(f"return_iDX on {rank}", return_idx) + return return_idx def test_optimization(self): From 20f503a39a5b8f83be5042cfe70013462fbd7ac4 Mon Sep 17 00:00:00 2001 From: Marco Mangano Date: Tue, 17 Dec 2024 14:24:53 -0500 Subject: [PATCH 12/14] add timeout option to testflo --- .github/test_real.sh | 2 +- .github/windows.yaml | 2 +- .github/workflows/windows-build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/test_real.sh b/.github/test_real.sh index 4af0258e..d4a79e72 100755 --- a/.github/test_real.sh +++ b/.github/test_real.sh @@ -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 diff --git a/.github/windows.yaml b/.github/windows.yaml index 4f600ccf..69c6ebf1 100644 --- a/.github/windows.yaml +++ b/.github/windows.yaml @@ -37,5 +37,5 @@ jobs: - script: | cd tests - testflo -n 1 . + testflo -n 1 --timeout 60 . displayName: Run tests diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index ecca3662..21614a77 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -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 . From 9b444721ef0bd0627aa52b432c44847c716f8d9b Mon Sep 17 00:00:00 2001 From: Marco Mangano Date: Tue, 17 Dec 2024 14:29:20 -0500 Subject: [PATCH 13/14] rerun tests --- tests/test_hs015_parallel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_hs015_parallel.py b/tests/test_hs015_parallel.py index 5fb426c5..a6fea26d 100644 --- a/tests/test_hs015_parallel.py +++ b/tests/test_hs015_parallel.py @@ -15,6 +15,7 @@ comm = MPI.COMM_WORLD rank = comm.Get_rank() size = comm.Get_size() + except ImportError: HAS_MPI = False From 30ddea31553a72e5ce2d46f262b00ed683485bf2 Mon Sep 17 00:00:00 2001 From: Marco Mangano Date: Tue, 17 Dec 2024 14:52:46 -0500 Subject: [PATCH 14/14] updated test with send/receive --- tests/test_hs015_parallel.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_hs015_parallel.py b/tests/test_hs015_parallel.py index a6fea26d..58732cc6 100644 --- a/tests/test_hs015_parallel.py +++ b/tests/test_hs015_parallel.py @@ -107,10 +107,11 @@ def my_snstop(iterDict): """manually terminate SNOPT after 1 major iteration if""" return_idx = 0 - if rank == 1 and iterDict["nMajor"] == 1: - return_idx = comm.bcast(1, root=1) - return_idx = comm.bcast(return_idx, root=1) - + 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):