Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
885b72d
Add helpful error message if `nom_getDVGeo` is called before `setup`
A-CGray Oct 6, 2025
1956cd8
Ignore MPhys testing output
A-CGray Oct 6, 2025
3b7143e
Improve `nom_addPointSet`
A-CGray Oct 6, 2025
7bba4ea
Add docstring for `nom_addPointSet`
A-CGray Oct 6, 2025
d3ffb46
Turn off OpenMDAO reports in MPhys tests
A-CGray Oct 7, 2025
77c26ea
Helper functions for projecting points
A-CGray Oct 8, 2025
3cd110e
Completely overhaul thickness to chord constraint implementation
A-CGray Oct 10, 2025
655d2db
Improve `DVConstraints.writeTecplot`
A-CGray Oct 10, 2025
47f53c5
Fix projection error
A-CGray Oct 10, 2025
de88ce1
Implement `addThicknessToChordConstraints2D` and `addThicknessToChord…
A-CGray Oct 10, 2025
d1eabd7
Add docstring for `GeometricConstraint` constructor
A-CGray Oct 10, 2025
388f56e
Update tests for new thickness to chord constraints
A-CGray Oct 10, 2025
a087b8f
Remove changes from https://github.com/mdolab/pygeo/pull/279
A-CGray Oct 10, 2025
7274548
Fix bug in derivatives of max t/c
A-CGray Oct 10, 2025
dafb639
Reword docstring
A-CGray Oct 10, 2025
a379513
Formatting
A-CGray Oct 10, 2025
ff93693
Test tecplot file writing for DVConstraints
A-CGray Oct 10, 2025
c21967c
Docstring changes
A-CGray Oct 10, 2025
35594c4
Return constraint object from DVCon `add*Constraint*` methods
A-CGray Oct 10, 2025
02e74b4
Add MPhys wrappers for thickness to chord constraints
A-CGray Oct 10, 2025
9cb0619
isort
A-CGray Oct 10, 2025
5855f4c
Accept `NotImplementedError` when testing `DVCon.writeTecplot`
A-CGray Oct 10, 2025
299a006
Update pygeo/constraints/thicknessConstraint.py
A-CGray Oct 10, 2025
6bf5c7b
Update pygeo/constraints/thicknessConstraint.py
A-CGray Oct 10, 2025
00ecec6
Update pygeo/constraints/DVCon.py
A-CGray Oct 10, 2025
49e9b83
Fix `compNames` type in docstrings
A-CGray Oct 10, 2025
22f188f
Merge branch 'thicknessToChord' of github.com:mdolab/pygeo into thick…
A-CGray Oct 10, 2025
a3dc418
Make `writeTecplot` work when some constraints don't support `writeTe…
A-CGray Oct 10, 2025
d6d2266
Fix bug in Scaled maximum t/c constraints
A-CGray Oct 10, 2025
2d147db
Turn off training
A-CGray Oct 10, 2025
12f9b45
Update pygeo/constraints/DVCon.py
A-CGray Oct 10, 2025
5e29787
Write separate tecplot one for max thickness locations
A-CGray Oct 13, 2025
f73754e
Remove changes incorrectly included in last commit
A-CGray Oct 13, 2025
00aa366
Correct and explain the ordering of points returned from `_projectToS…
A-CGray Oct 20, 2025
a30cc77
Merge branch 'main' into thicknessToChord
A-CGray Nov 28, 2025
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
538 changes: 317 additions & 221 deletions pygeo/constraints/DVCon.py

Large diffs are not rendered by default.

21 changes: 19 additions & 2 deletions pygeo/constraints/baseConstraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,26 @@ class GeometricConstraint(ABC):
"""

def __init__(self, name, nCon, lower, upper, scale, DVGeo, addToPyOpt):
"""Create a geometric constraint object.

Parameters
----------
name : str
Name for this constraint, will be used in pyOpt
nCon : int
Number of constraint values
lower : float or array, or None
Constraint lower bound, None means no lower bound
upper : float or array, or None
Constraint upper bound, None means no upper bound
scale : float or array, or None
Scaling factor for this constraint, None corresponds to 1.0
DVGeo : BaseDVGeometry
Geometry parameterization object
addToPyOpt : bool
Whether or not to add this constraint to pyOpt when addConstraintsPyOpt is called
"""
General init function. Every constraint has these functions
"""

self.name = name
self.nCon = nCon
self.lower = lower
Expand Down
265 changes: 196 additions & 69 deletions pygeo/constraints/thicknessConstraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,91 +210,218 @@ def writeTecplot(self, handle):


class ThicknessToChordConstraint(GeometricConstraint):
"""These are almost identical to ThicknessConstraint but track and additional set of points along the leading and
trailing edges used to compute a chord length for scaling the thickness.
"""
ThicknessToChordConstraint represents of a set of
thickess-to-chord ratio constraints. One of these objects is
created each time a addThicknessToChordConstraints2D or
addThicknessToChordConstraints1D call is made. The user should not
have to deal with this class directly.
"""

def __init__(self, name, coords, lower, upper, scale, DVGeo, addToPyOpt, compNames):
super().__init__(name, len(coords) // 4, lower, upper, scale, DVGeo, addToPyOpt)
self.coords = coords

# First thing we can do is embed the coordinates into DVGeo
# with the name provided:
self.DVGeo.addPointSet(self.coords, self.name, compNames=compNames)

# Now get the reference lengths
self.ToC0 = np.zeros(self.nCon)
for i in range(self.nCon):
t = np.linalg.norm(self.coords[4 * i] - self.coords[4 * i + 1])
c = np.linalg.norm(self.coords[4 * i + 2] - self.coords[4 * i + 3])
self.ToC0[i] = t / c

def evalFunctions(self, funcs, config):
"""
Evaluate the functions this object has and place in the funcs dictionary
def __init__(
self,
name,
thicknessCoords,
LeTeCoords,
lower,
upper,
scaled,
scale,
DVGeo,
addToPyOpt,
compNames,
sectionMax=False,
ksRho=50.0,
):
"""Instantiate a ThicknessToChordConstraint object, this should not be called directly by the user but instead
by addThicknessToChordConstraints1D or addThicknessToChordConstraints2D.

Parameters
----------
funcs : dict
Dictionary to place function values
name : str
See :meth:`GeometricConstraint.__init__ <.baseConstraint.GeometricConstraint.__init__>`.
thicknessCoords : numpy array of shape (..., nSpan, 2, 3)
Coordinates of the points used to measure thickness.
LeTeCoords : numpy array of shape (nSpan, 2, 3)
Coordinates of the leading and trailing edge points used to measure chord length.
lower : float, array, or None
See :meth:`GeometricConstraint.__init__ <.baseConstraint.GeometricConstraint.__init__>`.
upper : float, array, or None
See :meth:`GeometricConstraint.__init__ <.baseConstraint.GeometricConstraint.__init__>`.
scaled : bool
If true, the constraint values are normalized by their initial values.
scale : _type_
See :meth:`GeometricConstraint.__init__ <.baseConstraint.GeometricConstraint.__init__>`.
DVGeo : _type_
See :meth:`GeometricConstraint.__init__ <.baseConstraint.GeometricConstraint.__init__>`.
addToPyOpt : _type_
See :meth:`GeometricConstraint.__init__ <.baseConstraint.GeometricConstraint.__init__>`.
compNames : list or None
If using DVGeometryMulti, the components to which the point set associated with this constraint should be
added. If None, the point set is added to all components.
sectionMax : bool
If True, computes the maximum thickness-to-chord ratio in each section using KS aggregation.
ksRho : float
The rho value to use for KS aggregation if ``sectionMax=True``
"""
# Pull out the most recent set of coordinates:
self.coords = self.DVGeo.update(self.name, config=config)
ToC = np.zeros(self.nCon)
for i in range(self.nCon):
t = geo_utils.eDist(self.coords[4 * i], self.coords[4 * i + 1])
c = geo_utils.eDist(self.coords[4 * i + 2], self.coords[4 * i + 3])
ToC[i] = (t / c) / self.ToC0[i]
self.sectionMax = sectionMax
self.ksRho = ksRho
self.scaled = scaled

funcs[self.name] = ToC
# TODO: Alter this to deal with 1d or 2d sets of points
self.numSpanPoints = thicknessCoords.shape[0]
# The input coordinates may not have a chordwise dimension, so handle that here
if len(thicknessCoords.shape) == 3:
thicknessCoords = thicknessCoords.reshape((self.numSpanPoints, 1, 2, 3))
self.origCoordsShape = thicknessCoords.shape
self.numChordPoints = self.origCoordsShape[1]

# No point in doing section max if there's only one chordwise point
if self.numChordPoints == 1:
self.sectionMax = False

numCon = self.numSpanPoints if sectionMax else self.numSpanPoints * self.numChordPoints
super().__init__(name, numCon, lower, upper, scale, DVGeo, addToPyOpt)

# Verify that we have the right number of leading and trailing edge points
self.origLeTeShape = LeTeCoords.shape
if self.origLeTeShape != (self.numSpanPoints, 2, 3):
raise ValueError(
f"LeTeCoords has incorrect shape, should be ({self.numSpanPoints}, 2, 3), got {LeTeCoords.shape}"
)

self.thicknessConName = name + "_thickness"
self.chordConName = name + "_chord"

# Create a ThicknessConstraint object to handle the thickness part of this constraint
self.thicknessConstraint = ThicknessConstraint(
name=self.thicknessConName,
coords=thicknessCoords.reshape((-1, 3)),
lower=-np.inf,
upper=np.inf,
scaled=False,
scale=scale,
DVGeo=DVGeo,
addToPyOpt=False,
compNames=compNames,
)

# Create another thickness constraint to handle the chord length part of this constraint
self.chordConstraint = ThicknessConstraint(
name=self.chordConName,
coords=LeTeCoords.reshape((-1, 3)),
lower=-np.inf,
upper=np.inf,
scaled=False,
scale=scale,
DVGeo=DVGeo,
addToPyOpt=False,
compNames=compNames,
)

# Compute the initial thickness-to-chord ratios if we are scaling
self.tOverCInit = None
if scaled:
funcs = {}
self.evalFunctions(funcs, config=None)
self.tOverCInit = funcs[self.name].reshape((self.numSpanPoints, self.numChordPoints))

def evalFunctions(self, funcs, config):
thickness, chord = self.computeThicknessAndChord(config)
tOverC = thickness / chord
if self.sectionMax:
tOverC = self.ksMax(tOverC, self.ksRho, axis=1)

if self.scaled and self.tOverCInit is not None:
tOverC /= self.tOverCInit

funcs[self.name] = tOverC.flatten()

def computeThicknessAndChord(self, config):
tempFuncs = {}
self.thicknessConstraint.evalFunctions(tempFuncs, config)
self.chordConstraint.evalFunctions(tempFuncs, config)

thickness = tempFuncs[self.thicknessConName].reshape((self.numSpanPoints, self.numChordPoints))
chord = tempFuncs[self.chordConName].reshape((self.numSpanPoints, 1))
return thickness, chord

def computeThicknessAndChordSens(self, config):
tempFuncsSens = {}
self.thicknessConstraint.evalFunctionsSens(tempFuncsSens, config)
self.chordConstraint.evalFunctionsSens(tempFuncsSens, config)

for dvName in self.getVarNames():
numDVs = tempFuncsSens[self.thicknessConName][dvName].shape[-1]
tempFuncsSens[self.thicknessConName][dvName] = tempFuncsSens[self.thicknessConName][dvName].reshape(
(self.numSpanPoints, self.numChordPoints, numDVs)
)
tempFuncsSens[self.chordConName][dvName] = tempFuncsSens[self.chordConName][dvName].reshape(
(self.numSpanPoints, 1, numDVs)
)
return tempFuncsSens[self.thicknessConName], tempFuncsSens[self.chordConName]

def evalFunctionsSens(self, funcsSens, config):
"""
Evaluate the sensitivity of the functions this object has and
place in the funcsSens dictionary
thickness, chord = self.computeThicknessAndChord(config)
thicknessSens, chordSens = self.computeThicknessAndChordSens(config)

funcsSens[self.name] = {}
tOverCSens = funcsSens[self.name]
dvNames = set(thicknessSens.keys()).union(set(chordSens.keys()))
for dvName in dvNames:
dtdx = thicknessSens[dvName]
dcdx = chordSens[dvName]
numDVs = dtdx.shape[-1]

# Quotient rule says d(t/c)dx = (dtdx*c - t*dcdx) / c^2
tOverCSens[dvName] = (dtdx * chord[:, :, np.newaxis] - thickness[:, :, np.newaxis] * dcdx) / (chord**2)[
:, :, np.newaxis
]

if self.sectionMax:
dksdtc = self.ksMaxSens(thickness / chord, self.ksRho, axis=1) # shape (nSpan, nChord)
tOverCSens[dvName] = np.einsum("sc,scd->sd", dksdtc, tOverCSens[dvName])

tOverCSens[dvName] = tOverCSens[dvName].reshape(-1, numDVs)
if self.scaled:
tOverCSens[dvName] /= self.tOverCInit.flatten()[:, np.newaxis]

def writeTecplot(self, handle):
self.thicknessConstraint.writeTecplot(handle)
self.chordConstraint.writeTecplot(handle)

@staticmethod
def ksMax(f, rho, axis=None):
"""Approximate the maximum value of f along the given axis using KS aggregation.

Parameters
----------
funcsSens : dict
Dictionary to place function values
f : array
Input data
rho : float
KS aggregation parameter, larger values more closely approximate the max but are less smooth.
axis : int, optional
The dimension to compute the max along, by default computes max across entire array.
"""
fMax = np.max(f, axis=axis, keepdims=True)
exponents = np.exp(rho * (f - fMax))
ks = fMax + np.log(np.sum(exponents, axis=axis, keepdims=True)) / rho
return ks

nDV = self.DVGeo.getNDV()
if nDV > 0:
dToCdPt = np.zeros((self.nCon, self.coords.shape[0], self.coords.shape[1]))

for i in range(self.nCon):
t = geo_utils.eDist(self.coords[4 * i], self.coords[4 * i + 1])
c = geo_utils.eDist(self.coords[4 * i + 2], self.coords[4 * i + 3])

p1b, p2b = geo_utils.eDist_b(self.coords[4 * i, :], self.coords[4 * i + 1, :])
p3b, p4b = geo_utils.eDist_b(self.coords[4 * i + 2, :], self.coords[4 * i + 3, :])

dToCdPt[i, 4 * i, :] = p1b / c / self.ToC0[i]
dToCdPt[i, 4 * i + 1, :] = p2b / c / self.ToC0[i]
dToCdPt[i, 4 * i + 2, :] = (-p3b * t / c**2) / self.ToC0[i]
dToCdPt[i, 4 * i + 3, :] = (-p4b * t / c**2) / self.ToC0[i]

funcsSens[self.name] = self.DVGeo.totalSensitivity(dToCdPt, self.name, config=config)
@staticmethod
def ksMaxSens(f, rho, axis=None):
"""Compute the sensitivity of the KS max function.

def writeTecplot(self, handle):
"""
Write the visualization of this set of thickness constraints
to the open file handle
Parameters
----------
f : array
Input data
rho : float
KS aggregation parameter, larger values more closely approximate the max but are less smooth.
axis : int, optional
The dimension to compute the max along, by default computes max across entire array.
"""

handle.write("Zone T=%s\n" % self.name)
handle.write("Nodes = %d, Elements = %d ZONETYPE=FELINESEG\n" % (len(self.coords), len(self.coords) // 2))
handle.write("DATAPACKING=POINT\n")
for i in range(len(self.coords)):
handle.write(f"{self.coords[i, 0]:f} {self.coords[i, 1]:f} {self.coords[i, 2]:f}\n")

for i in range(len(self.coords) // 2):
handle.write("%d %d\n" % (2 * i + 1, 2 * i + 2))
fMax = np.max(f, axis=axis, keepdims=True)
exponents = np.exp(rho * (f - fMax))
sumExp = np.sum(exponents, axis=axis, keepdims=True)
dksdf = exponents / sumExp
return dksdf


class ProximityConstraint(GeometricConstraint):
Expand Down
2 changes: 1 addition & 1 deletion pygeo/geo_utils/ffd_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ def createFittedWingFFD(surf, surfFormat, outFile, leList, teList, nSpan, nChord
DVCon.setSurface(surf, surfFormat=surfFormat)

# Get the surface intersections; surfCoords has dimensions [nSpanTotal, nChord, 2, 3]
surfCoords = DVCon._generateIntersections(leList, teList, nSpan, nChord, surfaceName="default")
surfCoords = DVCon._generateGridIntersections(leList, teList, nSpan, nChord, surfaceName="default")

nSpanTotal = np.sum(nSpan)

Expand Down
32 changes: 32 additions & 0 deletions pygeo/mphys/mphys_dvgeo.py
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,38 @@ def nom_addThicknessConstraints1D(
)
self.add_output(name, distributed=False, val=np.ones(nCon), shape=nCon)

def nom_addThicknessToChordConstraints1D(self, name, ptList, leList, teList, nCon, **kwargs):
"""
Add a DVCon thickness to chord constraint to the problem
Wrapper for :meth:`addThicknessToChordConstraints1D <.DVConstraints.addThicknessToChordConstraints1D>`
Input parameters are identical to those in wrapped function unless otherwise specified
"""
con = self.DVCon.addThicknessToChordConstraints1D(
ptList,
leList,
teList,
nCon,
name=name,
**kwargs,
)
self.add_output(name, distributed=False, val=np.ones(con.nCon), shape=con.nCon)

def nom_addThicknessToChordConstraints2D(self, name, leList, teList, nSpan, nChord, **kwargs):
"""
Add a DVCon thickness to chord constraint to the problem
Wrapper for :meth:`addThicknessToChordConstraints2D <.DVConstraints.addThicknessToChordConstraints2D>`
Input parameters are identical to those in wrapped function unless otherwise specified
"""
con = self.DVCon.addThicknessToChordConstraints2D(
leList,
teList,
nSpan,
nChord,
name=name,
**kwargs,
)
self.add_output(name, distributed=False, val=np.ones(con.nCon), shape=con.nCon)

def nom_addVolumeConstraint(
self,
name,
Expand Down
Loading
Loading