From 885b72da1751775c9b63433ab450f9efda0414bb Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Mon, 6 Oct 2025 18:06:25 -0400 Subject: [PATCH 01/10] Add helpful error message if `nom_getDVGeo` is called before `setup` --- pygeo/mphys/mphys_dvgeo.py | 6 ++++++ tests/reg_tests/test_MPhysGeo.py | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/pygeo/mphys/mphys_dvgeo.py b/pygeo/mphys/mphys_dvgeo.py index 76eba4ac..b0079e81 100644 --- a/pygeo/mphys/mphys_dvgeo.py +++ b/pygeo/mphys/mphys_dvgeo.py @@ -269,6 +269,12 @@ def nom_getDVGeo(self, childName=None, comp=None, DVGeoName=None): DVGeometry object DVGeometry object held by this geometry component """ + # Calling this function before setup is not allowed because the DVGeo object(s) do not exist yet + if not hasattr(self, "DVGeos"): + raise RuntimeError( + "Cannot call `nom_getDVGeo` before OM_DVGEOCOMP's `setup` method has been called. If you are calling this function in the `setup` method of a group containing an OM_DVGEOCOMP, move the call to `configure` instead." + ) from None + # if we have multiple DVGeos use the one specified by name if self.multDVGeo: DVGeo = self.DVGeos[DVGeoName] diff --git a/tests/reg_tests/test_MPhysGeo.py b/tests/reg_tests/test_MPhysGeo.py index 49faf76c..87dca7c8 100644 --- a/tests/reg_tests/test_MPhysGeo.py +++ b/tests/reg_tests/test_MPhysGeo.py @@ -562,5 +562,20 @@ def test_deriv_rev(self): commonUtils.assert_check_totals(totals, atol=1e-5, rtol=1e-5) +@unittest.skipUnless(omInstalled, "OpenMDAO is required to test the pyGeo MPhys wrapper") +class TestGetDVGeoError(unittest.TestCase): + # Make sure we get an error if we try to call nom_getDVGeo before setup + def test_getDVGeo_error(self): + class BadGroup(Group): + def setup(self): + geometryComp = OM_DVGEOCOMP(file=outerFFD, type="ffd") + self.add_subsystem("geometry", geometryComp, promotes=["*"]) + geometryComp.nom_getDVGeo() + + prob = Problem(model=BadGroup()) + with self.assertRaises(RuntimeError): + prob.setup() + + if __name__ == "__main__": unittest.main() From 1956cd8be662c89d1c2587abf9d815908e4897ab Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Mon, 6 Oct 2025 18:06:34 -0400 Subject: [PATCH 02/10] Ignore MPhys testing output --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 02649981..2e414624 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,4 @@ doc/_build input_files .isort.cfg *.vscode -tests/reg_tests/reports/ +tests/reg_tests/test_MPhysGeo*_out From 3b7143ea1be50894a3e91fa7bc0f36373b365e57 Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Mon, 6 Oct 2025 19:38:48 -0400 Subject: [PATCH 03/10] Improve `nom_addPointSet` --- pygeo/mphys/mphys_dvgeo.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/pygeo/mphys/mphys_dvgeo.py b/pygeo/mphys/mphys_dvgeo.py index b0079e81..3aae82b7 100644 --- a/pygeo/mphys/mphys_dvgeo.py +++ b/pygeo/mphys/mphys_dvgeo.py @@ -225,22 +225,18 @@ def nom_add_discipline_coords(self, discipline, points=None, DVGeoName=None, **k self.add_input(inputName, distributed=True, val=points.flatten()) self.add_output(outputName, distributed=True, val=points.flatten()) - def nom_addPointSet(self, points, ptName, add_output=True, DVGeoName=None, **kwargs): + def nom_addPointSet(self, points, ptName, add_output=True, DVGeoName=None, distributed=True, **kwargs): # if we have multiple DVGeos use the one specified by name DVGeo = self.nom_getDVGeo(DVGeoName=DVGeoName) # add the points to the dvgeo object - dMaxGlobal = None - if isinstance(DVGeo, DVGeometryESP): - # DVGeoESP can return a value to check the pointset distribution - dMaxGlobal = DVGeo.addPointSet(points.reshape(len(points) // 3, 3), ptName, **kwargs) - else: - DVGeo.addPointSet(points.reshape(len(points) // 3, 3), ptName, **kwargs) + # DVGeoESP can return a value to check the pointset distribution + dMaxGlobal = DVGeo.addPointSet(points.reshape(-1, 3), ptName, **kwargs) self.omPtSetList.append(ptName) if add_output: # add an output to the om component - self.add_output(ptName, distributed=True, val=points.flatten()) + self.add_output(ptName, distributed=distributed, val=points.flatten()) return dMaxGlobal From 7bba4ea6548e8af07a936c4c5507d2880de6b804 Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Mon, 6 Oct 2025 19:47:49 -0400 Subject: [PATCH 04/10] Add docstring for `nom_addPointSet` --- pygeo/mphys/mphys_dvgeo.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/pygeo/mphys/mphys_dvgeo.py b/pygeo/mphys/mphys_dvgeo.py index 3aae82b7..0e873043 100644 --- a/pygeo/mphys/mphys_dvgeo.py +++ b/pygeo/mphys/mphys_dvgeo.py @@ -226,11 +226,31 @@ def nom_add_discipline_coords(self, discipline, points=None, DVGeoName=None, **k self.add_output(outputName, distributed=True, val=points.flatten()) def nom_addPointSet(self, points, ptName, add_output=True, DVGeoName=None, distributed=True, **kwargs): + """Add a pointset to the DVGeo object and create an output for it in the OpenMDAO component. + + Parameters + ---------- + points : numpy array + 3D points to add to the DVGeo object, shape (N,3) or (3N,) + ptName : str + Name for the pointset + add_output : bool, optional + Whether to add the deformed points as an output of the component, by default True + DVGeoName : str, optional + The name of the DVGeo to add the points to, necessary if there are multiple DVGeo objects. By default `None`. + distributed : bool, optional + Whether the output of the component should be a distributed variable, by default True + + Returns + ------- + None or float + If using DVGeometryESP or DVGeometryVSP, returns the maximum distance between pointset and the CAD model + """ # if we have multiple DVGeos use the one specified by name DVGeo = self.nom_getDVGeo(DVGeoName=DVGeoName) # add the points to the dvgeo object - # DVGeoESP can return a value to check the pointset distribution + # DVGeoESP and DVGeoVSP can return a value to check the pointset distribution dMaxGlobal = DVGeo.addPointSet(points.reshape(-1, 3), ptName, **kwargs) self.omPtSetList.append(ptName) From d3ffb46b780d0b5eaadd4975c41c63f8f1386e42 Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Tue, 7 Oct 2025 09:38:10 -0400 Subject: [PATCH 05/10] Turn off OpenMDAO reports in MPhys tests --- tests/reg_tests/test_MPhysGeo.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/reg_tests/test_MPhysGeo.py b/tests/reg_tests/test_MPhysGeo.py index 87dca7c8..90da68d0 100644 --- a/tests/reg_tests/test_MPhysGeo.py +++ b/tests/reg_tests/test_MPhysGeo.py @@ -323,7 +323,7 @@ def configure(self): self.add_constraint(f"geometry.{ptName}") - self.prob = Problem(model=FFDGroup()) + self.prob = Problem(model=FFDGroup(), reports=False) def test_run_model(self): self.prob.setup() @@ -404,7 +404,7 @@ def twist(val, geo): self.add_design_var("local") self.add_objective(paramKwargs["name"]) - p = Problem(model=BoxGeo()) + p = Problem(model=BoxGeo(), reports=False) return p def test_undeformed_vals(self): @@ -529,7 +529,7 @@ def configure(self): self.add_constraint(f"geometry.{ptName}") - self.prob = Problem(model=ESPGroup()) + self.prob = Problem(model=ESPGroup(), reports=False) def test_run_model(self): self.prob.setup() @@ -572,7 +572,7 @@ def setup(self): self.add_subsystem("geometry", geometryComp, promotes=["*"]) geometryComp.nom_getDVGeo() - prob = Problem(model=BadGroup()) + prob = Problem(model=BadGroup(), reports=False) with self.assertRaises(RuntimeError): prob.setup() From 01cfee5061e9aad6b30fee877391d0427c1e116b Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Wed, 15 Oct 2025 20:26:56 -0400 Subject: [PATCH 06/10] Initialize `self.update_jac` in `initialize` --- pygeo/mphys/mphys_dvgeo.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pygeo/mphys/mphys_dvgeo.py b/pygeo/mphys/mphys_dvgeo.py index 0e873043..c97ad370 100644 --- a/pygeo/mphys/mphys_dvgeo.py +++ b/pygeo/mphys/mphys_dvgeo.py @@ -36,6 +36,8 @@ def initialize(self): # since `nom_add_discipline_coords` can be called before `setup` self.omPtInOutDict = {} + self.update_jac = True + def setup(self): # create a constraints object to go with this DVGeo(s) self.DVCon = DVConstraints() From 74d2e661e832bd65e2a9d6bf1d8b409c918be268 Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Tue, 21 Oct 2025 16:58:04 -0400 Subject: [PATCH 07/10] Fix derivatives for non-distributed pointsets --- pygeo/mphys/mphys_dvgeo.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/pygeo/mphys/mphys_dvgeo.py b/pygeo/mphys/mphys_dvgeo.py index c97ad370..9a38f07b 100644 --- a/pygeo/mphys/mphys_dvgeo.py +++ b/pygeo/mphys/mphys_dvgeo.py @@ -1,3 +1,5 @@ +import inspect + # External modules from mpi4py import MPI import numpy as np @@ -99,7 +101,7 @@ def setup(self): for _, DVGeo in self.DVGeos.items(): self.DVCon.setDVGeo(DVGeo, name=DVConName) - self.omPtSetList = [] + self.omPtSetList = {} def compute(self, inputs, outputs): # check for inputs that have been added but the points have not been added to dvgeo @@ -223,7 +225,7 @@ def nom_add_discipline_coords(self, discipline, points=None, DVGeoName=None, **k else: # we are provided with points. we can do the full initialization now - self.nom_addPointSet(points, outputName, add_output=False, DVGeoName=DVGeoName, **kwargs) + self.nom_addPointSet(points, outputName, add_output=False, DVGeoName=DVGeoName, distributed=True, **kwargs) self.add_input(inputName, distributed=True, val=points.flatten()) self.add_output(outputName, distributed=True, val=points.flatten()) @@ -253,8 +255,13 @@ def nom_addPointSet(self, points, ptName, add_output=True, DVGeoName=None, distr # add the points to the dvgeo object # DVGeoESP and DVGeoVSP can return a value to check the pointset distribution - dMaxGlobal = DVGeo.addPointSet(points.reshape(-1, 3), ptName, **kwargs) - self.omPtSetList.append(ptName) + # Also, the addPointSet method for some DVGeos takes a distributed kwarg, in which case we can pass it + sig = inspect.signature(DVGeo.addPointSet) + if "distributed" in sig.parameters: + dMaxGlobal = DVGeo.addPointSet(points.reshape(-1, 3), ptName, distributed=distributed, **kwargs) + else: + dMaxGlobal = DVGeo.addPointSet(points.reshape(-1, 3), ptName, **kwargs) + self.omPtSetList[ptName] = {"distributed": distributed} if add_output: # add an output to the om component @@ -1124,8 +1131,11 @@ def compute_jacvec_product(self, inputs, d_inputs, d_outputs, mode): for k in xdot: # check if this dv is present if k in d_inputs: - # do the allreduce - xdotg[k] = self.comm.allreduce(xdot[k], op=MPI.SUM) + # do the allreduce if the pointset is distributed + if self.omPtSetList[ptSetName]["distributed"]: + xdotg[k] = self.comm.allreduce(xdot[k], op=MPI.SUM) + else: + xdotg[k] = xdot[k] # accumulate in the dict d_inputs[k] += xdotg[k][0] From 9ef2fa35f3e00c218a71e7c1437c6621f74d76bd Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Tue, 21 Oct 2025 17:05:24 -0400 Subject: [PATCH 08/10] Add test for non-distributed pointset with parallel MPhys --- tests/reg_tests/test_MPhysGeo.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/reg_tests/test_MPhysGeo.py b/tests/reg_tests/test_MPhysGeo.py index 90da68d0..73640451 100644 --- a/tests/reg_tests/test_MPhysGeo.py +++ b/tests/reg_tests/test_MPhysGeo.py @@ -266,6 +266,8 @@ @unittest.skipUnless(omInstalled, "OpenMDAO is required to test the pyGeo MPhys wrapper") @parameterized_class(ffd_test_params) class TestDVGeoMPhysFFD(unittest.TestCase): + N_PROCS = 2 + def setUp(self): # give the OM Group access to the test case attributes dvInfo = self.dvInfo @@ -286,7 +288,7 @@ def configure(self): points[0, :] = [0.25, 0, 0] points[1, :] = [-0.25, 0, 0] ptName = "testPoints" - self.geometry.nom_addPointSet(points.flatten(), ptName) + self.geometry.nom_addPointSet(points.flatten(), ptName, distributed=False) # create a reference axis for the parent axisPoints = [[-1.0, 0.0, 0.0], [1.5, 0.0, 0.0]] From f3e7bc86aa90c63ecdebe3f84a1cc7e6b1a3a040 Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Tue, 21 Oct 2025 17:45:11 -0400 Subject: [PATCH 09/10] isort --- pygeo/mphys/mphys_dvgeo.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pygeo/mphys/mphys_dvgeo.py b/pygeo/mphys/mphys_dvgeo.py index 9a38f07b..ca73a575 100644 --- a/pygeo/mphys/mphys_dvgeo.py +++ b/pygeo/mphys/mphys_dvgeo.py @@ -1,3 +1,4 @@ +# Standard Python modules import inspect # External modules From ffbda1232fd1198a9e6ac9b95037449097696e37 Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Thu, 23 Oct 2025 09:38:59 -0400 Subject: [PATCH 10/10] Rename variable --- pygeo/mphys/mphys_dvgeo.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pygeo/mphys/mphys_dvgeo.py b/pygeo/mphys/mphys_dvgeo.py index ca73a575..9509dc47 100644 --- a/pygeo/mphys/mphys_dvgeo.py +++ b/pygeo/mphys/mphys_dvgeo.py @@ -102,7 +102,7 @@ def setup(self): for _, DVGeo in self.DVGeos.items(): self.DVCon.setDVGeo(DVGeo, name=DVConName) - self.omPtSetList = {} + self.omPtSets = {} def compute(self, inputs, outputs): # check for inputs that have been added but the points have not been added to dvgeo @@ -112,7 +112,7 @@ def compute(self, inputs, outputs): # retrieve corresponding output name var_out = self.omPtInOutDict[var] # add pointset if it doesn't already exist - if var_out not in self.omPtSetList: + if var_out not in self.omPtSets: self.nom_addPointSet(inputs[var], var_out, add_output=False) # handle DV update and pointset changes for all of our DVGeos @@ -122,7 +122,7 @@ def compute(self, inputs, outputs): # ouputs are the coordinates of the pointsets we have for ptName in DVGeo.points: - if ptName in self.omPtSetList: + if ptName in self.omPtSets: # update this pointset and write it as output outputs[ptName] = DVGeo.update(ptName).flatten() @@ -262,7 +262,7 @@ def nom_addPointSet(self, points, ptName, add_output=True, DVGeoName=None, distr dMaxGlobal = DVGeo.addPointSet(points.reshape(-1, 3), ptName, distributed=distributed, **kwargs) else: dMaxGlobal = DVGeo.addPointSet(points.reshape(-1, 3), ptName, **kwargs) - self.omPtSetList[ptName] = {"distributed": distributed} + self.omPtSets[ptName] = {"distributed": distributed} if add_output: # add an output to the om component @@ -1101,7 +1101,7 @@ def compute_jacvec_product(self, inputs, d_inputs, d_outputs, mode): ptSetNames = DVGeo.ptSetNames for ptSetName in ptSetNames: - if ptSetName in self.omPtSetList: + if ptSetName in self.omPtSets: # Process the seeds if doFwd: # Collect the d_inputs associated with the current DVGeo @@ -1133,7 +1133,7 @@ def compute_jacvec_product(self, inputs, d_inputs, d_outputs, mode): # check if this dv is present if k in d_inputs: # do the allreduce if the pointset is distributed - if self.omPtSetList[ptSetName]["distributed"]: + if self.omPtSets[ptSetName]["distributed"]: xdotg[k] = self.comm.allreduce(xdot[k], op=MPI.SUM) else: xdotg[k] = xdot[k]