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

Adding custom reference axis projections #168

Merged
merged 8 commits into from
Dec 5, 2022
Merged
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
67 changes: 52 additions & 15 deletions pygeo/parameterization/DVGeo.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,9 +329,32 @@ def addRefAxis(
7. z-x-y + rot_theta
8. z-x-y + rotation about section axis (to allow for winglet rotation)

axis: str
Axis along which to project points/control points onto the
ref axis. Default is `x` which will project rays.
axis: str or numpy array of size 3
This parameter controls how the links between the control points
and the reference axis are computed. If the value is set to
"x", "y", or "z", then the code will extend rays out from the
control points in the direction specified in the "axis" variable,
and it will compute the projection of the ray to the reference axis.
This returns a point on the reference axis, which is taken as the
other end of the link of the control point to the reference axis,
where the other point of the link is the control point itself.
This approach works well enough for most cases, but may not be
ideal when the reference axis sits at an angle (e.g. wing with
dihedral). For these cases, setting the axis value with an array
of size 3 is the better approach. When axis is set to an array of
size 3, the code creates a plane that goes through each control point
with the normal that is defined by the direction of the axis parameter.
Then the end of the links are computed by finding the intersection of this
plane with the reference axis. The benefit of this approach is that
all of the reference axis links will lie on the same plane if the original
FFD control points were planar on each section. E.g., a wing FFD might have
x chordwise, y spanwise out, and z up, but with a dihedral. The FFD
control points for each spanwise section can lie on the x-z plane.
In this case, we want the links to be in a constant-y plane. To achieve
this, we can set the axis variable to [0, 1, 0], which defines the normal
of the plane we want. If you want to modify this option and see its effects,
consider writing the links between control points and the referece axis using
the "writeLinks" method in this class.

alignIndex: str
FFD axis along which the reference axis will lie. Can be `i`, `j`,
Expand Down Expand Up @@ -394,15 +417,6 @@ def addRefAxis(
# We don't do any of the final processing here; we simply
# record the information the user has supplied into a
# dictionary structure.
if axis is None:
pass
elif axis.lower() == "x":
axis = np.array([1, 0, 0], "d")
elif axis.lower() == "y":
axis = np.array([0, 1, 0], "d")
elif axis.lower() == "z":
axis = np.array([0, 0, 1], "d")

if curve is not None:
# Explicit curve has been supplied:
if self.FFD.symmPlane is None:
Expand Down Expand Up @@ -3197,9 +3211,32 @@ def _finalize(self):
if self.axis[key]["axis"] is None:
tmpIDs, tmpS0 = self.refAxis.projectPoints(curPts, curves=[curveID])
else:
tmpIDs, tmpS0 = self.refAxis.projectRays(
curPts, self.axis[key]["axis"], curves=[curveID], raySize=self.axis[key]["raySize"]
)

if isinstance(self.axis[key]["axis"], str) and len(self.axis[key]["axis"]) == 1:
# The axis can be a string of length one.
# If so, we follow the ray projection approach.
if self.axis[key]["axis"].lower() == "x":
axis = np.array([1, 0, 0], "d")
elif self.axis[key]["axis"].lower() == "y":
axis = np.array([0, 1, 0], "d")
elif self.axis[key]["axis"].lower() == "z":
axis = np.array([0, 0, 1], "d")
tmpIDs, tmpS0 = self.refAxis.projectRays(
curPts, axis, curves=[curveID], raySize=self.axis[key]["raySize"]
)

elif isinstance(self.axis[key]["axis"], np.ndarray) and len(self.axis[key]["axis"]) == 3:
# we want to intersect a plane that crosses the cur pts and the normal
# defined by the "axis" parameter used when adding the ref axis.
tmpIDs, tmpS0 = self.refAxis.intersectPlanes(
curPts, self.axis[key]["axis"], curves=[curveID], raySize=self.axis[key]["raySize"]
)
else:
raise Error(
"The 'axis' parameter when adding the reference axis must be a single character "
"specifying the direction ('x', 'y', or 'z') or a numpy array of size 3 that "
"defines the normal of the plane which will be used for reference axis projections."
)

curveIDs.extend(tmpIDs)
s.extend(tmpS0)
Expand Down
148 changes: 144 additions & 4 deletions pygeo/pyNetwork.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# ======================================================================
import os
import numpy as np
from pyspline import Surface
from pyspline.utils import openTecplot, writeTecplot1D, closeTecplot, line
from .topology import CurveTopology

Expand Down Expand Up @@ -55,17 +56,26 @@ def _doConnectivity(self):
# Curve Writing Output Functions
# ----------------------------------------------------------------------

def writeTecplot(self, fileName, orig=False, curves=True, coef=True, curveLabels=False, nodeLabels=False):
def writeTecplot(
self, fileName, orig=False, curves=True, coef=True, current=False, curveLabels=False, nodeLabels=False
):
"""Write the pyNetwork Object to Tecplot .dat file

Parameters
----------
fileName : str
File name for tecplot file. Should have .dat extension
orig : bool
Flag to determine if we will write the original X data used to
create this object
curves : bool
Flag to write discrete approximation of the actual curve
coef : bool
Flag to write b-spline coefficients
current : bool
Flag to determine if the current line is evaluated and added
to the file. This is useful for higher order curves (k>2) where
the coef array does not directly represent the curve.
curveLabels : bool
Flag to write a separate label file with the curve indices
nodeLabels : bool
Expand All @@ -82,7 +92,12 @@ def writeTecplot(self, fileName, orig=False, curves=True, coef=True, curveLabels
writeTecplot1D(f, "coef", self.curves[icurve].coef)
if orig:
for icurve in range(self.nCurve):
writeTecplot1D(f, "coef", self.curves[icurve].X)
writeTecplot1D(f, "orig_data", self.curves[icurve].X)
if current:
# evaluate the curve with the current coefs and write
for icurve in range(self.nCurve):
current_line = self.curves[icurve](np.linspace(0, 1, 201))
writeTecplot1D(f, "current_interp", current_line)

# Write out The Curve and Node Labels
dirName, fileName = os.path.split(fileName)
Expand Down Expand Up @@ -223,7 +238,7 @@ def projectRays(self, points, axis, curves=None, raySize=1.5, **kwargs):
curveID : int
The index of the curve with the closest distance
s : float or array
The curve parameter on self.curves[curveID] that is cloested
The curve parameter on self.curves[curveID] that is closest
to the point(s).
"""

Expand Down Expand Up @@ -296,7 +311,7 @@ def projectPoints(self, points, *args, curves=None, **kwargs):
curveID : int
The index of the curve with the closest distance
s : float or array
The curve parameter on self.curves[curveID] that is cloested
The curve parameter on self.curves[curveID] that is closest
to the point(s).
"""

Expand Down Expand Up @@ -325,3 +340,128 @@ def projectPoints(self, points, *args, curves=None, **kwargs):
curveID[i] = curves[j]

return curveID, s

def intersectPlanes(self, points, axis, curves=None, raySize=1.5, **kwargs):
"""Find the intersection of the curves with the plane defined by the points and
the normal vector. The ray size is used to define the extent of the plane
about the points. The closest intersection to the original point is taken.
The plane normal is determined by the "axis" parameter

Parameters
----------
points : array
A single point (array length 3) or a set of points (N,3) array
that lies on the plane. If multiple points are provided, one plane
is defined with each point.
axis : array of size 3
Normal of the plane.
curves : list
An optional list of curve indices to use. If not given, all
curve objects are used.
raySize : float
To define the plane, we use the point coordinates and the normal direction.
The plane is extended by raySize in all directions.
kwargs : dict
Keyword arguments passed to Surface.projectCurve() function

Returns
-------
curveID : int
The index of the curve with the closest distance
s : float or array
The curve parameter on self.curves[curveID] that is closest
to the point(s).
"""

# given the normal vector in the axis parameter, we need to find two directions
# that lie on the plane.

# normalize axis
axis /= np.linalg.norm(axis)

# we now need to pick one direction that is not aligned with axis.
# To do this, pick the smallest absolute component of the axis parameter.
# we start with a unit vector in this direction, which is almost guaranteed
# to be not perfectly aligned with the axis vector.
dir1_ind = np.argmin(np.abs(axis))
dir1 = np.zeros(3)
dir1[dir1_ind] = 1.0

# then we find the orthogonal component of dir1 to axis. this is the final dir1
dir1 -= axis * axis.dot(dir1)
dir1 /= np.linalg.norm(dir1)

# get the third vector with a cross product
dir2 = np.cross(axis, dir1)
dir2 /= np.linalg.norm(dir2)

# finally, we want to scale dir1 and dir2 by ray size. This controls
# the size of the plane we create. Needs to be big enough to intersect
# the curve.
dir1 *= raySize
dir2 *= raySize

if curves is None:
curves = np.arange(self.nCurve)

N = len(points)
S = np.zeros((N, len(curves)))
D = np.zeros((N, len(curves), 3))

for i in range(len(curves)):
icurve = curves[i]
for j in range(N):

# we need to initialize a pySurface object for this point
# the point is perturbed in dir 1 and dir2 to get 4 corners of the plane
point = points[j]

coef = np.zeros((2, 2, 3))
# indexing:
# 3 ------ 2
# | |
# | pt |
# | |
# 0 ------ 1
# ^ dir2
# |
# |
# ---> dir 1
coef[0, 0] = point - dir1 - dir2
coef[1, 0] = point + dir1 - dir2
coef[1, 1] = point + dir1 + dir2
coef[0, 1] = point - dir1 + dir2

ku = 2
kv = 2
tu = np.array([0.0, 0.0, 1.0, 1.0])
tv = np.array([0.0, 0.0, 1.0, 1.0])

surf = Surface(ku=ku, kv=kv, tu=tu, tv=tv, coef=coef)

# now we project the current curve to this plane
u, v, S[j, i], D[j, i, :] = surf.projectCurve(self.curves[icurve], **kwargs)

if u == 0.0 or u == 1.0 or v == 0.0 or v == 1.0:
print(
"Warning: The link for attached point {:d} was drawn "
"from the curve to the end of the plane, "
"indicating that the plane might not have been large "
"enough to intersect the nearest curve.".format(j)
sseraj marked this conversation as resolved.
Show resolved Hide resolved
)

s = np.zeros(N)
curveID = np.zeros(N, "intc")

# Now post-process to get the lowest one
for i in range(N):
d0 = np.linalg.norm(D[i, 0])
s[i] = S[i, 0]
curveID[i] = curves[0]
for j in range(len(curves)):
if np.linalg.norm(D[i, j]) < d0:
d0 = np.linalg.norm(D[i, j])
s[i] = S[i, j]
curveID[i] = curves[j]

return curveID, s
Loading