# External modules
from mpi4py import MPI
import numpy as np
import openmdao.api as om
from openmdao.api import AnalysisError
# Local modules
from .. import DVConstraints, DVGeometry, DVGeometryESP, DVGeometryMulti, DVGeometryVSP
# class that actually calls the DVGeometry methods
[docs]
class OM_DVGEOCOMP(om.ExplicitComponent):
[docs]
def initialize(self):
r"""
Set up a geometry component with either 1 DVGeo or multiple DVGeos.
A single DVGeo are initialized by specifying its file and type and, optionally, additional options.
Available options can be found in the specific DVGeometry class.
Multiple DVGeos are initialized in a dictionary of these values and must have a unique name. The format is:
>>> DVGeoInfo = {
>>> "name1": {"file": file1, "type": type1, "options": options1}
>>> "name2": {"file": file2, "type": type2, "options": options2}
>>> }
The two setup methods cannot currently be used together.
"""
self.options.declare("file", default=None)
self.options.declare("type", default=None)
self.options.declare("options", default=None)
self.options.declare("DVGeoInfo", default=None)
def setup(self):
# create a constraints object to go with this DVGeo(s)
self.DVCon = DVConstraints()
# hold the DVGeo(s) in a dictionary
self.DVGeos = {}
# conventional setup with one DVGeo. maintains old interface
if self.options["DVGeoInfo"] is None:
self.multDVGeo = False
# set up DVGeoInfo so a single DVGeo can be initialized with the multi-DVGeo case
DVGeoInfo = {
"defaultDVGeo": {
"file": self.options["file"],
"type": self.options["type"],
"options": self.options["options"],
}
}
# DVGeo and DVCon expect different defaults
DVConName = "default"
DVGeoName = None
# we need to add multiple DVGeos to this geometry component
# the actual initialization is handled in the same way regardless
else:
self.multDVGeo = True
DVGeoInfo = self.options["DVGeoInfo"]
# create the DVGeo object that does the computations (or multiple DVGeo objects)
for name, info in DVGeoInfo.items():
if self.multDVGeo:
DVGeoName = DVConName = name
if info.get("options") is None:
options = {}
else:
options = info["options"]
# this DVGeo uses FFD
if info["type"] == "ffd":
self.DVGeos.update({name: DVGeometry(info["file"], name=DVGeoName, **options)})
# this DVGeo uses VSP
elif info["type"] == "vsp":
self.DVGeos.update({name: DVGeometryVSP(info["file"], comm=self.comm, name=DVGeoName, **options)})
# this DVGeo uses ESP
elif info["type"] == "esp":
self.DVGeos.update({name: DVGeometryESP(info["file"], comm=self.comm, name=DVGeoName, **options)})
elif info["type"] == "multi":
self.DVGeos.update({name: DVGeometryMulti(comm=self.comm, **options)})
# add each geometry to the constraints object
for _, DVGeo in self.DVGeos.items():
self.DVCon.setDVGeo(DVGeo, name=DVConName)
self.omPtSetList = []
def compute(self, inputs, outputs):
# check for inputs that have been added but the points have not been added to dvgeo
for var in inputs.keys():
# check that the input name matches the convention for points
if var[:2] == "x_":
# trim the _in and add a "0" to signify that these are initial conditions initial
var_out = var[:-3] + "0"
if var_out not in self.omPtSetList:
self.nom_addPointSet(inputs[var], var_out, add_output=False)
# handle DV update and pointset changes for all of our DVGeos
for _, DVGeo in self.DVGeos.items():
# inputs are the geometric design variables
DVGeo.setDesignVars(inputs)
# ouputs are the coordinates of the pointsets we have
for ptName in DVGeo.points:
if ptName in self.omPtSetList:
# update this pointset and write it as output
outputs[ptName] = DVGeo.update(ptName).flatten()
# compute the DVCon constraint values
constraintfunc = dict()
self.DVCon.evalFunctions(constraintfunc, includeLinear=True)
for constraintname in constraintfunc:
# if any constraint returned a fail flag throw an error to OpenMDAO
# all constraints need the same fail flag, no <name_> prefix
if constraintname == "fail":
raise AnalysisError("Analysis error in geometric constraints")
outputs[constraintname] = constraintfunc[constraintname]
# we ran a compute so the inputs changed. update the dvcon jac
# next time the jacvec product routine is called
self.update_jac = True
[docs]
def nom_addComponent(self, comp, ffd_file, triMesh, DVGeoName=None):
"""
Add a component a DVGeometryMulti object. This is a wrapper for the DVGeoMulti.addComponent method.
Parameters
----------
comp : str
The name of the component.
ffd_file : str
Path to the FFD file for the new DVGeo object for this component.
triMesh : str, optional
Path to the triangulated mesh file for this component.
DVGeoName : str, optional
The name of the DVGeo object to add this component to.
"""
# if we have multiple DVGeos use the one specified by name
DVGeo = self.nom_getDVGeo(DVGeoName=DVGeoName)
# can only add a DVGeo to a DVGeoMulti
if not isinstance(DVGeo, DVGeometryMulti):
raise RuntimeError(
f"Only multi-based DVGeo objects can have components added to them, not type:{self.geo_type}"
)
# Add component
DVGeoComp = DVGeometry(ffd_file)
DVGeo.addComponent(comp=comp, DVGeo=DVGeoComp, triMesh=triMesh)
def nom_addChild(self, ffd_file, DVGeoName=None, childName=None, comp=None):
# if we have multiple DVGeos use the one specified by name
DVGeo = self.nom_getDVGeo(DVGeoName=DVGeoName)
# can only add a child to a FFD DVGeo
if not isinstance(DVGeo, DVGeometry):
raise RuntimeError(
f"Only FFD-based DVGeo objects can have children added to them, not type: {type(DVGeo).__name__}"
)
# Add child FFD
child_ffd = DVGeometry(ffd_file, child=True, name=childName)
if comp is None:
DVGeo.addChild(child_ffd)
else:
DVGeo.DVGeoDict[comp].addChild(child_ffd)
# Embed points from parent if not already done
for pointSet in DVGeo.points:
if pointSet not in child_ffd.points:
child_ffd.addPointSet(DVGeo.points[pointSet], pointSet)
def nom_add_discipline_coords(self, discipline, points=None, DVGeoName=None, **kwargs):
# TODO remove one of these methods to keep only one method to add pointsets
if points is None:
# no pointset info is provided, just do a generic i/o. We will add these points during the first compute
self.add_input("x_%s_in" % discipline, distributed=True, shape_by_conn=True)
self.add_output("x_%s0" % discipline, distributed=True, copy_shape="x_%s_in" % discipline)
else:
# we are provided with points. we can do the full initialization now
self.nom_addPointSet(points, "x_%s0" % discipline, add_output=False, DVGeoName=DVGeoName, **kwargs)
self.add_input("x_%s_in" % discipline, distributed=True, val=points.flatten())
self.add_output("x_%s0" % discipline, distributed=True, val=points.flatten())
def nom_addPointSet(self, points, ptName, add_output=True, DVGeoName=None, **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)
self.omPtSetList.append(ptName)
if isinstance(DVGeo, DVGeometry):
for child in DVGeo.children.values():
# Embed points from parent if not already done
for pointSet in DVGeo.points:
if pointSet not in child.points:
child.addPointSet(DVGeo.points[pointSet], pointSet)
if add_output:
# add an output to the om component
self.add_output(ptName, distributed=True, val=points.flatten())
return dMaxGlobal
def nom_add_point_dict(self, point_dict):
# add every pointset in the dict, and set the ptset name as the key
for k, v in point_dict.items():
self.nom_addPointSet(v, k)
[docs]
def nom_getDVGeo(self, childName=None, comp=None, DVGeoName=None):
"""
Gets the DVGeometry object held in the geometry component so DVGeo methods can be called directly on it
Parameters
----------
childName : str, optional
Name of the child FFD, if you want a child DVGeo returned
comp : str, optional
Name of the DVGeoMulti component, if this DV is for a multi component
DVGeoName : str, optional
The name of the DVGeo to return, necessary if there are multiple DVGeo objects
Returns
-------
DVGeometry object
DVGeometry object held by this geometry component
"""
# if we have multiple DVGeos use the one specified by name
if self.multDVGeo:
DVGeo = self.DVGeos[DVGeoName]
else:
DVGeo = self.DVGeos["defaultDVGeo"]
# return a child of a DVGeoMulti component
if childName is not None and comp is not None:
return DVGeo.DVGeoDict[comp].children[childName]
# return a component of a DVGeoMulti
elif comp is not None:
return DVGeo.DVGeoDict[comp]
# return a child of a DVGeo
elif childName is not None:
return DVGeo.children[childName]
# return the top level DVGeo
else:
return DVGeo
[docs]
def nom_getDVCon(self):
"""
Gets the DVConstraints object held in the geometry component so DVCon methods can be called directly on it
Returns
-------
self.DVCon, DVConstraints object
DVConstraints object held by this geometry component
"""
return self.DVCon
"""
Wrapper for DVGeo functions
"""
[docs]
def nom_addGlobalDV(
self,
dvName,
value,
func,
childName=None,
comp=None,
isComposite=False,
DVGeoName=None,
prependName=False,
config=None,
):
"""
Add a global design variable to the DVGeo object. This is a wrapper for the DVGeo.addGlobalDV method.
Parameters
----------
dvName : str
See :meth:`addGlobalDV <.DVGeometry.addGlobalDV>`
value : float, or iterable list of floats
See :meth:`addGlobalDV <.DVGeometry.addGlobalDV>`
func : python function
See :meth:`addGlobalDV <.DVGeometry.addGlobalDV>`
childName : str, optional
Name of the child FFD, if this DV is for a child FFD.
comp : str, optional
Name of the DVGeoMulti component, if this DV is for a multi component
isComposite : bool, optional
Whether this DV is to be included in the composite DVs, by default False
DVGeoName : string, optional
The name of the DVGeo to return, necessary if there are multiple DVGeo objects
Raises
------
RuntimeError
Raised if the underlying DVGeo object is not an FFD
"""
# if we have multiple DVGeos use the one specified by name
DVGeo = self.nom_getDVGeo(childName=childName, comp=comp, DVGeoName=DVGeoName)
# global DVs are only added to FFD-based DVGeo objects
if not isinstance(DVGeo, (DVGeometry, DVGeometryMulti)):
raise RuntimeError(f"Only FFD-based DVGeo objects can use global DVs, not type: {type(DVGeo).__name__}")
# if this DVGeo object has a name attribute, prepend it to match up with what DVGeo is expecting
# this keeps track of DVs between multiple DVGeo objects
if DVGeoName is not None and prependName:
dvName = DVGeoName + "_" + dvName
# call the dvgeo object and add this dv
DVGeo.addGlobalDV(dvName, value, func, prependName=False, config=config)
# define the input
# When composite DVs are used, input is not required for the default DVs. Now the composite DVs are
# the actual DVs. So OpenMDAO don't need the default DVs as inputs.
if not isComposite:
self.add_input(dvName, distributed=False, shape=len(np.atleast_1d(value)))
def nom_addLocalDV(
self,
dvName,
axis="y",
pointSelect=None,
childName=None,
comp=None,
isComposite=False,
DVGeoName=None,
prependName=False,
volList=None,
config=None,
):
# if we have multiple DVGeos use the one specified by name
DVGeo = self.nom_getDVGeo(childName=childName, comp=comp, DVGeoName=DVGeoName)
# local DVs are only added to FFD-based DVGeo objects
if not isinstance(DVGeo, (DVGeometry, DVGeometryMulti)):
raise RuntimeError(f"Only FFD-based DVGeo objects can use local DVs, not type: {type(DVGeo).__name__}")
# if this DVGeo object has a name attribute, prepend it to match up with what DVGeo is expecting
# this keeps track of DVs between multiple DVGeo objects
if DVGeoName is not None and prependName:
dvName = DVGeoName + "_" + dvName
# add the DV to DVGeo
nVal = DVGeo.addLocalDV(
dvName, axis=axis, pointSelect=pointSelect, prependName=False, config=config, volList=volList
)
# define the input
# When composite DVs are used, input is not required for the default DVs. Now the composite DVs are
# the actual DVs. So OpenMDAO don't need the default DVs as inputs.
if not isComposite:
self.add_input(dvName, distributed=False, shape=nVal)
return nVal
[docs]
def nom_addLocalSectionDV(
self,
dvName,
secIndex,
childName=None,
comp=None,
axis=1,
pointSelect=None,
volList=None,
orient0=None,
orient2="svd",
config=None,
DVGeoName=None,
prependName=False,
):
"""
Add one or more section local design variables to the DVGeometry object
Wrapper for :meth:`addLocalSectionDV <.DVGeometry.addLocalSectionDV>`
Input parameters are identical to those in wrapped function unless otherwise specified
Parameters
----------
dvName : str
Name to give this design variable
secIndex : char or list of chars
See wrapped
childName : str, optional
Name of the child FFD, if this DV is for a child FFD.
comp : str, optional
Name of the DVGeoMulti component, if this DV is for a multi component
axis : int, optional
See wrapped
pointSelect : pointSelect object, optional
See wrapped
volList : list, optional
See wrapped
orient0 : orientation, optional
See wrapped
orient2 : str, optional
See wrapped
config : str or list, optional
See wrapped
DVGeoName : string, optional
The name of the DVGeo to return, necessary if there are multiple DVGeo objects
Returns
-------
nVal, int
number of local section DVs
Raises
------
RuntimeError
Raised if the underlying DVGeo parameterization is not FFD-based
"""
# if we have multiple DVGeos use the one specified by name
DVGeo = self.nom_getDVGeo(childName=childName, DVGeoName=DVGeoName)
# local DVs are only added to FFD-based DVGeo objects
if not isinstance(DVGeo, (DVGeometry, DVGeometryMulti)):
raise RuntimeError(
f"Only FFD-based DVGeo objects can use local section DVs, not type: {type(DVGeo).__name__}"
)
# if this DVGeo object has a name attribute, prepend it to match up with what DVGeo is expecting
# this keeps track of DVs between multiple DVGeo objects
if DVGeoName is not None and prependName:
dvName = DVGeoName + "_" + dvName
# add the DV to DVGeo
nVal = DVGeo.addLocalSectionDV(
dvName, secIndex, axis, pointSelect, volList, orient0, orient2, config, prependName=False
)
# define the input
self.add_input(dvName, distributed=False, shape=nVal)
return nVal
[docs]
def nom_addShapeFunctionDV(
self, dvName, shapes, childName=None, comp=None, config=None, DVGeoName=None, prependName=False
):
"""
Add one or more local shape function design variables to the DVGeometry object
Wrapper for :meth:`addShapeFunctionDV <.DVGeometry.addShapeFunctionDV>`
Input parameters are identical to those in wrapped function unless otherwise specified
Parameters
----------
dvName : str
Name to give this design variable
shapes : list of dictionaries, or a single dictionary
See wrapped
childName : str, optional
Name of the child FFD, if this DV is for a child FFD.
comp : str, optional
Name of the DVGeoMulti component, if this DV is for a multi component
config : str or list, optional
See wrapped
DVGeoName : string, optional
The name of the DVGeo to return, necessary if there are multiple DVGeo objects
Returns
-------
N : int
The number of design variables added.
Raises
------
RuntimeError
Raised if the underlying DVGeo parameterization is not FFD-based
"""
# if we have multiple DVGeos use the one specified by name
DVGeo = self.nom_getDVGeo(childName=childName, DVGeoName=DVGeoName)
# shape function DVs are only added to FFD-based DVGeo objects
if not isinstance(DVGeo, DVGeometry):
raise RuntimeError(
f"Only FFD-based DVGeo objects can use shape function DVs, not type: {type(DVGeo).__name__}"
)
# if this DVGeo object has a name attribute, prepend it to match up with what DVGeo is expecting
# this keeps track of DVs between multiple DVGeo objects
if DVGeoName is not None and prependName:
dvName = DVGeoName + "_" + dvName
# add the DV to DVGeo
nVal = DVGeo.addShapeFunctionDV(dvName, shapes, config, prependName=False)
# define the input
self.add_input(dvName, distributed=False, shape=nVal)
return nVal
def nom_addGeoCompositeDV(
self, dvName, ptSetName=None, u=None, scale=None, DVGeoName=None, prependName=False, **kwargs
):
# if we have multiple DVGeos use the one specified by name
DVGeo = self.nom_getDVGeo(DVGeoName=DVGeoName)
# if this DVGeo object has a name attribute, prepend it to match up with what DVGeo is expecting
# this keeps track of DVs between multiple DVGeo objects
if DVGeoName is not None and prependName:
dvName = DVGeoName + "_" + dvName
# call the dvgeo object and add this dv
DVGeo.addCompositeDV(dvName, ptSetName=ptSetName, u=u, scale=scale, prependName=False, **kwargs)
val = DVGeo.getValues()
# define the input
self.add_input(dvName, distributed=False, shape=DVGeo.getNDV(), val=val[dvName][0])
def nom_addVSPVariable(self, component, group, parm, isComposite=False, DVGeoName=None, **kwargs):
# if we have multiple DVGeos use the one specified by name
DVGeo = self.nom_getDVGeo(DVGeoName=DVGeoName)
# VSP DVs are only added to VSP-based DVGeo objects
if not isinstance(DVGeo, DVGeometryVSP):
raise RuntimeError(f"Only VSP-based DVGeo objects can use VSP DVs, not type: {type(DVGeo).__name__}")
# actually add the DV to VSP
DVGeo.addVariable(component, group, parm, **kwargs)
# full name of this DV
dvName = "%s:%s:%s" % (component, group, parm)
# get the value
val = DVGeo.DVs[dvName].value.copy()
# define the input
# When composite DVs are used, input is not required for the default DVs. Now the composite DVs are
# the actual DVs. So OpenMDAO don't need the default DVs as inputs.
if not isComposite:
self.add_input(dvName, distributed=False, shape=1, val=val)
[docs]
def nom_addESPVariable(self, desmptr_name, rows=None, cols=None, dh=0.001, isComposite=False, DVGeoName=None):
"""
Add an ESP design variables to the DVGeometryESP object
Wrapper for :meth:`addVariable <.DVGeometryESP.addVariable>`
Input parameters are identical to those in wrapped function unless otherwise specified
Parameters
----------
desmptr_name : str
See :meth:`addVariable <.DVGeometryESP.addVariable>`
rows : list or None, optional
See :meth:`addVariable <.DVGeometryESP.addVariable>`
cols : list or None, optional
See :meth:`addVariable <.DVGeometryESP.addVariable>`
dh : float, optional
See :meth:`addVariable <.DVGeometryESP.addVariable>`
isComposite : bool, optional
Whether this DV is to be included in the composite DVs, by default False
DVGeoName : string, optional
The name of the DVGeo to add DVs to, necessary if there are multiple DVGeo objects
Raises
------
RuntimeError
Raised if the underlying DVGeo parameterization is not ESP-based
"""
# if we have multiple DVGeos use the one specified by name
DVGeo = self.nom_getDVGeo(DVGeoName=DVGeoName)
# ESP DVs are only added to VSP-based DVGeo objects
if not isinstance(DVGeo, DVGeometryESP):
raise RuntimeError(f"Only ESP-based DVGeo objects can use ESP DVs, not type: {type(DVGeo).__name__}")
# actually add the DV to ESP
DVGeo.addVariable(desmptr_name, rows=rows, cols=cols, dh=dh)
# get the value
val = DVGeo.DVs[desmptr_name].value.copy()
# add the input with the correct value, VSP DVs always have a size of 1
# When composite DVs are used, input is not required for the default DVs. Now the composite DVs are
# the actual DVs. So OpenMDAO don't need the default DVs as inputs.
if not isComposite:
self.add_input(desmptr_name, distributed=False, shape=val.shape, val=val)
def nom_addRefAxis(
self,
name,
childName=None,
comp=None,
DVGeoName=None,
curve=None,
xFraction=None,
yFraction=None,
zFraction=None,
volumes=None,
rotType=5,
axis="x",
alignIndex=None,
rotAxisVar=None,
rot0ang=None,
rot0axis=[1, 0, 0],
includeVols=[],
ignoreInd=[],
raySize=1.5,
):
# TODO: we should change `volume` to `volList`, to be consistent with other APIs.
# But doing this may create backward incompatibility. So we will use `volumes` for now
# if we have multiple DVGeos use the one specified by name
DVGeo = self.nom_getDVGeo(childName=childName, comp=comp, DVGeoName=DVGeoName)
# references axes are only needed in FFD-based DVGeo objects
if not isinstance(DVGeo, (DVGeometry, DVGeometryMulti)):
raise RuntimeError(f"Only FFD-based DVGeo objects can use reference axes, not type: {type(DVGeo).__name__}")
# add ref axis to this DVGeo
return DVGeo.addRefAxis(
name=name,
curve=curve,
xFraction=xFraction,
yFraction=yFraction,
zFraction=zFraction,
volumes=volumes,
rotType=rotType,
axis=axis,
alignIndex=alignIndex,
rotAxisVar=rotAxisVar,
rot0ang=rot0ang,
rot0axis=rot0axis,
includeVols=includeVols,
ignoreInd=ignoreInd,
raySize=raySize,
)
# add ref axis to the specified child
"""
Wrapper for DVCon functions
"""
def nom_addThicknessConstraints2D(
self,
name,
leList,
teList,
nSpan,
nChord,
scaled=True,
addToPyOpt=True,
surfaceName="default",
DVGeoName="default",
compNames=None,
projected=False,
):
self.DVCon.addThicknessConstraints2D(
leList,
teList,
nSpan,
nChord,
name=name,
scaled=scaled,
addToPyOpt=addToPyOpt,
surfaceName=surfaceName,
DVGeoName=DVGeoName,
compNames=compNames,
projected=projected,
)
self.add_output(name, distributed=False, val=np.ones((nSpan * nChord,)), shape=nSpan * nChord)
def nom_addThicknessConstraints1D(
self,
name,
ptList,
nCon,
axis,
scaled=True,
addToPyOpt=True,
surfaceName="default",
DVGeoName="default",
compNames=None,
projected=False,
):
self.DVCon.addThicknessConstraints1D(
ptList,
nCon,
axis,
name=name,
scaled=scaled,
addToPyOpt=addToPyOpt,
surfaceName=surfaceName,
DVGeoName=DVGeoName,
compNames=compNames,
projected=projected,
)
self.add_output(name, distributed=False, val=np.ones(nCon), shape=nCon)
[docs]
def nom_addVolumeConstraint(
self,
name,
leList,
teList,
nSpan=10,
nChord=10,
scaled=True,
addToPyOpt=True,
surfaceName="default",
DVGeoName="default",
compNames=None,
):
"""
Add a DVCon volume constraint to the problem
Wrapper for :meth:`addVolumeConstraint <.DVConstraints.addVolumeConstraint>`
Input parameters are identical to those in wrapped function unless otherwise specified
Parameters
----------
name :
See wrapped
leList :
See wrapped
teList :
See wrapped
nSpan : int, optional
See wrapped
nChord : int, optional
See wrapped
scaled : bool, optional
See wrapped
surfaceName : str, optional
See wrapped
DVGeoName : str, optional
See wrapped
compNames : list, optional
See wrapped
"""
self.DVCon.addVolumeConstraint(
leList,
teList,
nSpan=nSpan,
nChord=nChord,
scaled=scaled,
name=name,
addToPyOpt=addToPyOpt,
surfaceName=surfaceName,
DVGeoName=DVGeoName,
compNames=compNames,
)
self.add_output(name, distributed=False, val=1.0)
[docs]
def nom_addSurfaceAreaConstraint(
self, name, scaled=True, addToPyOpt=True, surfaceName="default", DVGeoName="default", compNames=None
):
"""
Add a DVCon surface area constraint to the problem
Wrapper for :meth:`addSurfaceAreaConstraint <.DVConstraints.addSurfaceAreaConstraint>`
Input parameters are identical to those in wrapped function unless otherwise specified
Parameters
----------
name :
See wrapped
scaled : bool, optional
See wrapped
surfaceName : str, optional
See wrapped
DVGeoName : str, optional
See wrapped
compNames : list, optional
See wrapped
"""
self.DVCon.addSurfaceAreaConstraint(
name=name,
scaled=scaled,
addToPyOpt=addToPyOpt,
surfaceName=surfaceName,
DVGeoName=DVGeoName,
compNames=compNames,
)
self.add_output(name, distributed=False, val=1.0)
[docs]
def nom_addProjectedAreaConstraint(
self, name, axis, scaled=True, addToPyOpt=True, surface_name="default", DVGeoName="default", compNames=None
):
"""
Add a DVCon projected area constraint to the problem
Wrapper for :meth:`addProjectedAreaConstraint <.DVConstraints.addProjectedAreaConstraint>`
Input parameters are identical to those in wrapped function unless otherwise specified
Parameters
----------
name :
See wrapped
axis :
See wrapped
scaled : bool, optional
See wrapped
surface_name : str, optional
See wrapped
DVGeoName : str, optional
See wrapped
compNames : list, optional
See wrapped
"""
self.DVCon.addProjectedAreaConstraint(
axis,
name=name,
scaled=scaled,
addToPyOpt=addToPyOpt,
surfaceName=surface_name,
DVGeoName=DVGeoName,
compNames=compNames,
)
self.add_output(name, distributed=False, val=1.0)
def nom_add_LETEConstraint(
self,
name,
volID,
faceID,
topID=None,
indSetA=None,
indSetB=None,
config=None,
childName=None,
comp=None,
DVGeoName="default",
):
self.DVCon.addLeTeConstraints(
volID=volID,
faceID=faceID,
topID=topID,
indSetA=indSetA,
indSetB=indSetB,
name=name,
config=config,
childName=childName,
comp=comp,
DVGeoName=DVGeoName,
)
# how many are there?
conobj = self.DVCon.linearCon[name]
nCon = len(conobj.indSetA)
self.add_output(name, distributed=False, val=np.zeros((nCon,)), shape=nCon)
return nCon
def nom_addLERadiusConstraints(
self,
name,
leList,
nSpan,
axis,
chordDir,
scaled=True,
addToPyOpt=True,
surfaceName="default",
DVGeoName="default",
compNames=None,
):
self.DVCon.addLERadiusConstraints(
leList=leList,
nSpan=nSpan,
axis=axis,
chordDir=chordDir,
name=name,
scaled=scaled,
addToPyOpt=addToPyOpt,
surfaceName=surfaceName,
DVGeoName=DVGeoName,
compNames=compNames,
)
self.add_output(name, distributed=False, val=np.ones(nSpan), shape=nSpan)
def nom_addCurvatureConstraint1D(
self,
name,
start,
end,
nPts,
axis,
curvatureType="mean",
scaled=True,
KSCoeff=1.0,
addToPyOpt=True,
surfaceName="default",
DVGeoName="default",
compNames=None,
):
self.DVCon.addCurvatureConstraint1D(
start=start,
end=end,
nPts=nPts,
axis=axis,
name=name,
curvatureType=curvatureType,
scaled=scaled,
KSCoeff=KSCoeff,
addToPyOpt=addToPyOpt,
surfaceName=surfaceName,
DVGeoName=DVGeoName,
compNames=compNames,
)
self.add_output(name, distributed=False, val=1.0)
def nom_addLinearConstraintsShape(
self, name, indSetA, indSetB, factorA, factorB, config=None, childName=None, comp=None, DVGeoName="default"
):
self.DVCon.addLinearConstraintsShape(
indSetA=indSetA,
indSetB=indSetB,
factorA=factorA,
factorB=factorB,
name=name,
config=config,
childName=childName,
comp=comp,
DVGeoName=DVGeoName,
)
lSize = len(indSetA)
self.add_output(name, distributed=False, val=np.zeros(lSize), shape=lSize)
def nom_addTriangulatedSurfaceConstraint(
self,
name,
surface_1_name=None,
DVGeo_1_name="default",
surface_2_name="default",
DVGeo_2_name="default",
rho=50.0,
heuristic_dist=None,
max_perim=3.0,
addToPyOpt=True,
):
self.DVCon.addTriangulatedSurfaceConstraint(
comm=self.comm,
surface_1_name=surface_1_name,
DVGeo_1_name=DVGeo_1_name,
surface_2_name=surface_2_name,
DVGeo_2_name=DVGeo_2_name,
rho=rho,
heuristic_dist=heuristic_dist,
max_perim=max_perim,
name=name,
addToPyOpt=addToPyOpt,
)
self.add_output(f"{name}_KS", distributed=False, val=0)
self.add_output(f"{name}_perim", distributed=False, val=0)
def nom_setConstraintSurface(
self, surface, name="default", addToDVGeo=False, DVGeoName="default", surfFormat="point-vector"
):
# constraint needs a triangulated reference surface at initialization
self.DVCon.setSurface(surface, name=name, addToDVGeo=addToDVGeo, DVGeoName=DVGeoName, surfFormat=surfFormat)
def compute_jacvec_product(self, inputs, d_inputs, d_outputs, mode):
# only do the computations when we have more than zero entries in d_inputs
# in the reverse mode or d_outputs in the forward mode
doRev = mode == "rev" and len(list(d_inputs.keys())) > 0
doFwd = mode == "fwd" and len(list(d_outputs.keys())) > 0
if doFwd or doRev:
# this flag will be set to True after every compute call.
# if it is true, we assume the design has changed so we re-run the sensitivity update
# there can be hundreds of calls to this routine due to thickness constraints,
# as a result, we only run the actual sensitivity comp once and save the jacobians
# this might be better suited with the matrix-based API
if self.update_jac:
self.constraintfuncsens = dict()
self.DVCon.evalFunctionsSens(self.constraintfuncsens, includeLinear=True)
# set the flag to False so we dont run the update again if this is called w/o a compute in between
self.update_jac = False
# Directly do Jacobian vector product with the derivatives from DVConstraints
for constraintname in self.constraintfuncsens:
for dvname in self.constraintfuncsens[constraintname]:
if constraintname in d_outputs and dvname in d_inputs:
dcdx = self.constraintfuncsens[constraintname][dvname]
if doFwd:
din = d_inputs[dvname]
jvtmp = np.dot(dcdx, din)
d_outputs[constraintname] += jvtmp
elif doRev:
dout = d_outputs[constraintname]
jvtmp = np.dot(np.transpose(dcdx), dout)
d_inputs[dvname] += jvtmp
for _, DVGeo in self.DVGeos.items():
dvs = DVGeo.getVarNames()
# Collet point sets from all DVGeos if DVGeometryMulti object
if isinstance(DVGeo, DVGeometryMulti):
ptSetNames = []
for comp in DVGeo.DVGeoDict.keys():
for ptSet in DVGeo.DVGeoDict[comp].ptSetNames:
if ptSet not in ptSetNames:
ptSetNames.append(ptSet)
else:
ptSetNames = DVGeo.ptSetNames
for ptSetName in ptSetNames:
if ptSetName in self.omPtSetList:
# Process the seeds
if doFwd:
# Collect the d_inputs associated with the current DVGeo
seeds = {}
for k in d_inputs:
if k in dvs:
seeds[k] = d_inputs[k]
elif doRev:
seeds = d_outputs[ptSetName].reshape(len(d_outputs[ptSetName]) // 3, 3)
# only do the calc. if seeds are not zero on ANY proc
local_all_zeros = np.all(seeds == 0)
global_all_zeros = np.zeros(1, dtype=bool)
# we need to communicate for this check otherwise we may hang
self.comm.Allreduce([local_all_zeros, MPI.BOOL], [global_all_zeros, MPI.BOOL], MPI.LAND)
# Compute the Jacobian vector product
if not global_all_zeros[0]:
if doFwd:
d_outputs[ptSetName] += DVGeo.totalSensitivityProd(seeds, ptSetName)
elif doRev:
# TODO totalSensitivityTransProd is broken. does not work with zero surface nodes on a proc
# xdot = DVGeo.totalSensitivityTransProd(dout, ptSetName)
xdot = DVGeo.totalSensitivity(seeds, ptSetName)
# loop over dvs and accumulate
xdotg = {}
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)
# accumulate in the dict
d_inputs[k] += xdotg[k][0]