Source code for aerocaps.geom.surfaces

"""
Parametric surface classes (two-dimensional geometric objects defined by parameters :math:`u` and :math:`v`
that reside in three-dimensional space)
"""
import typing
from copy import deepcopy
from enum import Enum

import numpy as np
import pyvista as pv
import shapely
from rust_nurbs import *
from scipy.optimize import fsolve, minimize, OptimizeResult

import aerocaps.iges.curves
import aerocaps.iges.entity
import aerocaps.iges.surfaces
from aerocaps.geom.transformation import transform_points_into_coordinate_system, Transformation3D
from aerocaps.geom import Surface, InvalidGeometryError, NegativeWeightError, Geometry3D
from aerocaps.geom.curves import BezierCurve3D, Line3D, RationalBezierCurve3D, NURBSCurve3D, BSplineCurve3D, \
    CurveOnParametricSurface, CompositeCurve3D
from aerocaps.geom.plane import Plane
from aerocaps.geom.point import Point3D
from aerocaps.geom.tools import project_point_onto_line, measure_distance_point_line, rotate_point_about_axis, \
    add_vector_to_point, concave_hull
from aerocaps.geom.vector import Vector3D, IHat3D, JHat3D, KHat3D
from aerocaps.units.angle import Angle
from aerocaps.units.length import Length
from aerocaps.utils.array import unique_with_tolerance

__all__ = [
    "SurfaceEdge",
    "SurfaceCorner",
    "BezierSurface",
    "RationalBezierSurface",
    "BSplineSurface",
    "NURBSSurface",
    "TrimmedSurface"
]


[docs] class SurfaceEdge(Enum): """ Enum describing the name of each edge of a four-sided surface. The names are defined by the name and value of the parameter that is constant along the edge. .. figure:: ../images/cardinal_transparent.* :width: 300 :align: center Surface edge nomenclature """ v1 = 0 v0 = 1 u1 = 2 u0 = 3
[docs] class SurfaceCorner(Enum): u1v1 = 0 u0v1 = 1 u0v0 = 2 u1v0 = 3
[docs] class BezierSurface(Surface): """ Bézier surface class. A NURBS surface with no internal knots and all weights equal to unity. """
[docs] def __init__(self, points: typing.List[typing.List[Point3D]] or np.ndarray, name: str = "BezierSurface", construction: bool = False): r""" A Bézier surface is a parametric surface described by a matrix of control points and defined on a rectangular domain :math:`\{u \in [0,1], v \in [0,1]\}`. The mathematical expression for the Bézier surface is identical to that of the Bézier curve except with an extra dimension: .. math:: \mathbf{S}(u,v) = \sum\limits_{i=0}^n \sum\limits_{j=0}^m B_{i,n}(u) B_{j,m}(v) \mathbf{P}_{i,j} Where :math:`B_{i,n}(t)` is the Bernstein polynomial given by .. math:: B_{i,n}(t) = {n \choose i} t^i (1-t)^{n-i} An example of a Bézier surface with :math:`n=2` and :math:`m=3` is shown below. Note that the only control points that lie directly on the surface are the corner points of the control point mesh. This is analogous to the fact that only the starting and ending control points of Bézier curves lie directly on the curve. In fact, Bézier curves derived from the bounding rows and columns of control points exactly represent the boundary curves of the surface. In this example, the control points given by :math:`\mathbf{P}_{i,j=0}` and :math:`\mathbf{P}_{i,j=m}` represent quadratic Bézier curves (:math:`n=2`), and the control points given by :math:`\mathbf{P}_{i=0,j}` and :math:`\mathbf{P}_{i=n,j}` represent cubic Bézier curves (:math:`m=3`). .. figure:: ../images/bezier_surf_2x3.* :width: 600 :align: center A :math:`2 \times 3` Bézier surface with control points and control point net lines shown .. figure:: ../images/bezier_surf_2x3_mesh_only.* :width: 600 :align: center A :math:`2 \times 3` Bézier surface with isoparametric curves in both :math:`u` and :math:`v` shown Bézier surfaces can be constructed either via the default constructor with a nested list of ``aerocaps.geom.point.Point3D`` objects of by a 3-D ``numpy`` array. For example, say we have six ``Point3D`` objects, A-F and would like to use them to create a :math:`2 \times 1` Bézier surface. Using the default constructor with the point objects, .. code-block:: python surf = BezierSurface([[pA, pB], [pC, pD], [pE, pF]]) Using the array class method and point :math:`xyz` float values given by ``pA_x``, ``pA_y``, ``pA_z``, etc., .. code-block:: python control_points = np.array([ [[pA_x, pA_y, pA_z], [pB_x, pB_y, pB_z]], [[pC_x, pC_y, pC_z], [pD_x, pD_y, pD_z]], [[pE_x, pE_y, pE_z], [pF_x, pF_y, pF_z]], ]) surf = BezierSurface(control_points) Parameters ---------- points: typing.List[typing.List[Point3D]] or numpy.ndarray Control points for the Bézier surface, either as a nested list of :obj:`~aerocaps.geom.point.Point3D` objects or an :obj:`~numpy.ndarray` of size :math:`(n+1) \times (m+1) \times 3`, where :math:`n` is the surface degree in the :math:`u`-parametric direction and :math:`m` is the surface degree in the :math:`v`-parametric direction name: str Name of the geometric object. May be re-assigned a unique name when added to a :obj:`~aerocaps.geom.geometry_container.GeometryContainer`. Default: 'BezierSurface' construction: bool Whether this is a geometry used only for construction of other geometries. If ``True``, this geometry will not be exported or plotted. Default: ``False`` """ if isinstance(points, np.ndarray): points = [[Point3D.from_array(pt_row) for pt_row in pt_mat] for pt_mat in points] self.points = points super().__init__(name=name, construction=construction)
@property def n_points_u(self) -> int: """Number of control points in the :math:`u`-parametric direction""" return len(self.points) @property def n_points_v(self) -> int: """Number of control points in the :math:`v`-parametric direction""" return len(self.points[0]) @property def degree_u(self) -> int: """Surface degree in the :math:`u`-parametric direction""" return self.n_points_u - 1 @property def degree_v(self) -> int: """Surface degree in the :math:`v`-parametric direction""" return self.n_points_v - 1 @property def n(self) -> int: """ Shorthand for :obj:`~aerocaps.geom.surfaces.BezierSurface.degree_u` Returns ------- int Surface degree in the :math:`u`-parametric direction """ return self.degree_u @property def m(self) -> int: """ Shorthand for :obj:`~aerocaps.geom.surfaces.BezierSurface.degree_v` Returns ------- int Surface degree in the :math:`v`-parametric direction """ return self.degree_v
[docs] def to_iges(self, *args, **kwargs) -> aerocaps.iges.entity.IGESEntity: """ Converts the Bézier surface to an IGES entity. To add this IGES entity to an ``.igs`` file, use an :obj:`~aerocaps.iges.iges_generator.IGESGenerator`. """ return aerocaps.iges.surfaces.BezierSurfaceIGES(self.get_control_point_array())
[docs] def to_rational_bezier_surface(self) -> "RationalBezierSurface": """ Converts the current non-rational Bézier surface to a rational Bézier surface by setting all weights to unity. Returns ------- RationalBezierSurface Converted surface """ return RationalBezierSurface(self.points, np.ones((self.degree_u + 1, self.degree_v + 1)))
[docs] def get_control_point_array(self) -> np.ndarray: """ Converts the nested list of control points to a 3-D :obj:`~numpy.ndarray`. Returns ------- numpy.ndarray 3-D array """ return np.array([np.array([p.as_array() for p in p_arr]) for p_arr in self.points])
[docs] @classmethod def from_curve_extrude(cls, curve: BezierCurve3D, distance: Length, extrude_axis: Vector3D = None, symmetric: bool = False, reverse: bool = False): """ Creates a Bézier surface by extruding a Bézier curve along an axis. .. important:: If the input curve is linear, the ``extrude_axis`` argument must be specified. Parameters ---------- curve: BezierCurve3D Curve to extrude. The most common use case is a planar curve, but this is not required. distance: Length Distance along the axis to extrude extrude_axis: Vector3D Optional direct specification of the extrusion axis. If not specified, a vector normal to the plane containing the first, second, and last control points of the curve is used. Default: ``None`` symmetric: bool Whether to extrude in both directions. Default: ``False`` reverse: bool Whether to flip the extrusion vector. Default: ``False`` Returns ------- BezierSurface Extruded surface """ # Input validation if curve.degree < 2 and extrude_axis is None: raise ValueError("For linear Bézier curves (those with only two control points), " "the 'extrude_axis` argument must be specified") # Get the axis along which to extrude if extrude_axis is None: plane = Plane(p0=curve.control_points[0], p1=curve.control_points[1], p2=curve.control_points[-1]) extrude_axis = plane.compute_normal() else: extrude_axis = extrude_axis.get_normalized_vector() # Get the scaled extrusion vectors extrude_vec_forward = extrude_axis.scale(distance.m) extrude_vec_backward = extrude_axis.scale(distance.m) # Get the start and end point lists for the surface if symmetric: start_points = [add_vector_to_point(extrude_vec_forward, p) for p in curve.control_points] end_points = [add_vector_to_point(extrude_vec_backward, p) for p in curve.control_points] else: start_points = curve.control_points if reverse: end_points = [add_vector_to_point(extrude_vec_backward, p) for p in curve.control_points] else: end_points = [add_vector_to_point(extrude_vec_forward, p) for p in curve.control_points] # Create the extruded surface surf = cls([start_points, end_points]) return surf
[docs] def dSdu(self, u: float, v: float) -> np.ndarray: r""" Evaluates the first derivative with respect to :math:`u` at a single :math:`(u,v)` pair Parameters ---------- u: float Position along :math:`u` in parametric space. Normally in the range :math:`[0,1]` v: float Position along :math:`v` in parametric space. Normally in the range :math:`[0,1]` Returns ------- np.ndarray 1-D array containing the :math:`x`-, :math:`y`-, and :math:`z`-components of the second derivative """ P = self.get_control_point_array() return np.array(bezier_surf_dsdu(P, u, v))
[docs] def dSdu_grid(self, Nu: int, Nv: int) -> np.ndarray: r""" Evaluates the first derivative with respect to :math:`u` on a linearly-spaced grid of :math:`u`- and :math:`v`-values. Parameters ---------- Nu: int Number of evenly spaced :math:`u` values Nv: int Number of evenly spaced :math:`v` values Returns ------- np.ndarray Array of size :math:`N_u \times N_v \times 3` """ P = self.get_control_point_array() return np.array(bezier_surf_dsdu_grid(P, Nu, Nv))
[docs] def dSdu_uvvecs(self, u: np.ndarray, v: np.ndarray): r""" Evaluates the first derivative of the surface with respect to :math:`u` at arbitrary vectors of :math:`u` and :math:`v`-values. Parameters ---------- u: np.ndarray 1-D array of :math:`u`-parameter values v: np.ndarray 1-D array of :math:`v`-parameter values Returns ------- np.ndarray Array of size :math:`\text{len}(u) \times \text{len}(v) \times 3` """ P = self.get_control_point_array() return np.array(bezier_surf_dsdu_uvvecs(P, u, v))
[docs] def dSdv(self, u: float or np.ndarray, v: float or np.ndarray): r""" Evaluates the first derivative with respect to :math:`v` at a single :math:`(u,v)` pair Parameters ---------- u: float Position along :math:`u` in parametric space. Normally in the range :math:`[0,1]` v: float Position along :math:`v` in parametric space. Normally in the range :math:`[0,1]` Returns ------- np.ndarray 1-D array containing the :math:`x`-, :math:`y`-, and :math:`z`-components of the second derivative """ P = self.get_control_point_array() return np.array(bezier_surf_dsdv(P, u, v))
[docs] def dSdv_grid(self, Nu: int, Nv: int) -> np.ndarray: r""" Evaluates the first derivative with respect to :math:`v` on a linearly-spaced grid of :math:`u`- and :math:`v`-values. Parameters ---------- Nu: int Number of evenly spaced :math:`u` values Nv: int Number of evenly spaced :math:`v` values Returns ------- np.ndarray Array of size :math:`N_u \times N_v \times 3` """ P = self.get_control_point_array() return np.array(bezier_surf_dsdv_grid(P, Nu, Nv))
[docs] def dSdv_uvvecs(self, u: np.ndarray, v: np.ndarray): r""" Evaluates the first derivative of the surface with respect to :math:`v` at arbitrary vectors of :math:`u` and :math:`v`-values. Parameters ---------- u: np.ndarray 1-D array of :math:`u`-parameter values v: np.ndarray 1-D array of :math:`v`-parameter values Returns ------- np.ndarray Array of size :math:`\text{len}(u) \times \text{len}(v) \times 3` """ P = self.get_control_point_array() return np.array(bezier_surf_dsdv_uvvecs(P, u, v))
[docs] def d2Sdu2(self, u: float, v: float) -> np.ndarray: r""" Evaluates the second derivative with respect to :math:`u` at a single :math:`(u,v)` pair Parameters ---------- u: float Position along :math:`u` in parametric space. Normally in the range :math:`[0,1]` v: float Position along :math:`v` in parametric space. Normally in the range :math:`[0,1]` Returns ------- np.ndarray 1-D array containing the :math:`x`-, :math:`y`-, and :math:`z`-components of the second derivative """ P = self.get_control_point_array() return np.array(bezier_surf_d2sdu2(P, u, v))
[docs] def d2Sdu2_grid(self, Nu: int, Nv: int) -> np.ndarray: r""" Evaluates the second derivative with respect to :math:`u` on a linearly-spaced grid of :math:`u`- and :math:`v`-values. Parameters ---------- Nu: int Number of evenly spaced :math:`u` values Nv: int Number of evenly spaced :math:`v` values Returns ------- np.ndarray Array of size :math:`N_u \times N_v \times 3` """ P = self.get_control_point_array() return np.array(bezier_surf_d2sdu2_grid(P, Nu, Nv))
[docs] def d2Sdu2_uvvecs(self, u: np.ndarray, v: np.ndarray): r""" Evaluates the second derivative of the surface with respect to :math:`u` at arbitrary vectors of :math:`u` and :math:`v`-values. Parameters ---------- u: np.ndarray 1-D array of :math:`u`-parameter values v: np.ndarray 1-D array of :math:`v`-parameter values Returns ------- np.ndarray Array of size :math:`\text{len}(u) \times \text{len}(v) \times 3` """ P = self.get_control_point_array() return np.array(bezier_surf_d2sdu2_uvvecs(P, u, v))
[docs] def d2Sdv2(self, u: float or np.ndarray, v: float or np.ndarray): r""" Evaluates the second derivative with respect to :math:`v` at a single :math:`(u,v)` pair Parameters ---------- u: float Position along :math:`u` in parametric space. Normally in the range :math:`[0,1]` v: float Position along :math:`v` in parametric space. Normally in the range :math:`[0,1]` Returns ------- np.ndarray 1-D array containing the :math:`x`-, :math:`y`-, and :math:`z`-components of the second derivative """ P = self.get_control_point_array() return np.array(bezier_surf_d2sdv2(P, u, v))
[docs] def d2Sdv2_grid(self, Nu: int, Nv: int) -> np.ndarray: r""" Evaluates the second derivative with respect to :math:`v` on a linearly-spaced grid of :math:`u`- and :math:`v`-values. Parameters ---------- Nu: int Number of evenly spaced :math:`u` values Nv: int Number of evenly spaced :math:`v` values Returns ------- np.ndarray Array of size :math:`N_u \times N_v \times 3` """ P = self.get_control_point_array() return np.array(bezier_surf_d2sdv2_grid(P, Nu, Nv))
[docs] def d2Sdv2_uvvecs(self, u: np.ndarray, v: np.ndarray): r""" Evaluates the second derivative of the surface with respect to :math:`v` at arbitrary vectors of :math:`u` and :math:`v`-values. Parameters ---------- u: np.ndarray 1-D array of :math:`u`-parameter values v: np.ndarray 1-D array of :math:`v`-parameter values Returns ------- np.ndarray Array of size :math:`\text{len}(u) \times \text{len}(v) \times 3` """ P = self.get_control_point_array() return np.array(bezier_surf_d2sdv2_uvvecs(P, u, v))
[docs] def get_edge(self, edge: SurfaceEdge, n_points: int = 10) -> np.ndarray: r""" Evaluates the surface at ``n_points`` parameter locations along a given edge. Parameters ---------- edge: SurfaceEdge Edge along which to evaluate n_points: int Number of evenly-spaced parameter locations at which to evaluate the edge curve. Default: 10 Returns ------- numpy.ndarray 2-D array of size :math:`n_\text{points} \times 3` """ P = self.get_control_point_array() if edge == SurfaceEdge.v1: return np.array(bezier_surf_eval_iso_v(P, n_points, 1.0)) elif edge == SurfaceEdge.v0: return np.array(bezier_surf_eval_iso_v(P, n_points, 0.0)) elif edge == SurfaceEdge.u1: return np.array(bezier_surf_eval_iso_u(P, 1.0, n_points)) elif edge == SurfaceEdge.u0: return np.array(bezier_surf_eval_iso_u(P, 0.0, n_points)) else: raise ValueError(f"No edge called {edge}")
[docs] def get_first_derivs_along_edge(self, edge: SurfaceEdge, n_points: int = 10, perp: bool = True) -> np.ndarray: r""" Evaluates the parallel or perpendicular derivative along a surface edge at ``n_points`` parameter locations. The derivative represents either :math:`\frac{\partial \mathbf{S}(u,v)}{\partial u}` or :math:`\frac{\partial \mathbf{S}(u,v)}{\partial v}` depending on which edge is selected and which value is assigned to ``perp``. Parameters ---------- edge: SurfaceEdge Edge along which to evaluate n_points: int Number of evenly-spaced parameter locations at which to evaluate the derivative. Default: 10 perp: bool Whether to evaluate the cross-derivative. If ``False``, the derivative along the parameter direction parallel to the edge will be evaluated instead. Default: ``True`` Returns ------- numpy.ndarray 2-D array of size :math:`n_\text{points} \times 3` """ P = self.get_control_point_array() if edge == SurfaceEdge.v1: return np.array(bezier_surf_dsdv_iso_v(P, n_points, 1.0)) if perp else np.array( bezier_surf_dsdu_iso_v(P, n_points, 1.0)) elif edge == SurfaceEdge.v0: return np.array(bezier_surf_dsdv_iso_v(P, n_points, 0.0)) if perp else np.array( bezier_surf_dsdu_iso_v(P, n_points, 0.0)) elif edge == SurfaceEdge.u1: return np.array(bezier_surf_dsdu_iso_u(P, 1.0, n_points)) if perp else np.array( bezier_surf_dsdv_iso_u(P, 1.0, n_points)) elif edge == SurfaceEdge.u0: return np.array(bezier_surf_dsdu_iso_u(P, 0.0, n_points)) if perp else np.array( bezier_surf_dsdv_iso_u(P, 0.0, n_points)) else: raise ValueError(f"No edge called {edge}")
[docs] def get_first_deriv_cp_sens_along_edge(self, edge: SurfaceEdge, i: int, j: int, n_points: int = 10, perp: bool = True) -> np.ndarray: r""" Gets the sensitivity of the first :math:`u`- or :math:`v`-derivative along an edge with respect to control point :math:`\mathbf{P}_{i,j}` Parameters ---------- edge: SurfaceEdge Edge along which to evaluate i: int :math:`i`-index of the control point j: int :math:`j`-index of the control point n_points: int Number of evenly-spaced parameter locations at which to evaluate the derivative. Default: 10 perp: bool Whether to evaluate the cross-derivative. If ``False``, the derivative along the parameter direction parallel to the edge will be evaluated instead. Default: ``True`` Returns ------- numpy.ndarray 2-D array of size :math:`n_\text{points} \times 3` """ if edge == SurfaceEdge.v1: return np.array(bezier_surf_dsdv_dp_iso_v(i, j, self.n, self.m, 3, n_points, 1.0)) if perp else np.array( bezier_surf_dsdu_dp_iso_v(i, j, self.n, self.m, 3, n_points, 1.0)) elif edge == SurfaceEdge.v0: return np.array(bezier_surf_dsdv_dp_iso_v(i, j, self.n, self.m, 3, n_points, 0.0)) if perp else np.array( bezier_surf_dsdu_dp_iso_v(i, j, self.n, self.m, 3, n_points, 0.0)) elif edge == SurfaceEdge.u1: return np.array(bezier_surf_dsdu_dp_iso_u(i, j, self.n, self.m, 3, 1.0, n_points)) if perp else np.array( bezier_surf_dsdv_dp_iso_u(i, j, self.n, self.m, 3, 1.0, n_points)) elif edge == SurfaceEdge.u0: return np.array(bezier_surf_dsdu_dp_iso_u(i, j, self.n, self.m, 3, 0.0, n_points)) if perp else np.array( bezier_surf_dsdv_dp_iso_u(i, j, self.n, self.m, 3, 0.0, n_points)) else: raise ValueError(f"No edge called {edge}")
[docs] def get_second_derivs_along_edge(self, edge: SurfaceEdge, n_points: int = 10, perp: bool = True) -> np.ndarray: r""" Evaluates the parallel or perpendicular second derivative along a surface edge at ``n_points`` parameter locations. The derivative represents either :math:`\frac{\partial^2 \mathbf{S}(u,v)}{\partial u^2}` or :math:`\frac{\partial^2 \mathbf{S}(u,v)}{\partial v^2}` depending on which edge is selected and which value is assigned to ``perp``. Parameters ---------- edge: SurfaceEdge Edge along which to evaluate n_points: int Number of evenly-spaced parameter locations at which to evaluate the second derivative. Default: 10 perp: bool Whether to evaluate the cross-derivative. If ``False``, the second derivative along the parameter direction parallel to the edge will be evaluated instead. Default: ``True`` Returns ------- numpy.ndarray 2-D array of size :math:`n_\text{points} \times 3` """ P = self.get_control_point_array() if edge == SurfaceEdge.v1: return np.array(bezier_surf_d2sdv2_iso_v(P, n_points, 1.0)) if perp else np.array( bezier_surf_d2sdu2_iso_v(P, n_points, 1.0)) elif edge == SurfaceEdge.v0: return np.array(bezier_surf_d2sdv2_iso_v(P, n_points, 0.0)) if perp else np.array( bezier_surf_d2sdu2_iso_v(P, n_points, 0.0)) elif edge == SurfaceEdge.u1: return np.array(bezier_surf_d2sdu2_iso_u(P, 1.0, n_points)) if perp else np.array( bezier_surf_d2sdv2_iso_u(P, 1.0, n_points)) elif edge == SurfaceEdge.u0: return np.array(bezier_surf_d2sdu2_iso_u(P, 0.0, n_points)) if perp else np.array( bezier_surf_d2sdv2_iso_u(P, 0.0, n_points)) else: raise ValueError(f"No edge called {edge}")
[docs] def get_second_deriv_cp_sens_along_edge(self, edge: SurfaceEdge, i: int, j: int, n_points: int = 10, perp: bool = True) -> np.ndarray: r""" Gets the sensitivity of the second :math:`u`- or :math:`v`-derivative along an edge with respect to control point :math:`\mathbf{P}_{i,j}` Parameters ---------- edge: SurfaceEdge Edge along which to evaluate i: int :math:`i`-index of the control point j: int :math:`j`-index of the control point n_points: int Number of evenly-spaced parameter locations at which to evaluate the derivative. Default: 10 perp: bool Whether to evaluate the cross-derivative. If ``False``, the derivative along the parameter direction parallel to the edge will be evaluated instead. Default: ``True`` Returns ------- numpy.ndarray 2-D array of size :math:`n_\text{points} \times 3` """ if edge == SurfaceEdge.v1: return np.array(bezier_surf_d2sdv2_dp_iso_v(i, j, self.n, self.m, 3, n_points, 1.0)) if perp else np.array( bezier_surf_d2sdu2_dp_iso_v(i, j, self.n, self.m, 3, n_points, 1.0)) elif edge == SurfaceEdge.v0: return np.array(bezier_surf_d2sdv2_dp_iso_v(i, j, self.n, self.m, 3, n_points, 0.0)) if perp else np.array( bezier_surf_d2sdu2_dp_iso_v(i, j, self.n, self.m, 3, n_points, 0.0)) elif edge == SurfaceEdge.u1: return np.array(bezier_surf_d2sdu2_dp_iso_u(i, j, self.n, self.m, 3, 1.0, n_points)) if perp else np.array( bezier_surf_d2sdv2_dp_iso_u(i, j, self.n, self.m, 3, 1.0, n_points)) elif edge == SurfaceEdge.u0: return np.array(bezier_surf_d2sdu2_dp_iso_u(i, j, self.n, self.m, 3, 0.0, n_points)) if perp else np.array( bezier_surf_d2sdv2_dp_iso_u(i, j, self.n, self.m, 3, 0.0, n_points)) else: raise ValueError(f"No edge called {edge}")
[docs] def verify_g0(self, other: "BezierSurface", surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge, n_points: int = 10): r""" Verifies that two Bézier surfaces are :math:`G^0`-continuous along their shared edge """ self_edge = self.get_edge(surface_edge, n_points=n_points) other_edge = other.get_edge(other_surface_edge, n_points=n_points) assert np.array_equal(self_edge, other_edge)
[docs] def verify_g1(self, other: "BezierSurface", surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge, n_points: int = 10): r""" Verifies that two Bézier surfaces are :math:`G^1`-continuous along their shared edge """ # Get the first derivatives at the boundary and perpendicular to the boundary for each surface, # evaluated at "n_points" locations along the boundary self_perp_edge_derivs = self.get_first_derivs_along_edge(surface_edge, n_points=n_points, perp=True) other_perp_edge_derivs = other.get_first_derivs_along_edge(other_surface_edge, n_points=n_points, perp=True) # Initialize an array of ratios of magnitude of the derivative values at each point for both sides # of the boundary magnitude_ratios = [] # Loop over each pair of cross-derivatives evaluated along the boundary for point_idx, (self_perp_edge_deriv, other_perp_edge_deriv) in enumerate(zip( self_perp_edge_derivs, other_perp_edge_derivs)): # Ensure that each derivative vector has the same direction along the boundary for each surface try: assert np.allclose( np.nan_to_num(self_perp_edge_deriv / np.linalg.norm(self_perp_edge_deriv)), np.nan_to_num(other_perp_edge_deriv / np.linalg.norm(other_perp_edge_deriv)) ) except AssertionError: assert np.allclose( np.nan_to_num(self_perp_edge_deriv / np.linalg.norm(self_perp_edge_deriv)), np.nan_to_num(-other_perp_edge_deriv / np.linalg.norm(other_perp_edge_deriv)) ) # Compute the ratio of the magnitudes for each derivative vector along the boundary for each surface. # These will be compared at the end. with np.errstate(divide="ignore"): magnitude_ratios.append(self_perp_edge_deriv / other_perp_edge_deriv) # Assert that the first derivatives along each boundary are proportional current_f = None for magnitude_ratio in magnitude_ratios: for dxdydz_ratio in magnitude_ratio: if np.isinf(dxdydz_ratio) or dxdydz_ratio == 0.0: continue if current_f is None: current_f = dxdydz_ratio continue assert np.isclose(dxdydz_ratio, current_f)
[docs] def verify_g2(self, other: "BezierSurface", surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge, n_points: int = 10): """ Verifies that two Bézier surfaces are :math:`G^2`-continuous along their shared edge """ # Get the first derivatives at the boundary and perpendicular to the boundary for each surface, # evaluated at "n_points" locations along the boundary self_perp_edge_derivs = self.get_second_derivs_along_edge(surface_edge, n_points=n_points, perp=True) other_perp_edge_derivs = other.get_second_derivs_along_edge(other_surface_edge, n_points=n_points, perp=True) # Initialize an array of ratios of magnitude of the derivative values at each point for both sides # of the boundary magnitude_ratios = [] # Loop over each pair of cross-derivatives evaluated along the boundary for point_idx, (self_perp_edge_deriv, other_perp_edge_deriv) in enumerate(zip( self_perp_edge_derivs, other_perp_edge_derivs)): # Ensure that each derivative vector has the same direction along the boundary for each surface assert np.allclose( np.nan_to_num(self_perp_edge_deriv / np.linalg.norm(self_perp_edge_deriv)), np.nan_to_num(other_perp_edge_deriv / np.linalg.norm(other_perp_edge_deriv)) ) # Compute the ratio of the magnitudes for each derivative vector along the boundary for each surface. # These will be compared at the end. with np.errstate(divide="ignore"): magnitude_ratios.append(self_perp_edge_deriv / other_perp_edge_deriv) # Assert that the second derivatives along each boundary are proportional current_f = None for magnitude_ratio in magnitude_ratios: for dxdydz_ratio in magnitude_ratio: if np.isinf(dxdydz_ratio) or dxdydz_ratio == 0.0: continue if current_f is None: current_f = dxdydz_ratio continue assert np.isclose(dxdydz_ratio, current_f)
[docs] def evaluate(self, u: float, v: float): r""" Evaluates the surface at a given :math:`(u,v)` parameter pair. Parameters ---------- u: float Position along :math:`u` in parametric space. Normally in the range :math:`[0,1]` v: float Position along :math:`v` in parametric space. Normally in the range :math:`[0,1]` Returns ------- numpy.ndarray 1-D array of the form ``array([x, y, z])`` representing the evaluated point on the surface """ P = self.get_control_point_array() return np.array(bezier_surf_eval(P, u, v))
[docs] def evaluate_point3d(self, u: float, v: float) -> Point3D: r""" Evaluates the Bézier surface at a single :math:`(u,v)` parameter pair and returns a point object. Parameters ---------- u: float Position along :math:`u` in parametric space. Normally in the range :math:`[0,1]` v: float Position along :math:`v` in parametric space. Normally in the range :math:`[0,1]` Returns ------- Point3D Point object corresponding to the :math:`(u,v)` pair """ return Point3D.from_array(self.evaluate(u, v))
[docs] def evaluate_grid(self, Nu: int, Nv: int) -> np.ndarray: r""" Evaluates the Bézier surface on a uniform :math:`N_u \times N_v` grid of parameter values. Parameters ---------- Nu: int Number of uniformly spaced parameter values in the :math:`u`-direction Nv: int Number of uniformly spaced parameter values in the :math:`v`-direction Returns ------- numpy.ndarray Array of size :math:`N_u \times N_v \times 3` """ P = self.get_control_point_array() return np.array(bezier_surf_eval_grid(P, Nu, Nv))
[docs] def evaluate_uvvecs(self, u: np.ndarray, v: np.ndarray) -> np.ndarray: r""" Evaluates the Bézier surface at arbitrary vectors of :math:`u` and :math:`v`-values. Parameters ---------- u: np.ndarray 1-D array of :math:`u`-parameter values v: np.ndarray 1-D array of :math:`v`-parameter values Returns ------- np.ndarray Array of size :math:`\text{len}(u) \times \text{len}(v) \times 3` """ P = self.get_control_point_array() return np.array(bezier_surf_eval_uvvecs(P, u, v))
[docs] def extract_edge_curve(self, surface_edge: SurfaceEdge) -> BezierCurve3D: """ Extracts the control points from one of the four edges of the Bézier surface and outputs a Bézier curve with these control points Parameters ---------- surface_edge: SurfaceEdge Edge along which to extract the curve Returns ------- BezierCurve3D Bézier curve with control points corresponding to the control points along the edge of the surface """ P = self.get_control_point_array() if surface_edge == SurfaceEdge.u0: return BezierCurve3D(P[0, :, :]) if surface_edge == SurfaceEdge.u1: return BezierCurve3D(P[-1, :, :]) if surface_edge == SurfaceEdge.v0: return BezierCurve3D(P[:, 0, :]) if surface_edge == SurfaceEdge.v1: return BezierCurve3D(P[:, -1, :]) raise ValueError(f"Invalid surface edge {surface_edge}")
[docs] def elevate_degree_u(self) -> "BezierSurface": """ Elevates the degree of the Bézier surface in the :math:`u`-parametric direction. .. figure:: ../images/bezier_surface_2x3_u_elevation.* :width: 600 :align: center :math:`u` degree (:math:`n`) elevation Returns ------- BezierSurface A new Bézier surface with identical shape to the current one but with one additional row of control points in the :math:`u`-parametric direction """ n = self.degree_u m = self.degree_v P = self.get_control_point_array() # New array has one additional control point (current array only has n+1 control points) new_control_points = np.zeros((P.shape[0] + 1, P.shape[1], P.shape[2])) # Set starting and ending control points to what they already were new_control_points[0, :, :] = P[0, :, :] new_control_points[-1, :, :] = P[-1, :, :] # Update all the other control points for i in range(1, n + 1): # 1 <= i <= n for j in range(0, m + 1): # for all j new_control_points[i, j, :] = i / (n + 1) * P[i - 1, j, :] + (1 - i / (n + 1)) * P[i, j, :] return BezierSurface(new_control_points)
[docs] def elevate_degree_v(self) -> "BezierSurface": r""" Elevates the degree of the Bézier surface in the :math:`v`-parametric direction. .. figure:: ../images/bezier_surface_2x3_v_elevation.* :width: 600 :align: center :math:`v` degree (:math:`m`) elevation Returns ------- BezierSurface A new Bézier surface with identical shape to the current one but with one additional row of control points in the :math:`v`-parametric direction """ n = self.degree_u m = self.degree_v P = self.get_control_point_array() # New array has one additional control point (current array only has n+1 control points) new_control_points = np.zeros((P.shape[0], P.shape[1] + 1, P.shape[2])) # Set starting and ending control points to what they already were new_control_points[:, 0, :] = P[:, 0, :] new_control_points[:, -1, :] = P[:, -1, :] # Update all the other control points for i in range(0, n + 1): # for all i for j in range(1, m + 1): # 1 <= j <= m new_control_points[i, j, :] = j / (m + 1) * P[i, j - 1, :] + (1 - j / (m + 1)) * P[i, j, :] return BezierSurface(new_control_points)
[docs] def extract_isoparametric_curve_u(self, u: float, Nv: int) -> np.ndarray: r""" Extracts a curve along the :math:`v`-direction at a fixed value of :math:`u` Parameters ---------- u: float Constant value of :math:`u` Nv: int Number of points to evaluate, linearly spaced in :math:`v` Returns ------- numpy.ndarray Array of size :math:`N_v \times 3` representing the :math:`x`-, :math:`y`-, and :math:`z`-coordinates of the points evaluated along the isoparametric curve """ v_vec = np.linspace(0.0, 1.0, Nv) return np.array([self.evaluate(u, v) for v in v_vec])
[docs] def extract_isoparametric_curve_v(self, Nu: int, v: float) -> np.ndarray: r""" Extracts a curve along the :math:`u`-direction at a fixed value of :math:`v` Parameters ---------- Nu: int Number of points to evaluate, linearly spaced in :math:`u` v: float Constant value of :math:`v` Returns ------- numpy.ndarray Array of size :math:`N_u \times 3` representing the :math:`x`-, :math:`y`-, and :math:`z`-coordinates of the points evaluated along the isoparametric curve """ P = self.get_control_point_array() return np.array(bezier_surf_eval_iso_v(P, Nu, v))
[docs] def get_parallel_degree(self, surface_edge: SurfaceEdge) -> int: r""" Gets the degree of the curve corresponding to the input surface edge. Parameters ---------- surface_edge: SurfaceEdge Edge along which the parallel degree is evaluated Returns ------- int Degree parallel to the edge """ if surface_edge in [SurfaceEdge.v1, SurfaceEdge.v0]: return self.degree_u return self.degree_v
[docs] def get_perpendicular_degree(self, surface_edge: SurfaceEdge) -> int: r""" Gets the degree of the curve in the parametric direction perpendicular to the input surface edge. Parameters ---------- surface_edge: SurfaceEdge Edge along which the perpendicular degree is evaluated Returns ------- int Degree perpendicular to the edge """ if surface_edge in [SurfaceEdge.v1, SurfaceEdge.v0]: return self.degree_v return self.degree_u
[docs] def get_parallel_n_points(self, surface_edge: SurfaceEdge) -> int: r""" Gets the number of control points in the parametric direction parallel to the input surface edge. Parameters ---------- surface_edge: SurfaceEdge Edge along which the parallel number of control points is evaluated Returns ------- int Number of control points parallel to the edge """ if surface_edge in (SurfaceEdge.v1, SurfaceEdge.v0): return self.n_points_u return self.n_points_v
[docs] def get_perpendicular_n_points(self, surface_edge: SurfaceEdge) -> int: r""" Gets the number of control points in the parametric direction perpendicular to the input surface edge. Parameters ---------- surface_edge: SurfaceEdge Edge along which the perpendicular number of control points is evaluated Returns ------- int Number of control points perpendicular to the edge """ if surface_edge in (SurfaceEdge.v1, SurfaceEdge.v0): return self.n_points_v return self.n_points_u
[docs] def get_point(self, row_index: int, continuity_index: int, surface_edge: SurfaceEdge) -> Point3D: r""" Gets the point corresponding to a particular index along the edge curve with perpendicular index corresponding to the level of continuity being applied. For example, for a :math:`6 \times 5` Bézier surface, the following code .. code-block:: python p = surf.get_point(2, 1, ac.SurfaceEdge.v0) returns the point :math:`\mathbf{P}_{2,1}` and .. code-block:: python p = surf.get_point(2, 1, ac.SurfaceEdge.u1) returns the point :math:`\mathbf{P}_{6-1,2} = \mathbf{P}_{5,2}`. .. seealso:: :obj:`~aerocaps.geom.surfaces.BezierSurface.set_point` Setter equivalent of this method Parameters ---------- row_index: int Index along the surface edge control points continuity_index: int Index in the parametric direction perpendicular to the surface edge. Normally either ``0``, ``1``, or ``2`` surface_edge: SurfaceEdge Edge of the surface along which to retrieve the control point Returns ------- Point3D Point used to enforce :math:`G^x` continuity, where :math:`x` is the value of ``continuity_index`` """ if surface_edge == SurfaceEdge.v1: return self.points[row_index][-(continuity_index + 1)] elif surface_edge == SurfaceEdge.v0: return self.points[row_index][continuity_index] elif surface_edge == SurfaceEdge.u1: return self.points[-(continuity_index + 1)][row_index] elif surface_edge == SurfaceEdge.u0: return self.points[continuity_index][row_index] else: raise ValueError("Invalid surface_edge value")
[docs] def get_point_ij(self, row_index: int, continuity_index: int, surface_edge: SurfaceEdge) -> (int, int): r""" Gets the point indices corresponding to a particular index along the edge curve with perpendicular index corresponding to the level of continuity being applied. Parameters ---------- row_index: int Index along the surface edge control points continuity_index: int Index in the parametric direction perpendicular to the surface edge. Normally either ``0``, ``1``, or ``2`` surface_edge: SurfaceEdge Edge of the surface along which to retrieve the control point Returns ------- int, int Point indices used to enforce :math:`G^x` continuity, where :math:`x` is the value of ``continuity_index`` """ if surface_edge == SurfaceEdge.v1: return row_index, len(self.points[0]) - (continuity_index + 1) elif surface_edge == SurfaceEdge.v0: return row_index, continuity_index elif surface_edge == SurfaceEdge.u1: return len(self.points) - (continuity_index + 1), row_index elif surface_edge == SurfaceEdge.u0: return continuity_index, row_index else: raise ValueError("Invalid surface_edge value")
[docs] def set_point(self, point: Point3D, row_index: int, continuity_index: int, surface_edge: SurfaceEdge): r""" Sets the point corresponding to a particular index along the edge curve with perpendicular index corresponding to the level of continuity being applied. For example, for a :math:`6 \times 5` Bézier surface, the following code .. code-block:: python p = ac.Point3D.from_array(np.array([3.0, 4.0, 5.0])) surf.set_point(p, 2, 1, ac.SurfaceEdge.v0) sets the value of point :math:`\mathbf{P}_{2,1}` to :math:`[3,4,5]^T` and .. code-block:: python p = ac.Point3D.from_array(np.array([3.0, 4.0, 5.0])) surf.set_point(p, 2, 1, ac.SurfaceEdge.u1) sets the value of point :math:`\mathbf{P}_{6-1,2} = \mathbf{P}_{5,2}` to :math:`[3,4,5]^T`. .. seealso:: :obj:`~aerocaps.geom.surfaces.BezierSurface.get_point` Getter equivalent of this method Parameters ---------- point: Point3D Point object to apply at the specified indices row_index: int Index along the surface edge control points continuity_index: int Index in the parametric direction perpendicular to the surface edge. Normally either ``0``, ``1``, or ``2`` surface_edge: SurfaceEdge Edge of the surface along which to retrieve the control point """ if surface_edge == SurfaceEdge.v1: self.points[row_index][-(continuity_index + 1)].x.m = point.x.m self.points[row_index][-(continuity_index + 1)].y.m = point.y.m self.points[row_index][-(continuity_index + 1)].z.m = point.z.m elif surface_edge == SurfaceEdge.v0: self.points[row_index][continuity_index].x.m = point.x.m self.points[row_index][continuity_index].y.m = point.y.m self.points[row_index][continuity_index].z.m = point.z.m elif surface_edge == SurfaceEdge.u1: self.points[-(continuity_index + 1)][row_index].x.m = point.x.m self.points[-(continuity_index + 1)][row_index].y.m = point.y.m self.points[-(continuity_index + 1)][row_index].z.m = point.z.m elif surface_edge == SurfaceEdge.u0: self.points[continuity_index][row_index].x.m = point.x.m self.points[continuity_index][row_index].y.m = point.y.m self.points[continuity_index][row_index].z.m = point.z.m else: raise ValueError("Invalid surface_edge value")
@staticmethod def _evaluate_f_sign(surf_edge_1: SurfaceEdge, surf_edge_2: SurfaceEdge) -> float: """ Evaluates the sign of the tangent proportionality factor across an edge pair Parameters ---------- surf_edge_1: SurfaceEdge First surface edge surf_edge_2: SurfaceEdge Second surface edge Returns ------- float ``-1.0`` if both surface edges end in 0 or both surface edges end in 1, ``1.0`` otherwise """ surf_edges_0 = (SurfaceEdge.u0, SurfaceEdge.v0) surf_edges_1 = (SurfaceEdge.u1, SurfaceEdge.v1) if surf_edge_1 in surf_edges_0 and surf_edge_2 in surf_edges_0: return -1.0 if surf_edge_1 in surf_edges_1 and surf_edge_2 in surf_edges_1: return -1.0 return 1.0
[docs] def enforce_g0(self, other: "BezierSurface", surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge): r""" Enforces :math:`G^0` continuity along the input ``surface_edge`` by equating the control points along this edge to the control points along the ``other_surface_edge`` of the Bézier surface given by ``other``. The control points of the surface from which this method is called are modified in-place, and the control points of ``other`` are left unchanged. .. important:: The parallel degree of the current surface along ``surface_edge`` must be equal to the parallel degree of the ``other`` surface along ``other_surface_edge``, otherwise an ``AssertionError`` will be raised. If these degrees are not equal, first elevate the degree of the surface with the lower parallel degree until the degrees match using either :obj:`~aerocaps.geom.surfaces.BezierSurface.elevate_degree_u` or :obj:`~aerocaps.geom.surfaces.BezierSurface.elevate_degree_v`, whichever is appropriate. .. seealso:: :obj:`~aerocaps.geom.surfaces.BezierSurface.enforce_c0` Parametric continuity equivalent (:math:`C^0`) Parameters ---------- other: BezierSurface Another Bézier surface along which an edge will be used for stitching surface_edge: SurfaceEdge The edge of the current surface to modify other_surface_edge: SurfaceEdge Tool edge of surface ``other`` which determines the positions of control points along ``surface_edge`` of the current surface """ self_parallel_degree = self.get_parallel_degree(surface_edge) other_parallel_degree = other.get_parallel_degree(other_surface_edge) if self_parallel_degree != other_parallel_degree: raise ValueError(f"Degree parallel to the edge of the input surface ({self_parallel_degree}) does " f"not match the degree parallel to the edge of the other surface " f"({other_parallel_degree})") for row_index in range(self.get_parallel_degree(surface_edge) + 1): self.set_point(other.get_point(row_index, 0, other_surface_edge), row_index, 0, surface_edge)
[docs] def enforce_c0(self, other: "BezierSurface", surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge): """ For zeroth-degree continuity, there is no difference between geometric (:math:`G^0`) and parametric (:math:`C^0`) continuity. Because this method is simply a convenience method that calls :obj:`~aerocaps.geom.surfaces.BezierSurface.enforce_g0`, see the documentation for that method for more detailed documentation. Parameters ---------- other: BezierSurface Another Bézier surface along which an edge will be used for stitching surface_edge: SurfaceEdge The edge of the current surface to modify other_surface_edge: SurfaceEdge Tool edge of surface ``other`` which determines the positions of control points along ``surface_edge`` of the current surface """ self.enforce_g0(other, surface_edge, other_surface_edge)
[docs] def enforce_g0g1(self, other: "BezierSurface", f: float, surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge): r""" First enforces :math:`G^0` continuity, then tangent (:math:`G^1`) continuity is enforced according to the following equation: .. math:: \mathcal{P}^{b,\mathcal{E}_b}_{k,1} = \mathcal{P}^{b,\mathcal{E}_b}_{k,0} + f \frac{p_{\perp}^{a,\mathcal{E}_a}}{p_{\perp}^{b,\mathcal{E}_b}} \left[\mathcal{P}^{a,\mathcal{E}_a}_{k,0} - \mathcal{P}^{a,\mathcal{E}_a}_{k,1} \right] \text{ for }k=0,1,\ldots,p_{\parallel}^{b,\mathcal{E}_b} Here, :math:`b` corresponds to the current surface, and :math:`a` corresponds to the ``other`` surface. The control points of the surface from which this method is called are modified in-place, and the control points of ``other`` are left unchanged. .. seealso:: :obj:`~aerocaps.geom.surfaces.BezierSurface.enforce_g0` Geometric point continuity enforcement (:math:`G^0`) :obj:`~aerocaps.geom.surfaces.BezierSurface.enforce_c0c1` Parametric continuity equivalent (:math:`C^1`) Parameters ---------- other: BezierSurface Another Bézier surface along which an edge will be used for stitching f: float Tangent proportionality factor surface_edge: SurfaceEdge The edge of the current surface to modify other_surface_edge: SurfaceEdge Tool edge of surface ``other`` which determines the positions of control points along ``surface_edge`` of the current surface """ self.enforce_g0(other, surface_edge, other_surface_edge) n_ratio = other.get_perpendicular_degree(other_surface_edge) / self.get_perpendicular_degree(surface_edge) for row_index in range(self.get_parallel_degree(surface_edge) + 1): P_i0_b = self.get_point(row_index, 0, surface_edge) P_im_a = other.get_point(row_index, 0, other_surface_edge) P_im1_a = other.get_point(row_index, 1, other_surface_edge) P_i1_b = P_i0_b + f * n_ratio * (P_im_a - P_im1_a) self.set_point(P_i1_b, row_index, 1, surface_edge)
[docs] def enforce_c0c1(self, other: "BezierSurface", surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge): r""" Equivalent to calling :obj:`~aerocaps.geom.surfaces.BezierSurface.enforce_g0g1` with ``f=1.0``. See that method for more detailed documentation. Parameters ---------- other: BezierSurface Another Bézier surface along which an edge will be used for stitching surface_edge: SurfaceEdge The edge of the current surface to modify other_surface_edge: SurfaceEdge Tool edge of surface ``other`` which determines the positions of control points along ``surface_edge`` of the current surface """ self.enforce_g0g1(other, 1.0, surface_edge, other_surface_edge)
[docs] def enforce_g0g1_multiface(self, adjacent_surf_u0: "BezierSurface" = None, adjacent_surf_u1: "BezierSurface" = None, adjacent_surf_v0: "BezierSurface" = None, adjacent_surf_v1: "BezierSurface" = None, other_edge_u0: SurfaceEdge = None, other_edge_u1: SurfaceEdge = None, other_edge_v0: SurfaceEdge = None, other_edge_v1: SurfaceEdge = None, f_u0_initial: float = 1.0, f_u1_initial: float = 1.0, f_v0_initial: float = 1.0, f_v1_initial: float = 1.0, n_deriv_points: int = 10, ) -> OptimizeResult: r""" .. warning:: This is an experimental feature and should not be used in production geometries Enforces :math:`G^0` and :math:`G^1` continuity across multiple adjacent boundaries of a surface, up to all four boundaries. This is done by first enforcing :math:`G^0` continuity at all required boundaries and then optimizing the locations of the second rows of control points to minimize :math:`G^1` error at ``n_deriv_points`` locations along each of the boundary curves. The following is the cost function that is minimized: .. math:: J(x_k) = \sum\limits_{i=0}^{n_p n_{\mathcal{E}}} \left( \left. \frac{\partial \mathbf{S}_i^a(u,v)}{\partial \mu} \right|_{u=u_i,v=v_i} - \frac{f_{\text{sgn},i}}{f_i(x_k)} \left. \frac{\partial \mathbf{S}^b(u,v,x_k)}{\partial \mu} \right|_{u=u_i,v=v_i} \right)^2 where * :math:`x_k` is the set of design variables to be optimized including the internal control point locations and tangent proportionality factors across each boundary * :math:`n_p` is the number of discrete first derivative calculations on each boundary (specified by ``n_deriv_points``) * :math:`n_\mathcal{E}` is the number of edges across which continuity is being enforced * :math:`\mathbf{S}_i^a(u,v)` is a surface specified by ``adjacent_surf_u0``, etc. * :math:`\mu` is equal to either :math:`u` or :math:`v`, determined by the parametric direction perpendicular to the edge * :math:`(u_i,v_i)` is the point along an edge where the derivative is being evaluated * :math:`f_{\text{sgn},i}` is the sign of the proportionality factor, :math:`-1` if both the target edge and other edge specified by :math:`i` end in :math:`0` or both end in :math:`1`, :math:`1` otherwise * :math:`f_i(x_k)` is the tangent proportionality factor * :math:`\mathbf{S}^b(u,v,x_k)` is the target surface (``self``) For maximum performance of the optimizer, the exact Jacobian is calculated: .. math:: \frac{\partial J(x_k)}{\partial x_k} = 2 \sum\limits_{i=0}^{n_p n_{\mathcal{E}}} \left( \left. \frac{\partial \mathbf{S}_i^a(u,v)}{\partial \mu} \right|_{u=u_i,v=v_i} - \frac{f_{\text{sgn},i}}{f_i(x_k)} \left. \frac{\partial \mathbf{S}^b(u,v,x_k)}{\partial \mu} \right|_{u=u_i,v=v_i} \right) \left[ -f_{\text{sgn},i} \frac{\partial}{\partial x_k} \left( \frac{1}{f_i} \right) \left( \left. \frac{\partial \mathbf{S}^b(u,v,x_k)}{\partial \mu} \right|_{u=u_i,v=v_i} \right) -\frac{f_{\text{sgn},i}}{f_i} \frac{\partial}{\partial x_k} \left( \left. \frac{\partial \mathbf{S}^b(u,v,x_k)}{\partial \mu} \right|_{u=u_i,v=v_i} \right) \right] .. note:: This method is reserved for the complex case where continuity is required across boundaries that share a surface corner. In the case of continuity with a single surface or a pair of surfaces with common boundaries on opposite sides of the surface (such as the :math:`v_0` and :math:`v_1` boundaries), the much simpler :obj:`~aerocaps.geom.surfaces.BezierSurface.enforce_g0g1` should be used. .. figure:: ../images/bezier_enforce_g0g1_multiface.* :align: center :width: 600 Multi-face :math:`G^0` and :math:`G^1` continuity enforcement Parameters ---------- adjacent_surf_u0: BezierSurface Surface sharing the :math:`u_0` boundary of ``target_surf``. Default: ``None`` adjacent_surf_u1: BezierSurface Surface sharing the :math:`u_1` boundary of ``target_surf``. Default: ``None`` adjacent_surf_v0: BezierSurface Surface sharing the :math:`v_0` boundary of ``target_surf``. Default: ``None`` adjacent_surf_v1: BezierSurface Surface sharing the :math:`v_1` boundary of ``target_surf``. Default: ``None`` other_edge_u0: SurfaceEdge Edge of surface ``adjacent_surf_u0`` that will be stitched. Default: ``None`` other_edge_u1: SurfaceEdge Edge of surface ``adjacent_surf_u0`` that will be stitched. Default: ``None`` other_edge_v0: SurfaceEdge Edge of surface ``adjacent_surf_u0`` that will be stitched. Default: ``None`` other_edge_v1: SurfaceEdge Edge of surface ``adjacent_surf_u0`` that will be stitched. Default: ``None`` f_u0_initial: float Initial value of the tangent proportionality factor across boundary :math:`u_0`. The final value selected by the optimizer will be different from this value. Default: ``1.0`` f_u1_initial: float Initial value of the tangent proportionality factor across boundary :math:`u_1`. The final value selected by the optimizer will be different from this value. Default: ``1.0`` f_v0_initial: float Initial value of the tangent proportionality factor across boundary :math:`v_0`. The final value selected by the optimizer will be different from this value. Default: ``1.0`` f_v1_initial: float Initial value of the tangent proportionality factor across boundary :math:`v_1`. The final value selected by the optimizer will be different from this value. Default: ``1.0`` n_deriv_points: int Number of discrete locations where the continuity error will be evaluated. Default: ``10`` Returns ------- OptimizeResult Result from the :math:`G^1` continuity error minimization problem solution """ adjacent_surfs = (adjacent_surf_u0, adjacent_surf_u1, adjacent_surf_v0, adjacent_surf_v1) other_edges = (other_edge_u0, other_edge_u1, other_edge_v0, other_edge_v1) # Input validation if not any(adjacent_surfs): raise ValueError("Must specify at least one adjacent surface") if not any(other_edges): raise ValueError("Must specify at least one other edge") if len(adjacent_surfs) == 1: raise ValueError("For continuity enforcement with only one other surface, use 'enforce_g0g1' instead") if len(adjacent_surfs) != len(other_edges): raise ValueError("Must specify one 'other_edge' for every 'adjacent_surf'") # Create a mapping between the surfaces and edges surf_edge_mapping = { SurfaceEdge.u0: (adjacent_surf_u0, other_edge_u0, f_u0_initial), SurfaceEdge.u1: (adjacent_surf_u1, other_edge_u1, f_u1_initial), SurfaceEdge.v0: (adjacent_surf_v0, other_edge_v0, f_v0_initial), SurfaceEdge.v1: (adjacent_surf_v1, other_edge_v1, f_v1_initial) } for self_edge, other_data in surf_edge_mapping.items(): if any(other_data) or all(other_data): continue raise ValueError("Must specify either both an 'adjacent_surf' and an 'other_edge' or neither for every " "edge of the current surface") # Enforce G0 continuity with all surfaces for self_edge in surf_edge_mapping.keys(): data = surf_edge_mapping[self_edge] if data[0] is None: continue self.enforce_g0( data[0], surface_edge=self_edge, other_surface_edge=data[1] ) d1_other = { self_edge: data[0].get_first_derivs_along_edge(data[1], n_points=n_deriv_points) if data[0] else None for self_edge, data in surf_edge_mapping.items() } def get_point_ijs_to_update() -> typing.List[typing.Tuple[int]]: """Gets the indices of the points in the target surface that will be updated during the optimization""" point_ijs_to_update = [] for surface_edge, _data in surf_edge_mapping.items(): # Loop through all the points in the second row starting from the second point and ending at the # second-to-last point for row_index in range(1, self.get_parallel_n_points(surface_edge) - 1): point_ij = self.get_point_ij(row_index, continuity_index=1, surface_edge=surface_edge) if point_ij in point_ijs_to_update: continue point_ijs_to_update.append(point_ij) return point_ijs_to_update f_signs = {self_edge: self._evaluate_f_sign(self_edge, data[1]) for self_edge, data in surf_edge_mapping.items() if data is not None} f_vals = {self_edge: data[2] for self_edge, data in surf_edge_mapping.items() if data is not None} mod_ijs = get_point_ijs_to_update() mod_points = [self.points[i][j] for i, j in mod_ijs] x0 = np.array([p.as_array() for p in mod_points]).flatten() x0 = np.append(x0, np.array(list(f_vals.values()))) def obj_fun_and_jac(x: np.ndarray) -> (float, np.ndarray): """ Computes the objective function as the sum of the squares of the :math:`G^1` continuity error, along with the Jacobian Parameters ---------- x: np.ndarray 1-D array of design variable values Returns ------- float, np.ndarray The objective function value and the Jacobian (a 1-D array of sensitivities) """ x_reshaped = x[:3 * len(mod_ijs)].reshape((len(mod_points), 3)) jac_arr = np.zeros(x.shape) # Update the points in-place for i in range(x_reshaped.shape[0]): mod_points[i].x.m = x_reshaped[i, 0] mod_points[i].y.m = x_reshaped[i, 1] mod_points[i].z.m = x_reshaped[i, 2] # Evaluate the objective function and Jacobian obj_fun_val = 0.0 for edge_idx, (target_edge, multiface_data) in enumerate(surf_edge_mapping.items()): if surf_edge_mapping[target_edge][0] is None: continue f = x[3 * len(mod_ijs) + edge_idx] f_sign = f_signs[target_edge] A = -f_sign * 1 / abs(f) dA = f_sign * 1 / f ** 2 d1_self = self.get_first_derivs_along_edge(target_edge, n_points=n_deriv_points) # Objective function value update obj_fun_val += np.sum((d1_other[target_edge] + A * d1_self) ** 2) # Jacobian update loop start_xyz = 0 for mod_ij in mod_ijs: d1_sens_self = self.get_first_deriv_cp_sens_along_edge( target_edge, mod_ij[0], mod_ij[1], n_points=n_deriv_points ) # Jacobian array update for k in range(3): jac_arr[start_xyz + k] += np.sum( 2 * (d1_other[target_edge][:, k] + A * d1_self[:, k]) * A * d1_sens_self[:, k] ) start_xyz += 3 jac_arr[3 * len(mod_ijs) + edge_idx] = np.sum( 2 * (d1_other[target_edge] + A * d1_self) * dA * d1_self ) return obj_fun_val, jac_arr res = minimize(obj_fun_and_jac, x0, jac=True) return res
[docs] def enforce_g0g1g2(self, other: "BezierSurface", f: float, surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge): r""" First enforces :math:`G^0` and :math:`G^1` continuity, then curvature (:math:`G^2`) continuity is enforced according to the following equation: .. math:: \mathcal{P}^{b,\mathcal{E}_b}_{k,2} = 2 \mathcal{P}^{b,\mathcal{E}_b}_{k,1} - \mathcal{P}^{b,\mathcal{E}_b}_{k,0} + f^2 \frac{p_{\perp}^{a,\mathcal{E}_a}(p_{\perp}^{a,\mathcal{E}_a}-1)}{p_{\perp}^{b,\mathcal{E}_b}(p_{\perp}^{b,\mathcal{E}_b}-1)} \left[ \mathcal{P}^{a,\mathcal{E}_a}_{k,0} - 2 \mathcal{P}^{a,\mathcal{E}_a}_{k,1} + \mathcal{P}^{a,\mathcal{E}_a}_{k,2} \right] \text{ for }k=0,1,\ldots,p_{\parallel}^{b,\mathcal{E}_b} Here, :math:`b` corresponds to the current surface, and :math:`a` corresponds to the ``other`` surface. The control points of the surface from which this method is called are modified in-place, and the control points of ``other`` are left unchanged. .. seealso:: :obj:`~aerocaps.geom.surfaces.BezierSurface.enforce_g0` Geometric point continuity enforcement (:math:`G^0`) :obj:`~aerocaps.geom.surfaces.BezierSurface.enforce_g0g1` Geometric tangent continuity enforcement (:math:`G^1`) :obj:`~aerocaps.geom.surfaces.BezierSurface.enforce_c0c1c2` Parametric continuity equivalent (:math:`C^2`) Parameters ---------- other: BezierSurface Another Bézier surface along which an edge will be used for stitching f: float Tangent proportionality factor surface_edge: SurfaceEdge The edge of the current surface to modify other_surface_edge: SurfaceEdge Tool edge of surface ``other`` which determines the positions of control points along ``surface_edge`` of the current surface """ self.enforce_g0g1(other, f, surface_edge, other_surface_edge) p_perp_a = other.get_perpendicular_degree(other_surface_edge) p_perp_b = self.get_perpendicular_degree(surface_edge) n_ratio = (p_perp_a ** 2 - p_perp_a) / (p_perp_b ** 2 - p_perp_b) for row_index in range(self.get_parallel_degree(surface_edge) + 1): P_i0_b = self.get_point(row_index, 0, surface_edge) P_i1_b = self.get_point(row_index, 1, surface_edge) P_im_a = other.get_point(row_index, 0, other_surface_edge) P_im1_a = other.get_point(row_index, 1, other_surface_edge) P_im2_a = other.get_point(row_index, 2, other_surface_edge) P_i2_b = (2.0 * P_i1_b - P_i0_b) + f ** 2 * n_ratio * (P_im_a - 2.0 * P_im1_a + P_im2_a) self.set_point(P_i2_b, row_index, 2, surface_edge)
[docs] def enforce_c0c1c2(self, other: "BezierSurface", surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge): r""" Equivalent to calling :obj:`~aerocaps.geom.surfaces.BezierSurface.enforce_g0g1g2` with ``f=1.0``. See that method for more detailed documentation. Parameters ---------- other: BezierSurface Another Bézier surface along which an edge will be used for stitching surface_edge: SurfaceEdge The edge of the current surface to modify other_surface_edge: SurfaceEdge Tool edge of surface ``other`` which determines the positions of control points along ``surface_edge`` of the current surface """ self.enforce_g0g1g2(other, 1.0, surface_edge, other_surface_edge)
[docs] def enforce_g0g1g2_multiface(self, adjacent_surf_u0: "BezierSurface" = None, adjacent_surf_u1: "BezierSurface" = None, adjacent_surf_v0: "BezierSurface" = None, adjacent_surf_v1: "BezierSurface" = None, other_edge_u0: SurfaceEdge = None, other_edge_u1: SurfaceEdge = None, other_edge_v0: SurfaceEdge = None, other_edge_v1: SurfaceEdge = None, f_u0_initial: float = 1.0, f_u1_initial: float = 1.0, f_v0_initial: float = 1.0, f_v1_initial: float = 1.0, n_deriv_points: int = 10, ) -> OptimizeResult: r""" .. warning:: This is an experimental feature and should not be used in production geometries Enforces :math:`G^0`, :math:`G^1`, and :math:`G^2` continuity across multiple adjacent boundaries of a surface, up to all four boundaries. This is done by first enforcing :math:`G^0` continuity at all required boundaries and then optimizing the locations of the second and third rows of control points to minimize :math:`G^1` and :math:`G^2` error at ``n_deriv_points`` locations along each of the boundary curves. The following is the cost function that is minimized: .. math:: J(x_k) = \sum\limits_{l=1}^2 \sum\limits_{i=0}^{n_p n_{\mathcal{E}}} \left( \left. \frac{\partial^l \mathbf{S}_i^a(u,v)}{\partial \mu^l} \right|_{u=u_i,v=v_i} - \frac{f_{\text{sgn},i}}{f^l_i(x_k)} \left. \frac{\partial^l \mathbf{S}^b(u,v,x_k)}{\partial \mu^l} \right|_{u=u_i,v=v_i} \right)^2 where * :math:`x_k` is the set of design variables to be optimized including the internal control point locations and tangent proportionality factors across each boundary * :math:`n_p` is the number of discrete derivative calculations on each boundary (specified by ``n_deriv_points``) * :math:`n_\mathcal{E}` is the number of edges across which continuity is being enforced * :math:`\mathbf{S}_i^a(u,v)` is a surface specified by ``adjacent_surf_u0``, etc. * :math:`\mu` is equal to either :math:`u` or :math:`v`, determined by the parametric direction perpendicular to the edge * :math:`(u_i,v_i)` is the point along an edge where the derivative is being evaluated * :math:`f_{\text{sgn},i}` is the sign of the proportionality factor, :math:`-1` if both the target edge and other edge specified by :math:`i` end in :math:`0` or both end in :math:`1`, :math:`1` otherwise * :math:`f_i(x_k)` is the tangent proportionality factor * :math:`\mathbf{S}^b(u,v,x_k)` is the target surface (``self``) * :math:`l` is the derivative order For maximum performance of the optimizer, the exact Jacobian is calculated: .. math:: \frac{\partial J(x_k)}{\partial x_k} = 2 \sum\limits_{l=1}^2 \sum\limits_{i=0}^{n_p n_{\mathcal{E}}} \left( \left. \frac{\partial^l \mathbf{S}_i^a(u,v)}{\partial \mu^l} \right|_{u=u_i,v=v_i} - \frac{f_{\text{sgn},i}}{f^l_i(x_k)} \left. \frac{\partial^l \mathbf{S}^b(u,v,x_k)}{\partial \mu^l} \right|_{u=u_i,v=v_i} \right) \left[ -f_{\text{sgn},i} \frac{\partial}{\partial x_k} \left( \frac{1}{f^l_i} \right) \left( \left. \frac{\partial^l \mathbf{S}^b(u,v,x_k)}{\partial \mu^l} \right|_{u=u_i,v=v_i} \right) -\frac{f_{\text{sgn},i}}{f_i^l} \frac{\partial}{\partial x_k} \left( \left. \frac{\partial^l \mathbf{S}^b(u,v,x_k)}{\partial \mu^l} \right|_{u=u_i,v=v_i} \right) \right] .. note:: This method is reserved for the complex case where continuity is required across boundaries that share a surface corner. In the case of continuity with a single surface or a pair of surfaces with common boundaries on opposite sides of the surface (such as the :math:`v_0` and :math:`v_1` boundaries), the much simpler :obj:`~aerocaps.geom.surfaces.BezierSurface.enforce_g0g1g2` should be used. Parameters ---------- adjacent_surf_u0: BezierSurface Surface sharing the :math:`u_0` boundary of ``target_surf``. Default: ``None`` adjacent_surf_u1: BezierSurface Surface sharing the :math:`u_1` boundary of ``target_surf``. Default: ``None`` adjacent_surf_v0: BezierSurface Surface sharing the :math:`v_0` boundary of ``target_surf``. Default: ``None`` adjacent_surf_v1: BezierSurface Surface sharing the :math:`v_1` boundary of ``target_surf``. Default: ``None`` other_edge_u0: SurfaceEdge Edge of surface ``adjacent_surf_u0`` that will be stitched. Default: ``None`` other_edge_u1: SurfaceEdge Edge of surface ``adjacent_surf_u0`` that will be stitched. Default: ``None`` other_edge_v0: SurfaceEdge Edge of surface ``adjacent_surf_u0`` that will be stitched. Default: ``None`` other_edge_v1: SurfaceEdge Edge of surface ``adjacent_surf_u0`` that will be stitched. Default: ``None`` f_u0_initial: float Initial value of the tangent proportionality factor across boundary :math:`u_0`. The final value selected by the optimizer will be different from this value. Default: ``1.0`` f_u1_initial: float Initial value of the tangent proportionality factor across boundary :math:`u_1`. The final value selected by the optimizer will be different from this value. Default: ``1.0`` f_v0_initial: float Initial value of the tangent proportionality factor across boundary :math:`v_0`. The final value selected by the optimizer will be different from this value. Default: ``1.0`` f_v1_initial: float Initial value of the tangent proportionality factor across boundary :math:`v_1`. The final value selected by the optimizer will be different from this value. Default: ``1.0`` n_deriv_points: int Number of discrete locations where the continuity error will be evaluated. Default: ``10`` Returns ------- OptimizeResult Result from the :math:`G^1`- and :math:`G^2`-continuity error minimization problem solution """ adjacent_surfs = (adjacent_surf_u0, adjacent_surf_u1, adjacent_surf_v0, adjacent_surf_v1) other_edges = (other_edge_u0, other_edge_u1, other_edge_v0, other_edge_v1) # Input validation if not any(adjacent_surfs): raise ValueError("Must specify at least one adjacent surface") if not any(other_edges): raise ValueError("Must specify at least one other edge") if len(adjacent_surfs) == 1: raise ValueError("For continuity enforcement with only one other surface, use 'enforce_g0g1' instead") if len(adjacent_surfs) != len(other_edges): raise ValueError("Must specify one 'other_edge' for every 'adjacent_surf'") # Create a mapping between the surfaces and edges surf_edge_mapping = { SurfaceEdge.u0: (adjacent_surf_u0, other_edge_u0, f_u0_initial), SurfaceEdge.u1: (adjacent_surf_u1, other_edge_u1, f_u1_initial), SurfaceEdge.v0: (adjacent_surf_v0, other_edge_v0, f_v0_initial), SurfaceEdge.v1: (adjacent_surf_v1, other_edge_v1, f_v1_initial) } for self_edge, other_data in surf_edge_mapping.items(): if any(other_data) or all(other_data): continue raise ValueError("Must specify either both an 'adjacent_surf' and an 'other_edge' or neither for every " "edge of the current surface") # Enforce G0 continuity with all surfaces for self_edge in surf_edge_mapping.keys(): data = surf_edge_mapping[self_edge] if data[0] is None: continue self.enforce_g0( data[0], surface_edge=self_edge, other_surface_edge=data[1] ) d1_other = { self_edge: data[0].get_first_derivs_along_edge(data[1], n_points=n_deriv_points) if data[0] else None for self_edge, data in surf_edge_mapping.items() } d2_other = { self_edge: data[0].get_second_derivs_along_edge(data[1], n_points=n_deriv_points) if data[0] else None for self_edge, data in surf_edge_mapping.items() } def get_point_ijs_to_update() -> typing.List[typing.Tuple[int]]: """Gets the indices of the points in the target surface that will be updated during the optimization""" point_ijs_to_update = [] for surface_edge, _data in surf_edge_mapping.items(): # Loop through all the points in the second row starting from the second point and ending at the # second-to-last point for row_index in range(1, self.get_parallel_n_points(surface_edge) - 1): point_ij = self.get_point_ij(row_index, continuity_index=1, surface_edge=surface_edge) if point_ij in point_ijs_to_update: continue point_ijs_to_update.append(point_ij) for surface_edge, _data in surf_edge_mapping.items(): for row_index in range(1, self.get_parallel_n_points(surface_edge) - 1): point_ij_2 = self.get_point_ij(row_index, continuity_index=2, surface_edge=surface_edge) if point_ij_2 in point_ijs_to_update: continue point_ijs_to_update.append(point_ij_2) return point_ijs_to_update f_signs = {self_edge: self._evaluate_f_sign(self_edge, data[1]) for self_edge, data in surf_edge_mapping.items() if data is not None} f_vals = {self_edge: data[2] for self_edge, data in surf_edge_mapping.items() if data is not None} mod_ijs = get_point_ijs_to_update() mod_points = [self.points[i][j] for i, j in mod_ijs] x0 = np.array([p.as_array() for p in mod_points]).flatten() x0 = np.append(x0, np.array(list(f_vals.values()))) def obj_fun_and_jac(x: np.ndarray) -> (float, np.ndarray): """ Computes the objective function as the sum of the squares of the :math:`G^1` continuity error, along with the Jacobian Parameters ---------- x: np.ndarray 1-D array of design variable values Returns ------- float, np.ndarray The objective function value and the Jacobian (a 1-D array of sensitivities) """ x_reshaped = x[:3 * len(mod_ijs)].reshape((len(mod_points), 3)) jac_arr = np.zeros(x.shape) # Update the points in-place for i in range(x_reshaped.shape[0]): mod_points[i].x.m = x_reshaped[i, 0] mod_points[i].y.m = x_reshaped[i, 1] mod_points[i].z.m = x_reshaped[i, 2] # Evaluate the objective function and Jacobian obj_fun_val = 0.0 for edge_idx, (target_edge, multiface_data) in enumerate(surf_edge_mapping.items()): if surf_edge_mapping[target_edge][0] is None: continue f = x[3 * len(mod_ijs) + edge_idx] f_sign = f_signs[target_edge] A = -f_sign * 1 / abs(f) A2 = -f_sign * (1 / f) ** 2 dA = f_sign * (1 / f) ** 2 dA2 = 2 * f_sign * (1 / abs(f)) ** 3 d1_self = self.get_first_derivs_along_edge(target_edge, n_points=n_deriv_points) d2_self = self.get_second_derivs_along_edge(target_edge, n_points=n_deriv_points) # Objective function value update obj_fun_val += np.sum((d1_other[target_edge] + A * d1_self) ** 2) obj_fun_val += np.sum((d2_other[target_edge] + A2 * d2_self) ** 2) # Jacobian update loop start_xyz = 0 # Jacobian array starting index for mod_ij_idx, mod_ij in enumerate(mod_ijs): d1_sens_self = self.get_first_deriv_cp_sens_along_edge( target_edge, mod_ij[0], mod_ij[1], n_points=n_deriv_points ) d2_sens_self = self.get_second_deriv_cp_sens_along_edge( target_edge, mod_ij[0], mod_ij[1], n_points=n_deriv_points ) # Jacobian array update for k in range(3): jac_arr[start_xyz + k] += 2 * np.sum( (d1_other[target_edge][:, k] + A * d1_self[:, k]) * A * d1_sens_self[:, k] + ( d2_other[target_edge][:, k] + A2 * d2_self[:, k] ) * A2 * d2_sens_self[:, k] ) # Increment the starting Jacobian index start_xyz += 3 jac_arr[3 * len(mod_ijs) + edge_idx] = np.sum( 2 * (d1_other[target_edge] + A * d1_self) * dA * d1_self + 2 * (d2_other[target_edge] + A2 * d2_self) * dA2 * d2_self ) return obj_fun_val, jac_arr # bounds = np.zeros((x0.shape[0], 2)) # bounds[-4:, 0] = 0.05 # bounds[-4:, 1] = 3.0 # xyz_start_iii = 0 # for iii in range(16): # bounds[xyz_start_iii, 0] = 0.0 # bounds[xyz_start_iii, 1] = 1.0 # bounds[xyz_start_iii + 1, 0] = -1.0 # bounds[xyz_start_iii + 1, 1] = 0.0 # bounds[xyz_start_iii + 2, 0] = -0.3 # bounds[xyz_start_iii + 2, 1] = 0.3 # xyz_start_iii += 3 # res = minimize(obj_fun_and_jac, x0, jac=True, bounds=bounds) res = minimize(obj_fun_and_jac, x0, jac=True) return res
[docs] def get_u_or_v_given_uvxyz(self, u: float = None, v: float = None, uv_guess: float = 0.5, x: Length = None, y: Length = None, z: Length = None) -> float: """ Computes one parametric value given the other and a specified :math:`x`-, :math:`y`-, or :math:`z`-location. As an example, given a :obj:`~aerocaps.geom.surfaces.BezierSurface` object assigned to the variable ``surf``, the :math:`u`-parameter corresponding to :math:`y=1.4` along the :math:`v=0.8` isoparametric curve can be computed using .. code-block:: python u = surf.get_u_or_v_given_uvxyz(v=0.8, y=1.4) Note that the inputs are keyword arguments to avoid having to specify ``None`` for each of the arguments not used. Parameters ---------- u: float or None Value of :math:`u` to solve for or specify. If left as ``None``, this parameter will be solved for. If ``None``, :math:`v` must be specified. Default: ``None`` v: float or None Value of :math:`v` to solve for or specify. If left as ``None``, this parameter will be solved for. If ``None``, :math:`u` must be specified. Default: ``None`` uv_guess: float Starting guess for the unsolved :math:`u` or :math:`v` parameter. Default: ``0.5`` x: Length or None :math:`x`-location corresponding to the :math:`u` or :math:`v` parameter to be solved. If this value is outside the surface geometry, the root-finder will fail and an error will be raised. If unspecified, either :math:`y` or :math:`z` must be specified. Default: ``None`` y: Length or None :math:`y`-location corresponding to the :math:`u` or :math:`v` parameter to be solved. If this value is outside the surface geometry, the root-finder will fail and an error will be raised. If unspecified, either :math:`x` or :math:`z` must be specified. Default: ``None`` z: Length or None :math:`z`-location corresponding to the :math:`u` or :math:`v` parameter to be solved. If this value is outside the surface geometry, the root-finder will fail and an error will be raised. If unspecified, either :math:`x` or :math:`y` must be specified. Default: ``None`` Returns ------- float The value of :math:`u` if :math:`v` is specified or :math:`v` if :math:`u` is specified """ # Validate inputs if u is None and v is None or (u is not None and v is not None): raise ValueError("Must specify exactly one of either u or v") xyz_spec = (x is not None, y is not None, z is not None) if len([xyz for xyz in xyz_spec if xyz]) != 1: raise ValueError("Must specify exactly one of x, y, or z") if x is not None: xyz, xyz_val = "x", x.m elif y is not None: xyz, xyz_val = "y", y.m elif z is not None: xyz, xyz_val = "z", z.m else: raise ValueError("Did not detect an x, y, or z input") def root_find_func_u(u_current): point = self.evaluate_point3d(u_current, v) return np.array([getattr(point, xyz).m - xyz_val]) def root_find_func_v(v_current): point = self.evaluate_point3d(u, v_current) return np.array([getattr(point, xyz).m - xyz_val]) if v is not None: return fsolve(root_find_func_u, x0=np.array([uv_guess]))[0] if u is not None: return fsolve(root_find_func_v, x0=np.array([uv_guess]))[0] raise ValueError("Did not detect a u or v input")
[docs] def split_at_u(self, u0: float) -> ("BezierSurface", "BezierSurface"): """ Splits the Bézier surface at :math:`u=u_0` along the :math:`v`-parametric direction. """ P = self.get_control_point_array() def de_casteljau(i: int, j: int, k: int) -> np.ndarray: """ Based on https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm. Recursive algorithm where the base case is just the value of the ith original control point. Parameters ---------- i: int Lower index j: int Upper index k: int Control point row index Returns ------- np.ndarray A one-dimensional array containing the :math:`x` and :math:`y` values of a control point evaluated at :math:`(i,j)` for a Bézier curve split at the parameter value ``t_split`` """ if j == 0: return P[i, k, :] return de_casteljau(i, j - 1, k) * (1 - u0) + de_casteljau(i + 1, j - 1, k) * u0 bez_surf_split_1_P = np.array([ [de_casteljau(i=0, j=i, k=k) for i in range(self.n_points_u)] for k in range(self.n_points_v) ]) bez_surf_split_2_P = np.array([ [de_casteljau(i=i, j=self.degree_u - i, k=k) for i in range(self.n_points_u)] for k in range(self.n_points_v) ]) return ( BezierSurface( np.transpose( bez_surf_split_1_P, (1, 0, 2) ) ), BezierSurface( np.transpose( bez_surf_split_2_P, (1, 0, 2) ) ) )
[docs] def split_at_v(self, v0: float) -> ("BezierSurface", "BezierSurface"): """ Splits the Bézier surface at :math:`v=v_0` along the :math:`u`-parametric direction. """ P = self.get_control_point_array() def de_casteljau(i: int, j: int, k: int) -> np.ndarray: """ Based on https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm. Recursive algorithm where the base case is just the value of the ith original control point. Parameters ---------- i: int Lower index j: int Upper index k: int Control point row index Returns ------- np.ndarray A one-dimensional array containing the :math:`x` and :math:`y` values of a control point evaluated at :math:`(i,j)` for a Bézier curve split at the parameter value ``t_split`` """ if j == 0: return P[k, i, :] return de_casteljau(i, j - 1, k) * (1 - v0) + de_casteljau(i + 1, j - 1, k) * v0 bez_surf_split_1_P = np.array([ [de_casteljau(i=0, j=i, k=k) for i in range(self.n_points_v)] for k in range(self.n_points_u) ]) bez_surf_split_2_P = np.array([ [de_casteljau(i=i, j=self.degree_v - i, k=k) for i in range(self.n_points_v)] for k in range(self.n_points_u) ]) return ( BezierSurface(bez_surf_split_1_P), BezierSurface(bez_surf_split_2_P) )
[docs] def transform(self, **transformation_kwargs) -> "BezierSurface": """ Creates a transformed copy of the surface by transforming each of the control points Parameters ---------- transformation_kwargs Keyword arguments passed to :obj:`~aerocaps.geom.transformation.Transformation3D` Returns ------- BezierSurface Transformed surface """ transformation = Transformation3D(**transformation_kwargs) initial_control_points = self.get_control_point_array() return BezierSurface( np.array([transformation.transform(p_arr) for p_arr in initial_control_points]), name=self.name, construction=self.construction )
[docs] def generate_control_point_net(self) -> (typing.List[Point3D], typing.List[Line3D]): """ Generates a list of :obj:`~aerocaps.geom.point.Point3D` and :obj:`~aerocaps.geom.curves.Line3D` objects representing the Bézier surface's control points and connections between them Returns ------- typing.List[Point3D], typing.List[Line3D] Control points and lines between adjacent control points in flattened lists """ points = [] lines = [] control_points = self.get_control_point_array() for i in range(self.n_points_u): for j in range(self.n_points_v): points.append(Point3D.from_array(control_points[i, j, :])) for i in range(self.n_points_u - 1): for j in range(self.n_points_v - 1): point_obj_1 = Point3D.from_array(control_points[i, j, :]) point_obj_2 = Point3D.from_array(control_points[i + 1, j, :]) point_obj_3 = Point3D.from_array(control_points[i, j + 1, :]) line_1 = Line3D(p0=point_obj_1, p1=point_obj_2) line_2 = Line3D(p0=point_obj_1, p1=point_obj_3) lines.extend([line_1, line_2]) if i < self.n_points_u - 2 and j < self.n_points_v - 2: continue point_obj_4 = Point3D.from_array(control_points[i + 1, j + 1, :]) line_3 = Line3D(p0=point_obj_3, p1=point_obj_4) line_4 = Line3D(p0=point_obj_2, p1=point_obj_4) lines.extend([line_3, line_4]) return points, lines
[docs] def plot_surface(self, plot: pv.Plotter, Nu: int = 50, Nv: int = 50, **mesh_kwargs) -> pv.StructuredGrid: """ Plots the Bézier surface using the `pyvista <https://pyvista.org/>`_ library Parameters ---------- plot: :obj:`pyvista.Plotter` instance Nu: int Number of points to evaluate in the :math:`u`-parametric direction. Default: ``50`` Nv: int Number of points to evaluate in the :math:`v`-parametric direction. Default: ``50`` mesh_kwargs: Keyword arguments to pass to :obj:`pyvista.Plotter.add_mesh` Returns ------- pyvista.core.pointset.StructuredGrid The evaluated Bézier surface """ XYZ = self.evaluate_grid(Nu, Nv) grid = pv.StructuredGrid(XYZ[:, :, 0], XYZ[:, :, 1], XYZ[:, :, 2]) plot.add_mesh(grid, **mesh_kwargs) return grid
[docs] def plot_control_point_mesh_lines(self, plot: pv.Plotter, **line_kwargs) -> pv.Actor: """ Plots the network of lines connecting the Bézier surface control points using the `pyvista <https://pyvista.org/>`_ library Parameters ---------- plot: :obj:`pyvista.Plotter` instance line_kwargs: Keyword arguments to pass to the :obj:`pyvista.Plotter.add_lines` Returns ------- pv.Actor The lines actor """ _, line_objs = self.generate_control_point_net() line_arr = np.array([[line_obj.p0.as_array(), line_obj.p1.as_array()] for line_obj in line_objs]) line_arr = line_arr.reshape((len(line_objs) * 2, 3)) line_actor = plot.add_lines(line_arr, **line_kwargs) return line_actor
[docs] def plot_control_points(self, plot: pv.Plotter, **point_kwargs) -> pv.Actor: """ Plots the Bézier surface control points using the `pyvista <https://pyvista.org/>`_ library Parameters ---------- plot: :obj:`pyvista.Plotter` instance point_kwargs: Keyword arguments to pass to the :obj:`pyvista.Plotter.add_points` Returns ------- pv.Actor The points actor """ point_objs, _ = self.generate_control_point_net() point_arr = np.array([point_obj.as_array() for point_obj in point_objs]) point_actor = plot.add_points(point_arr, **point_kwargs) return point_actor
def __repr__(self): return (f"{self.name}: {self.degree_u} x {self.degree_v} {self.__class__.__name__} " f"({self.degree_u + 1} x {self.degree_v + 1} control points)")
[docs] class RationalBezierSurface(Surface): """ Rational Bézier surface class. A NURBS surface with no internal knot vectors. """
[docs] def __init__(self, points: typing.List[typing.List[Point3D]] or np.ndarray, weights: np.ndarray, name: str = "RationalBezierSurface", construction: bool = False ): """ Parameters ---------- points weights name: str Name of the geometric object. May be re-assigned a unique name when added to a :obj:`~aerocaps.geom.geometry_container.GeometryContainer`. Default: 'RationalBezierSurface' construction: bool Whether this is a geometry used only for construction of other geometries. If ``True``, this geometry will not be exported or plotted. Default: ``False`` """ if isinstance(points, np.ndarray): points = [[Point3D.from_array(pt_row) for pt_row in pt_mat] for pt_mat in points] self.points = points knots_u = np.zeros(2 * len(points)) knots_v = np.zeros(2 * len(points[0])) knots_u[len(points):] = 1.0 knots_v[len(points[0]):] = 1.0 degree_u = len(points) - 1 degree_v = len(points[0]) - 1 assert knots_u.ndim == 1 assert knots_v.ndim == 1 assert weights.ndim == 2 assert len(knots_u) == len(points) + degree_u + 1 assert len(knots_v) == len(points[0]) + degree_v + 1 assert len(points) == weights.shape[0] assert len(points[0]) == weights.shape[1] # Negative weight check for weight_row in weights: for weight in weight_row: if weight < 0: raise NegativeWeightError("All weights must be non-negative") self._knots_u = knots_u self._knots_v = knots_v self.weights = deepcopy(weights) super().__init__(name=name, construction=construction)
@property def n_points_u(self) -> int: """Number of control points in the :math:`u`-parametric direction""" return len(self.points) @property def n_points_v(self) -> int: """Number of control points in the :math:`v`-parametric direction""" return len(self.points[0]) @property def degree_u(self) -> int: """Surface degree in the :math:`u`-parametric direction""" return self.n_points_u - 1 @property def degree_v(self) -> int: """Surface degree in the :math:`v`-parametric direction""" return self.n_points_v - 1 @property def n(self) -> int: """ Shorthand for :obj:`~aerocaps.geom.surfaces.RationalBezierSurface.degree_u` Returns ------- int Surface degree in the :math:`u`-parametric direction """ return self.degree_u @property def m(self) -> int: """ Shorthand for :obj:`~aerocaps.geom.surfaces.RationalBezierSurface.degree_v` Returns ------- int Surface degree in the :math:`v`-parametric direction """ return self.degree_v @property def knots_u(self) -> np.ndarray: """Knots in the :math:`u`-direction""" return self._knots_u @property def knots_v(self) -> np.ndarray: """Knots in the :math:`v`-direction""" return self._knots_v
[docs] def to_iges(self, *args, **kwargs) -> aerocaps.iges.entity.IGESEntity: return aerocaps.iges.surfaces.RationalBSplineSurfaceIGES( control_points=self.get_control_point_array(), knots_u=self.knots_u, knots_v=self.knots_v, weights=self.weights, degree_u=self.degree_u, degree_v=self.degree_v )
[docs] def get_control_point_array(self) -> np.ndarray: """ Converts the nested list of control points to a 3-D :obj:`~numpy.ndarray`. Returns ------- numpy.ndarray 3-D array """ return np.array([np.array([p.as_array() for p in p_arr]) for p_arr in self.points])
[docs] def get_homogeneous_control_points(self) -> np.ndarray: r""" Gets the array of control points in homogeneous coordinates, :math:`\mathbf{P}_{i,j} \cdot w_{i,j}` Returns ------- numpy.ndarray Array of size :math:`(n + 1) \times (m + 1) \times 4`, where :math:`n` is the surface degree in the :math:`u`-direction and :math:`m` is the surface degree in the :math:`v`-direction. The four elements of the last array dimension are, in order, the :math:`x`-coordinate, :math:`y`-coordinate, :math:`z`-coordinate, and weight of each control point. """ return np.dstack(( self.get_control_point_array() * np.repeat(self.weights[:, :, np.newaxis], 3, axis=2), self.weights ))
[docs] @staticmethod def project_homogeneous_control_points(homogeneous_points: np.ndarray) -> (np.ndarray, np.ndarray): """ Projects the homogeneous coordinates onto the :math:`w=1` hyperplane. Returns ------- numpy.ndarray, numpy.ndarray The projected coordinates in three-dimensional space followed by the weight array """ P = homogeneous_points[:, :, :3] / np.repeat(homogeneous_points[:, :, -1][:, :, np.newaxis], 3, axis=2) w = homogeneous_points[:, :, -1] return P, w
[docs] def elevate_degree_u(self) -> "RationalBezierSurface": """ Elevates the degree of the rational Bézier surface in the :math:`u`-parametric direction. Returns ------- RationalBezierSurface A new rational Bézier surface with identical shape to the current one but with one additional row of control points in the :math:`u`-parametric direction """ n = self.degree_u m = self.degree_v Pw = self.get_homogeneous_control_points() # New array has one additional control point (current array only has n+1 control points) new_Pw = np.zeros((Pw.shape[0] + 1, Pw.shape[1], Pw.shape[2])) # Set starting and ending control points to what they already were new_Pw[0, :, :] = Pw[0, :, :] new_Pw[-1, :, :] = Pw[-1, :, :] # Update all the other control points for i in range(1, n + 1): # 1 <= i <= n for j in range(0, m + 1): # for all j new_Pw[i, j, :] = i / (n + 1) * Pw[i - 1, j, :] + (1 - i / (n + 1)) * Pw[i, j, :] # Extract projected control points and weights from array new_P, new_w = self.project_homogeneous_control_points(new_Pw) return RationalBezierSurface(new_P, new_w)
[docs] def elevate_degree_v(self) -> "RationalBezierSurface": """ Elevates the degree of the rational Bézier surface in the :math:`v`-parametric direction. Returns ------- RationalBezierSurface A new rational Bézier surface with identical shape to the current one but with one additional row of control points in the :math:`v`-parametric direction """ n = self.degree_u m = self.degree_v Pw = self.get_homogeneous_control_points() # New array has one additional control point (current array only has n+1 control points) new_Pw = np.zeros((Pw.shape[0], Pw.shape[1] + 1, Pw.shape[2])) # Set starting and ending control points to what they already were new_Pw[:, 0, :] = Pw[:, 0, :] new_Pw[:, -1, :] = Pw[:, -1, :] # Update all the other control points for i in range(0, n + 1): # for all i for j in range(1, m + 1): # 1 <= j <= m new_Pw[i, j, :] = j / (m + 1) * Pw[i, j - 1, :] + (1 - j / (m + 1)) * Pw[i, j, :] # Extract projected control points and weights from array new_P, new_w = self.project_homogeneous_control_points(new_Pw) return RationalBezierSurface(new_P, new_w)
[docs] @classmethod def from_bezier_revolve(cls, bezier: BezierCurve3D, axis: Line3D, start_angle: Angle, end_angle: Angle): """ Creates a rational Bézier surface from the revolution of a Bézier curve about an axis. Parameters ---------- bezier: BezierCurve3D Bézier curve to revolve axis: Line3D Axis of revolution start_angle: Angle Starting angle for the revolve end_angle: Angle Ending angle for the revolve Returns ------- RationalBezierSurface Surface of revolution """ # if abs(end_angle.rad - start_angle.rad) > np.pi / 2: # raise ValueError("Angle difference must be less than or equal to 90 degrees for a rational Bezier surface" # " creation from Bezier revolve. For angle differences larger than 90 degrees, use" # " NURBSSurface.from_bezier_revolve.") def _determine_angle_distribution() -> typing.List[Angle]: angle_diff = abs(end_angle.rad - start_angle.rad) if angle_diff == 0.0: raise InvalidGeometryError("Starting and ending angles cannot be the same for a " "NURBSSurface from revolved Bezier curve") if angle_diff % (0.5 * np.pi) == 0.0: # If angle difference is a multiple of 90 degrees N_angles = 2 * int(angle_diff // (0.5 * np.pi)) + 1 else: N_angles = 2 * int(angle_diff // (0.5 * np.pi)) + 3 rad_dist = np.linspace(start_angle.rad, end_angle.rad, N_angles) return [Angle(rad=r) for r in rad_dist] control_points = [] weights = [] angles = _determine_angle_distribution() for point in bezier.control_points: axis_projection = project_point_onto_line(point, axis) radius = measure_distance_point_line(point, axis) if radius == 0.0: new_points = [point for _ in angles] else: new_points = [rotate_point_about_axis(point, axis, angle) for angle in angles] for idx, rotated_point in enumerate(new_points): if idx == 0: weights.append([]) if not idx % 2: # Skip even indices (these represent the "through" control points) weights[-1].append(1.0) continue sine_half_angle = np.sin(0.5 * np.pi - 0.5 * (angles[idx + 1].rad - angles[idx - 1].rad)) if radius != 0.0: distance = radius / sine_half_angle # radius / sin(half angle) vector = Vector3D(p0=axis_projection, p1=rotated_point) new_points[idx] = axis_projection + Point3D.from_array( distance * np.array(vector.normalized_value())) weights[-1].append(sine_half_angle) control_points.append(np.array([new_point.as_array() for new_point in new_points])) control_points = np.array(control_points) weights = np.array(weights) return cls(control_points, weights)
[docs] @staticmethod def fill_surface_from_four_boundaries(left_curve: BezierCurve3D or RationalBezierCurve3D, right_curve: BezierCurve3D or RationalBezierCurve3D, top_curve: BezierCurve3D or RationalBezierCurve3D, bottom_curve: BezierCurve3D or RationalBezierCurve3D, displacement_degree: int = 3) -> "RationalBezierSurface": """ Creates a fill surface from four boundary curves by linearly interpolating the ``left_curve`` and ``right_curve`` and displacing the edges created by the interpolation to form the ``top_curve`` and ``bottom_curve`` boundaries. .. figure:: ../images/fill_surface.* :width: 600 :align: center Fill surface from four curve boundaries .. warning:: While this method works for non-planar sets of boundary curves, the primary intended use is for co-planar boundary curves. Undesirable surface shapes may result if using non-planar curves. Parameters ---------- left_curve: BezierCurve3D or RationalBezierCurve3D Left boundary curve right_curve: Bezier3D or RationalBezierCurve3D Right boundary curve top_curve: Bezier3D or RationalBezierCurve3D Top boundary curve bottom_curve: Bezier3D or RationalBezierCurve3D Bottom boundary curve displacement_degree: int Degree of function used to displace the surface control points to accommodate the top and bottom surfaces. Default: 3 Returns ------- RationalBezierSurface Fill surface """ # Convert the boundary curves to rational Bézier curves if they are non-rational if isinstance(left_curve, BezierCurve3D): left_curve = left_curve.to_rational_bezier_curve() if isinstance(right_curve, BezierCurve3D): right_curve = right_curve.to_rational_bezier_curve() if isinstance(top_curve, BezierCurve3D): top_curve = top_curve.to_rational_bezier_curve() if isinstance(bottom_curve, BezierCurve3D): bottom_curve = bottom_curve.to_rational_bezier_curve() # Ensure the boundary curve loop is closed left_cps = left_curve.get_homogeneous_control_points() right_cps = right_curve.get_homogeneous_control_points() top_cps = top_curve.get_homogeneous_control_points() bottom_cps = bottom_curve.get_homogeneous_control_points() corners = np.array([ left_cps[0], left_cps[-1], right_cps[0], right_cps[-1], top_cps[0], top_cps[-1], bottom_cps[0], bottom_cps[-1] ]) unique_corners = unique_with_tolerance(corners) if len(unique_corners) > 4: raise ValueError("Boundary curve loop is not closed") # Re-orient the curves if necessary if not ( all(np.isclose(left_cps[-1], top_cps[0])) or all(np.isclose(left_cps[0], top_cps[0])) ): top_curve = top_curve.reverse() if not ( all(np.isclose(left_cps[-1], top_cps[-1])) or all(np.isclose(left_cps[0], top_cps[-1])) ): raise ValueError("Top curve and left curve are not connected") else: if not ( all(np.isclose(right_cps[-1], top_cps[-1])) or all(np.isclose(right_cps[0], top_cps[-1])) or all(np.isclose(right_cps[-1], top_cps[0])) or all(np.isclose(right_cps[0], top_cps[0])) ): raise ValueError("Top curve and right curve are not connected") if not ( all(np.isclose(left_cps[-1], bottom_cps[0])) or all(np.isclose(left_cps[0], bottom_cps[0])) ): bottom_curve = bottom_curve.reverse() if not ( all(np.isclose(left_cps[-1], bottom_cps[-1])) or all(np.isclose(left_cps[0], bottom_cps[-1])) ): raise ValueError("Bottom curve and left curve are not connected") else: if not ( all(np.isclose(right_cps[-1], bottom_cps[-1])) or all(np.isclose(right_cps[0], bottom_cps[-1])) or all(np.isclose(right_cps[-1], bottom_cps[0])) or all(np.isclose(right_cps[0], bottom_cps[0])) ): raise ValueError("Bottom curve and right curve are not connected") # Elevate the curve degrees of the curve pairs so that they match internally if left_curve.degree != right_curve.degree: if left_curve.degree < right_curve.degree: while left_curve.degree < right_curve.degree: left_curve = left_curve.elevate_degree() else: while right_curve.degree < left_curve.degree: right_curve = right_curve.elevate_degree() if top_curve.degree != bottom_curve.degree: if top_curve.degree < bottom_curve.degree: while top_curve.degree < bottom_curve.degree: top_curve = top_curve.elevate_degree() else: while bottom_curve.degree < top_curve.degree: bottom_curve = bottom_curve.elevate_degree() # Retrieve the new homogeneous control points for each (possibly modified) curve left_cps = left_curve.get_homogeneous_control_points() right_cps = right_curve.get_homogeneous_control_points() top_cps = top_curve.get_homogeneous_control_points() bottom_cps = bottom_curve.get_homogeneous_control_points() # Linearly interpolate between the left and right curves, length equal to number of control points # in the top/bottom direction arrays_to_stack = [] for t in np.linspace(0.0, 1.0, top_curve.degree + 1): arrays_to_stack.append(left_cps + t * (right_cps - left_cps)) Pw = np.array(arrays_to_stack) # Displace the control points associated with the top side of the surface for Pw_slice, curve_Pw in zip(Pw[1:-1], bottom_cps[1:-1]): displacement = curve_Pw - Pw_slice[0] for i in range(left_curve.degree): Pw_slice[i] += ((left_curve.degree - i) / left_curve.degree) ** displacement_degree * displacement # Displace the control points associated with the bottom side of the surface for Pw_slice, curve_Pw in zip(Pw[1:-1], top_cps[1:-1]): displacement = curve_Pw - Pw_slice[-1] for i in range(left_curve.degree): Pw_slice[-1 - i] += ((left_curve.degree - i) / left_curve.degree) ** displacement_degree * displacement # Project the new homogeneous control points onto the w=1 hyperplane new_points, new_weights = RationalBezierSurface.project_homogeneous_control_points(Pw) return RationalBezierSurface(new_points, new_weights)
[docs] def evaluate(self, u: float, v: float) -> np.ndarray: r""" Evaluates the surface at a given :math:`(u,v)` parameter pair. Parameters ---------- u: float Position along :math:`u` in parametric space. Normally in the range :math:`[0,1]` v: float Position along :math:`v` in parametric space. Normally in the range :math:`[0,1]` Returns ------- numpy.ndarray 1-D array of the form ``array([x, y, z])`` representing the evaluated point on the surface """ P = self.get_control_point_array() return np.array(rational_bezier_surf_eval(P, self.weights, u, v))
[docs] def evaluate_point3d(self, u: float, v: float) -> Point3D: r""" Evaluates the rational Bézier surface at a single :math:`(u,v)` parameter pair and returns a point object. Parameters ---------- u: float Position along :math:`u` in parametric space. Normally in the range :math:`[0,1]` v: float Position along :math:`v` in parametric space. Normally in the range :math:`[0,1]` Returns ------- Point3D Point object corresponding to the :math:`(u,v)` pair """ return Point3D.from_array(self.evaluate(u, v))
[docs] def evaluate_grid(self, Nu: int, Nv: int) -> np.ndarray: r""" Evaluates the rational Bézier surface on a uniform :math:`N_u \times N_v` grid of parameter values. Parameters ---------- Nu: int Number of uniformly spaced parameter values in the :math:`u`-direction Nv: int Number of uniformly spaced parameter values in the :math:`v`-direction Returns ------- numpy.ndarray Array of size :math:`N_u \times N_v \times 3` """ P = self.get_control_point_array() return np.array(rational_bezier_surf_eval_grid(P, self.weights, Nu, Nv))
[docs] def extract_edge_curve(self, surface_edge: SurfaceEdge) -> RationalBezierCurve3D: """ Extracts the control points and weights from one of the four edges of the rational Bézier surface and outputs a rational Bézier curve with these control points and weights Parameters ---------- surface_edge: SurfaceEdge Edge along which to extract the curve Returns ------- RationalBezierCurve3D Rational Bézier curve with control points corresponding to the control points along the edge of the surface """ P = self.get_control_point_array() w = self.weights if surface_edge == SurfaceEdge.u0: return RationalBezierCurve3D(P[0, :, :], w[0, :]) if surface_edge == SurfaceEdge.u1: return RationalBezierCurve3D(P[-1, :, :], w[-1, :]) if surface_edge == SurfaceEdge.v0: return RationalBezierCurve3D(P[:, 0, :], w[:, 0]) if surface_edge == SurfaceEdge.v1: return RationalBezierCurve3D(P[:, -1, :], w[:, -1]) raise ValueError(f"Invalid surface edge {surface_edge}")
[docs] def reverse_u(self) -> "RationalBezierSurface": """Reverses the surface in the :math:`u`-direction""" P = self.get_control_point_array()[::-1, :, :] w = self.weights[::-1, :] return RationalBezierSurface(P, w)
[docs] def reverse_v(self) -> "RationalBezierSurface": """Reverses the surface in the :math:`v`-direction""" P = self.get_control_point_array()[:, ::-1, :] w = self.weights[:, ::-1] return RationalBezierSurface(P, w)
[docs] def get_parallel_degree(self, surface_edge: SurfaceEdge): r""" Gets the degree of the curve corresponding to the input surface edge. Parameters ---------- surface_edge: SurfaceEdge Edge along which the parallel degree is evaluated Returns ------- int Degree parallel to the edge """ if surface_edge in [SurfaceEdge.v1, SurfaceEdge.v0]: return self.degree_u return self.degree_v
[docs] def get_perpendicular_degree(self, surface_edge: SurfaceEdge): r""" Gets the degree of the curve in the parametric direction perpendicular to the input surface edge. Parameters ---------- surface_edge: SurfaceEdge Edge along which the perpendicular degree is evaluated Returns ------- int Degree perpendicular to the edge """ if surface_edge in [SurfaceEdge.v1, SurfaceEdge.v0]: return self.degree_v return self.degree_u
[docs] def get_parallel_n_points(self, surface_edge: SurfaceEdge) -> int: r""" Gets the number of control points in the parametric direction parallel to the input surface edge. Parameters ---------- surface_edge: SurfaceEdge Edge along which the parallel number of control points is evaluated Returns ------- int Number of control points parallel to the edge """ if surface_edge in (SurfaceEdge.v1, SurfaceEdge.v0): return self.n_points_u return self.n_points_v
[docs] def get_perpendicular_n_points(self, surface_edge: SurfaceEdge) -> int: r""" Gets the number of control points in the parametric direction perpendicular to the input surface edge. Parameters ---------- surface_edge: SurfaceEdge Edge along which the perpendicular number of control points is evaluated Returns ------- int Number of control points perpendicular to the edge """ if surface_edge in (SurfaceEdge.v1, SurfaceEdge.v0): return self.n_points_v return self.n_points_u
[docs] def get_corner_index(self, surface_corner: SurfaceCorner) -> (int, int): """ Gets the :math:`i`- and :math:`j`-indices of the control point corresponding to the input corner Parameters ---------- surface_corner: SurfaceCorner Corner from which to retrieve the index Returns ------- int, int :math:`i`-index and :math:`j`-index, respectively """ if surface_corner == SurfaceCorner.u1v1: return self.degree_u, self.degree_v elif surface_corner == SurfaceCorner.u0v1: return 0, self.degree_v elif surface_corner == SurfaceCorner.u0v0: return 0, 0 elif surface_corner == SurfaceCorner.u1v0: return self.degree_u, 1 else: raise ValueError("Invalid surface_corner value")
[docs] def get_point(self, row_index: int, continuity_index: int, surface_edge: SurfaceEdge): r""" Gets the point corresponding to a particular index along the edge curve with perpendicular index corresponding to the level of continuity being applied. For example, for a :math:`6 \times 5` rational Bézier surface, the following code .. code-block:: python p = surf.get_point(2, 1, ac.SurfaceEdge.v0) returns the point :math:`\mathbf{P}_{2,1}` and .. code-block:: python p = surf.get_point(2, 1, ac.SurfaceEdge.u1) returns the point :math:`\mathbf{P}_{6-1,2} = \mathbf{P}_{5,2}`. .. seealso:: :obj:`~aerocaps.geom.surfaces.RationalBezierSurface.set_point` Setter equivalent of this method Parameters ---------- row_index: int Index along the surface edge control points continuity_index: int Index in the parametric direction perpendicular to the surface edge. Normally either ``0``, ``1``, or ``2`` surface_edge: SurfaceEdge Edge of the surface along which to retrieve the control point Returns ------- Point3D Point used to enforce :math:`G^x` continuity, where :math:`x` is the value of ``continuity_index`` """ if surface_edge == SurfaceEdge.v1: return self.points[row_index][-(continuity_index + 1)] elif surface_edge == SurfaceEdge.v0: return self.points[row_index][continuity_index] elif surface_edge == SurfaceEdge.u1: return self.points[-(continuity_index + 1)][row_index] elif surface_edge == SurfaceEdge.u0: return self.points[continuity_index][row_index] else: raise ValueError("Invalid surface_edge value")
[docs] def get_point_ij(self, row_index: int, continuity_index: int, surface_edge: SurfaceEdge) -> (int, int): r""" Gets the point indices corresponding to a particular index along the edge curve with perpendicular index corresponding to the level of continuity being applied. Parameters ---------- row_index: int Index along the surface edge control points continuity_index: int Index in the parametric direction perpendicular to the surface edge. Normally either ``0``, ``1``, or ``2`` surface_edge: SurfaceEdge Edge of the surface along which to retrieve the control point Returns ------- int, int Point indices used to enforce :math:`G^x` continuity, where :math:`x` is the value of ``continuity_index`` """ if surface_edge == SurfaceEdge.v1: return row_index, len(self.points[0]) - (continuity_index + 1) elif surface_edge == SurfaceEdge.v0: return row_index, continuity_index elif surface_edge == SurfaceEdge.u1: return len(self.points) - (continuity_index + 1), row_index elif surface_edge == SurfaceEdge.u0: return continuity_index, row_index else: raise ValueError("Invalid surface_edge value")
[docs] def set_point(self, point: Point3D, row_index: int, continuity_index: int, surface_edge: SurfaceEdge): r""" Sets the point corresponding to a particular index along the edge curve with perpendicular index corresponding to the level of continuity being applied. For example, for a :math:`6 \times 5` rational Bézier surface, the following code .. code-block:: python p = ac.Point3D.from_array(np.array([3.0, 4.0, 5.0])) surf.set_point(p, 2, 1, ac.SurfaceEdge.v0) sets the value of point :math:`\mathbf{P}_{2,1}` to :math:`[3,4,5]^T` and .. code-block:: python p = ac.Point3D.from_array(np.array([3.0, 4.0, 5.0])) surf.set_point(p, 2, 1, ac.SurfaceEdge.u1) sets the value of point :math:`\mathbf{P}_{6-1,2} = \mathbf{P}_{5,2}` to :math:`[3,4,5]^T`. .. seealso:: :obj:`~aerocaps.geom.surfaces.RationalBezierSurface.get_point` Getter equivalent of this method Parameters ---------- point: Point3D Point object to apply at the specified indices row_index: int Index along the surface edge control points continuity_index: int Index in the parametric direction perpendicular to the surface edge. Normally either ``0``, ``1``, or ``2`` surface_edge: SurfaceEdge Edge of the surface along which to retrieve the control point """ if surface_edge == SurfaceEdge.v1: self.points[row_index][-(continuity_index + 1)].x.m = point.x.m self.points[row_index][-(continuity_index + 1)].y.m = point.y.m self.points[row_index][-(continuity_index + 1)].z.m = point.z.m elif surface_edge == SurfaceEdge.v0: self.points[row_index][continuity_index].x.m = point.x.m self.points[row_index][continuity_index].y.m = point.y.m self.points[row_index][continuity_index].z.m = point.z.m elif surface_edge == SurfaceEdge.u1: self.points[-(continuity_index + 1)][row_index].x.m = point.x.m self.points[-(continuity_index + 1)][row_index].y.m = point.y.m self.points[-(continuity_index + 1)][row_index].z.m = point.z.m elif surface_edge == SurfaceEdge.u0: self.points[continuity_index][row_index].x.m = point.x.m self.points[continuity_index][row_index].y.m = point.y.m self.points[continuity_index][row_index].z.m = point.z.m else: raise ValueError("Invalid surface_edge value")
[docs] def get_weight(self, row_index: int, continuity_index: int, surface_edge: SurfaceEdge) -> float: r""" Gets the weight corresponding to a particular index along the edge curve with perpendicular index corresponding to the level of continuity being applied. For example, for a :math:`6 \times 5` rational Bézier surface, the following code .. code-block:: python w = surf.get_weight(2, 1, ac.SurfaceEdge.v0) returns the weight :math:`w_{2,1}` and .. code-block:: python w = surf.get_weight(2, 1, ac.SurfaceEdge.u1) returns the weight :math:`w_{6-1,2} = w_{5,2}`. .. seealso:: :obj:`~aerocaps.geom.surfaces.RationalBezierSurface.set_weight` Setter equivalent of this method Parameters ---------- row_index: int Index along the surface edge weights continuity_index: int Index in the parametric direction perpendicular to the surface edge. Normally either ``0``, ``1``, or ``2`` surface_edge: SurfaceEdge Edge of the surface along which to retrieve the weight Returns ------- float Weight used to enforce :math:`G^x` continuity, where :math:`x` is the value of ``continuity_index`` """ if surface_edge == SurfaceEdge.v1: return self.weights[row_index][-(continuity_index + 1)] elif surface_edge == SurfaceEdge.v0: return self.weights[row_index][continuity_index] elif surface_edge == SurfaceEdge.u1: return self.weights[-(continuity_index + 1)][row_index] elif surface_edge == SurfaceEdge.u0: return self.weights[continuity_index][row_index] else: raise ValueError("Invalid surface_edge value")
[docs] def set_weight(self, weight: float, row_index: int, continuity_index: int, surface_edge: SurfaceEdge): r""" Sets the weight corresponding to a particular index along the edge curve with perpendicular index corresponding to the level of continuity being applied. For example, for a :math:`6 \times 5` rational Bézier surface, the following code .. code-block:: python surf.set_weight(0.9, 2, 1, ac.SurfaceEdge.v0) sets the value of weight :math:`w_{2,1}` to :math:`0.9` and .. code-block:: python surf.set_weight(1.1, 2, 1, ac.SurfaceEdge.u1) sets the value of weight :math:`w_{6-1,2} = w_{5,2}` to :math:`1.1`. .. seealso:: :obj:`~aerocaps.geom.surfaces.RationalBezierSurface.get_weight` Getter equivalent of this method Parameters ---------- weight: float Weight to apply at the specified indices row_index: int Index along the surface edge weights continuity_index: int Index in the parametric direction perpendicular to the surface edge. Normally either ``0``, ``1``, or ``2`` surface_edge: SurfaceEdge Edge of the surface along which to retrieve the weight """ if surface_edge == SurfaceEdge.v1: self.weights[row_index][-(continuity_index + 1)] = weight elif surface_edge == SurfaceEdge.v0: self.weights[row_index][continuity_index] = weight elif surface_edge == SurfaceEdge.u1: self.weights[-(continuity_index + 1)][row_index] = weight elif surface_edge == SurfaceEdge.u0: self.weights[continuity_index][row_index] = weight else: raise ValueError("Invalid surface_edge value")
@staticmethod def _evaluate_f_sign(surf_edge_1: SurfaceEdge, surf_edge_2: SurfaceEdge) -> float: """ Evaluates the sign of the tangent proportionality factor across an edge pair Parameters ---------- surf_edge_1: SurfaceEdge First surface edge surf_edge_2: SurfaceEdge Second surface edge Returns ------- float ``-1.0`` if both surface edges end in 0 or both surface edges end in 1, ``1.0`` otherwise """ surf_edges_0 = (SurfaceEdge.u0, SurfaceEdge.v0) surf_edges_1 = (SurfaceEdge.u1, SurfaceEdge.v1) if surf_edge_1 in surf_edges_0 and surf_edge_2 in surf_edges_0: return -1.0 if surf_edge_1 in surf_edges_1 and surf_edge_2 in surf_edges_1: return -1.0 return 1.0
[docs] def enforce_g0(self, other: "RationalBezierSurface", surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge): r""" Enforces :math:`G^0` continuity along the input ``surface_edge`` by equating the control points and weights along this edge to the corresponding control points and weights along the ``other_surface_edge`` of the rational Bézier surface given by ``other``. The control points of the surface from which this method is called are modified in-place, and the control points of ``other`` are left unchanged. .. important:: The parallel degree of the current surface along ``surface_edge`` must be equal to the parallel degree of the ``other`` surface along ``other_surface_edge``, otherwise an ``AssertionError`` will be raised. If these degrees are not equal, first elevate the degree of the surface with the lower parallel degree until the degrees match using either :obj:`~aerocaps.geom.surfaces.RationalBezierSurface.elevate_degree_u` or :obj:`~aerocaps.geom.surfaces.RationalBezierSurface.elevate_degree_v`, whichever is appropriate. .. seealso:: :obj:`~aerocaps.geom.surfaces.RationalBezierSurface.enforce_c0` Parametric continuity equivalent (:math:`C^0`) Parameters ---------- other: RationalBezierSurface Another rational Bézier surface along which an edge will be used for stitching surface_edge: SurfaceEdge The edge of the current surface to modify other_surface_edge: SurfaceEdge Tool edge of surface ``other`` which determines the positions of control points along ``surface_edge`` of the current surface """ # P^b[:, 0] = P^a[:, -1] self_parallel_degree = self.get_parallel_degree(surface_edge) other_parallel_degree = other.get_parallel_degree(other_surface_edge) if self_parallel_degree != other_parallel_degree: raise ValueError(f"Degree parallel to the edge of the input surface ({self_parallel_degree}) does " f"not match the degree parallel to the edge of the other surface " f"({other_parallel_degree})") for row_index in range(self.get_parallel_degree(surface_edge) + 1): self.set_point(other.get_point(row_index, 0, other_surface_edge), row_index, 0, surface_edge) self.set_weight(other.get_weight(row_index, 0, other_surface_edge), row_index, 0, surface_edge)
[docs] def enforce_c0(self, other: "RationalBezierSurface", surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge): """ For zeroth-degree continuity, there is no difference between geometric (:math:`G^0`) and parametric (:math:`C^0`) continuity. Because this method is simply a convenience method that calls :obj:`~aerocaps.geom.surfaces.RationalBezierSurface.enforce_g0`, see the documentation for that method for more detailed documentation. Parameters ---------- other: RationalBezierSurface Another rational Bézier surface along which an edge will be used for stitching surface_edge: SurfaceEdge The edge of the current surface to modify other_surface_edge: SurfaceEdge Tool edge of surface ``other`` which determines the positions of control points along ``surface_edge`` of the current surface """ self.enforce_g0(other, surface_edge, other_surface_edge)
[docs] def enforce_g0g1(self, other: "RationalBezierSurface", f: float or np.ndarray, surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge): r""" First enforces :math:`G^0` continuity, then tangent (:math:`G^1`) continuity is enforced according to the following equations: .. math:: \mathcal{W}^{b,\mathcal{E}_b}_{k,1} = \mathcal{W}^{b,\mathcal{E}_b}_{k,0} + f \frac{p_{\perp}^{a,\mathcal{E}_a}}{p_{\perp}^{b,\mathcal{E}_b}} \left( \mathcal{W}^{a,\mathcal{E}_a}_{k,0} - \mathcal{W}^{a,\mathcal{E}_a}_{k,1} \right) \text{ for }k=0,1,\ldots,p_{\parallel}^{b,\mathcal{E}_b} .. math:: \mathcal{P}^{b,\mathcal{E}_b}_{k,1} = \frac{\mathcal{W}^{b,\mathcal{E}_b}_{k,0}}{\mathcal{W}^{b,\mathcal{E}_b}_{k,1}} \mathcal{P}^{b,\mathcal{E}_b}_{k,0} + f \frac{p_{\perp}^{a,\mathcal{E}_a}}{p_{\perp}^{b,\mathcal{E}_b}} \frac{1}{\mathcal{W}^{b,\mathcal{E}_b}_{k,1}} \left[\mathcal{W}^{a,\mathcal{E}_a}_{k,0} \mathcal{P}^{a,\mathcal{E}_a}_{k,0} - \mathcal{P}^{a,\mathcal{E}_a}_{k,1} \mathcal{W}^{a,\mathcal{E}_a}_{k,1} \right] \text{ for }k=0,1,\ldots,p_{\parallel}^{b,\mathcal{E}_b} Here, :math:`b` corresponds to the current surface, and :math:`a` corresponds to the ``other`` surface. The control points of the surface from which this method is called are modified in-place, and the control points of ``other`` are left unchanged. .. seealso:: :obj:`~aerocaps.geom.surfaces.RationalBezierSurface.enforce_g0` Geometric point continuity enforcement (:math:`G^0`) :obj:`~aerocaps.geom.surfaces.RationalBezierSurface.enforce_c0c1` Parametric continuity equivalent (:math:`C^1`) Parameters ---------- other: RationalBezierSurface Another rational Bézier surface along which an edge will be used for stitching f: float Tangent proportionality factor surface_edge: SurfaceEdge The edge of the current surface to modify other_surface_edge: SurfaceEdge Tool edge of surface ``other`` which determines the positions of control points along ``surface_edge`` of the current surface """ if isinstance(f, np.ndarray): assert len(f) == self.get_parallel_degree(surface_edge) + 1 self.enforce_g0(other, surface_edge, other_surface_edge) n_ratio = other.get_perpendicular_degree(other_surface_edge) / self.get_perpendicular_degree(surface_edge) for row_index in range(self.get_parallel_degree(surface_edge) + 1): f_row = f if isinstance(f, float) else f[row_index] w_i0_b = self.get_weight(row_index, 0, surface_edge) w_im_a = other.get_weight(row_index, 0, other_surface_edge) w_im1_a = other.get_weight(row_index, 1, other_surface_edge) w_i1_b = w_i0_b + f_row * n_ratio * (w_im_a - w_im1_a) if w_i1_b < 0: raise NegativeWeightError("G1 enforcement generated a negative weight") self.set_weight(w_i1_b, row_index, 1, surface_edge) P_i0_b = self.get_point(row_index, 0, surface_edge) P_im_a = other.get_point(row_index, 0, other_surface_edge) P_im1_a = other.get_point(row_index, 1, other_surface_edge) P_i1_b = w_i0_b / w_i1_b * P_i0_b + f_row * n_ratio / w_i1_b * (w_im_a * P_im_a - w_im1_a * P_im1_a) self.set_point(P_i1_b, row_index, 1, surface_edge)
[docs] def enforce_c0c1(self, other: "RationalBezierSurface", surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge): r""" Equivalent to calling :obj:`~aerocaps.geom.surfaces.RationalBezierSurface.enforce_g0g1` with ``f=1.0``. See that method for more detailed documentation. Parameters ---------- other: RationalBezierSurface Another rational Bézier surface along which an edge will be used for stitching surface_edge: SurfaceEdge The edge of the current surface to modify other_surface_edge: SurfaceEdge Tool edge of surface ``other`` which determines the positions of control points along ``surface_edge`` of the current surface """ self.enforce_g0g1(other, 1.0, surface_edge, other_surface_edge)
[docs] def enforce_g0g1g2(self, other: "RationalBezierSurface", f: float or np.ndarray, surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge): r""" First enforces :math:`G^0` and :math:`G^1` continuity, then curvature (:math:`G^2`) continuity is enforced according to the following equations: .. math:: \mathcal{W}^{b,\mathcal{E}_b}_{k,2} = 2 \mathcal{W}^{b,\mathcal{E}_b}_{k,1} - \mathcal{W}^{b,\mathcal{E}_b}_{k,0} + f^2 \frac{p_{\perp}^{a,\mathcal{E}_a}(p_{\perp}^{a,\mathcal{E}_a}-1)}{p_{\perp}^{b,\mathcal{E}_b}(p_{\perp}^{b,\mathcal{E}_b}-1)} \left[ \mathcal{W}^{a,\mathcal{E}_a}_{k,0} - 2 \mathcal{W}^{a,\mathcal{E}_a}_{k,1} + \mathcal{W}^{a,\mathcal{E}_a}_{k,2} \right] \text{ for }k=0,1,\ldots,p_{\parallel}^{b,\mathcal{E}_b} .. math:: \mathcal{P}^{b,\mathcal{E}_b}_{k,2} = 2 \frac{\mathcal{W}^{b,\mathcal{E}_b}_{k,1}}{\mathcal{W}^{b,\mathcal{E}_b}_{k,2}} \mathcal{P}^{b,\mathcal{E}_b}_{k,1} - \frac{\mathcal{W}^{b,\mathcal{E}_b}_{k,0}}{\mathcal{W}^{b,\mathcal{E}_b}_{k,2}} \mathcal{P}^{b,\mathcal{E}_b}_{k,0} + f^2 \frac{p_{\perp}^{a,\mathcal{E}_a}(p_{\perp}^{a,\mathcal{E}_a}-1)}{p_{\perp}^{b,\mathcal{E}_b}(p_{\perp}^{b,\mathcal{E}_b}-1)} \frac{1}{\mathcal{W}^{b,\mathcal{E}_b}_{k,2}} \left[ \mathcal{W}^{a,\mathcal{E}_a}_{k,1} \mathcal{P}^{a,\mathcal{E}_a}_{k,0} - 2 \mathcal{W}^{a,\mathcal{E}_a}_{k,1} \mathcal{P}^{a,\mathcal{E}_a}_{k,1} + \mathcal{W}^{a,\mathcal{E}_a}_{k,2} \mathcal{P}^{a,\mathcal{E}_a}_{k,2} \right] \text{ for }k=0,1,\ldots,p_{\parallel}^{b,\mathcal{E}_b} Here, :math:`b` corresponds to the current surface, and :math:`a` corresponds to the ``other`` surface. The control points of the surface from which this method is called are modified in-place, and the control points of ``other`` are left unchanged. .. seealso:: :obj:`~aerocaps.geom.surfaces.RationalBezierSurface.enforce_g0` Geometric point continuity enforcement (:math:`G^0`) :obj:`~aerocaps.geom.surfaces.RationalBezierSurface.enforce_g0g1` Geometric tangent continuity enforcement (:math:`G^1`) :obj:`~aerocaps.geom.surfaces.RationalBezierSurface.enforce_c0c1c2` Parametric continuity equivalent (:math:`C^2`) Parameters ---------- other: RationalBezierSurface Another rational Bézier surface along which an edge will be used for stitching f: float Tangent proportionality factor surface_edge: SurfaceEdge The edge of the current surface to modify other_surface_edge: SurfaceEdge Tool edge of surface ``other`` which determines the positions of control points along ``surface_edge`` of the current surface """ self.enforce_g0g1(other, f, surface_edge, other_surface_edge) n_ratio = (other.get_perpendicular_degree(other_surface_edge) ** 2 - other.get_perpendicular_degree(other_surface_edge)) / ( self.get_perpendicular_degree(surface_edge) ** 2 - self.get_perpendicular_degree( surface_edge)) for row_index in range(self.get_parallel_degree(surface_edge) + 1): w_i0_b = self.get_weight(row_index, 0, surface_edge) w_i1_b = self.get_weight(row_index, 1, surface_edge) w_im_a = other.get_weight(row_index, 0, other_surface_edge) w_im1_a = other.get_weight(row_index, 1, other_surface_edge) w_im2_a = other.get_weight(row_index, 2, other_surface_edge) f_row = f if isinstance(f, float) else f[row_index] w_i2_b = 2.0 * w_i1_b - w_i0_b + f_row ** 2 * n_ratio * (w_im_a - 2.0 * w_im1_a + w_im2_a) if w_i2_b < 0: raise NegativeWeightError("G2 enforcement generated a negative weight") self.set_weight(w_i2_b, row_index, 2, surface_edge) P_i0_b = self.get_point(row_index, 0, surface_edge) P_i1_b = self.get_point(row_index, 1, surface_edge) P_im_a = other.get_point(row_index, 0, other_surface_edge) P_im1_a = other.get_point(row_index, 1, other_surface_edge) P_im2_a = other.get_point(row_index, 2, other_surface_edge) P_i2_b = (2.0 * w_i1_b / w_i2_b * P_i1_b - w_i0_b / w_i2_b * P_i0_b) + f_row ** 2 * n_ratio * ( 1 / w_i2_b) * ( w_im_a * P_im_a - 2.0 * w_im1_a * P_im1_a + w_im2_a * P_im2_a) self.set_point(P_i2_b, row_index, 2, surface_edge)
[docs] def enforce_c0c1c2(self, other: "RationalBezierSurface", surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge): r""" Equivalent to calling :obj:`~aerocaps.geom.surfaces.RationalBezierSurface.enforce_g0g1g2` with ``f=1.0``. See that method for more detailed documentation. Parameters ---------- other: RationalBezierSurface Another rational Bézier surface along which an edge will be used for stitching surface_edge: SurfaceEdge The edge of the current surface to modify other_surface_edge: SurfaceEdge Tool edge of surface ``other`` which determines the positions of control points along ``surface_edge`` of the current surface """ self.enforce_g0g1g2(other, 1.0, surface_edge, other_surface_edge)
[docs] def enforce_g0g1g2_multiface(self, adjacent_surf_u0: "RationalBezierSurface" = None, adjacent_surf_u1: "RationalBezierSurface" = None, adjacent_surf_v0: "RationalBezierSurface" = None, adjacent_surf_v1: "RationalBezierSurface" = None, other_edge_u0: SurfaceEdge = None, other_edge_u1: SurfaceEdge = None, other_edge_v0: SurfaceEdge = None, other_edge_v1: SurfaceEdge = None, f_u0_initial: float = 1.0, f_u1_initial: float = 1.0, f_v0_initial: float = 1.0, f_v1_initial: float = 1.0, n_deriv_points: int = 10, ) -> OptimizeResult: r""" .. warning:: This is an experimental feature and should not be used in production geometries Enforces :math:`G^0`, :math:`G^1`, and :math:`G^2` continuity across multiple adjacent boundaries of a surface, up to all four boundaries. This is done by first enforcing :math:`G^0` continuity at all required boundaries and then optimizing the locations of the second and third rows of control points to minimize :math:`G^1` and :math:`G^2` error at ``n_deriv_points`` locations along each of the boundary curves. The following is the cost function that is minimized: .. math:: J(x_k) = \sum\limits_{l=1}^2 \sum\limits_{i=0}^{n_p n_{\mathcal{E}}} \left( \left. \frac{\partial^l \mathbf{S}_i^a(u,v)}{\partial \mu^l} \right|_{u=u_i,v=v_i} - \frac{f_{\text{sgn},i}}{f^l_i(x_k)} \left. \frac{\partial^l \mathbf{S}^b(u,v,x_k)}{\partial \mu^l} \right|_{u=u_i,v=v_i} \right)^2 where * :math:`x_k` is the set of design variables to be optimized including the internal control point locations and tangent proportionality factors across each boundary * :math:`n_p` is the number of discrete derivative calculations on each boundary (specified by ``n_deriv_points``) * :math:`n_\mathcal{E}` is the number of edges across which continuity is being enforced * :math:`\mathbf{S}_i^a(u,v)` is a surface specified by ``adjacent_surf_u0``, etc. * :math:`\mu` is equal to either :math:`u` or :math:`v`, determined by the parametric direction perpendicular to the edge * :math:`(u_i,v_i)` is the point along an edge where the derivative is being evaluated * :math:`f_{\text{sgn},i}` is the sign of the proportionality factor, :math:`-1` if both the target edge and other edge specified by :math:`i` end in :math:`0` or both end in :math:`1`, :math:`1` otherwise * :math:`f_i(x_k)` is the tangent proportionality factor * :math:`\mathbf{S}^b(u,v,x_k)` is the target surface (``self``) * :math:`l` is the derivative order For maximum performance of the optimizer, the exact Jacobian is calculated: .. math:: \frac{\partial J(x_k)}{\partial x_k} = 2 \sum\limits_{l=1}^2 \sum\limits_{i=0}^{n_p n_{\mathcal{E}}} \left( \left. \frac{\partial^l \mathbf{S}_i^a(u,v)}{\partial \mu^l} \right|_{u=u_i,v=v_i} - \frac{f_{\text{sgn},i}}{f^l_i(x_k)} \left. \frac{\partial^l \mathbf{S}^b(u,v,x_k)}{\partial \mu^l} \right|_{u=u_i,v=v_i} \right) \left[ -f_{\text{sgn},i} \frac{\partial}{\partial x_k} \left( \frac{1}{f^l_i} \right) \left( \left. \frac{\partial^l \mathbf{S}^b(u,v,x_k)}{\partial \mu^l} \right|_{u=u_i,v=v_i} \right) -\frac{f_{\text{sgn},i}}{f_i^l} \frac{\partial}{\partial x_k} \left( \left. \frac{\partial^l \mathbf{S}^b(u,v,x_k)}{\partial \mu^l} \right|_{u=u_i,v=v_i} \right) \right] .. note:: This method is reserved for the complex case where continuity is required across boundaries that share a surface corner. In the case of continuity with a single surface or a pair of surfaces with common boundaries on opposite sides of the surface (such as the :math:`v_0` and :math:`v_1` boundaries), the much simpler :obj:`~aerocaps.geom.surfaces.BezierSurface.enforce_g0g1g2` should be used. .. figure:: ../images/bezier_enforce_g0g1g2_multiface.* :align: center :width: 600 Multi-face :math:`G^0`, :math:`G^1`, and :math:`G^2` continuity enforcement Parameters ---------- adjacent_surf_u0: BezierSurface Surface sharing the :math:`u_0` boundary of ``target_surf``. Default: ``None`` adjacent_surf_u1: BezierSurface Surface sharing the :math:`u_1` boundary of ``target_surf``. Default: ``None`` adjacent_surf_v0: BezierSurface Surface sharing the :math:`v_0` boundary of ``target_surf``. Default: ``None`` adjacent_surf_v1: BezierSurface Surface sharing the :math:`v_1` boundary of ``target_surf``. Default: ``None`` other_edge_u0: SurfaceEdge Edge of surface ``adjacent_surf_u0`` that will be stitched. Default: ``None`` other_edge_u1: SurfaceEdge Edge of surface ``adjacent_surf_u0`` that will be stitched. Default: ``None`` other_edge_v0: SurfaceEdge Edge of surface ``adjacent_surf_u0`` that will be stitched. Default: ``None`` other_edge_v1: SurfaceEdge Edge of surface ``adjacent_surf_u0`` that will be stitched. Default: ``None`` f_u0_initial: float Initial value of the tangent proportionality factor across boundary :math:`u_0`. The final value selected by the optimizer will be different from this value. Default: ``1.0`` f_u1_initial: float Initial value of the tangent proportionality factor across boundary :math:`u_1`. The final value selected by the optimizer will be different from this value. Default: ``1.0`` f_v0_initial: float Initial value of the tangent proportionality factor across boundary :math:`v_0`. The final value selected by the optimizer will be different from this value. Default: ``1.0`` f_v1_initial: float Initial value of the tangent proportionality factor across boundary :math:`v_1`. The final value selected by the optimizer will be different from this value. Default: ``1.0`` n_deriv_points: int Number of discrete locations where the continuity error will be evaluated. Default: ``10`` Returns ------- OptimizeResult Result from the :math:`G^1`- and :math:`G^2`-continuity error minimization problem solution """ adjacent_surfs = (adjacent_surf_u0, adjacent_surf_u1, adjacent_surf_v0, adjacent_surf_v1) other_edges = (other_edge_u0, other_edge_u1, other_edge_v0, other_edge_v1) # Input validation if not any(adjacent_surfs): raise ValueError("Must specify at least one adjacent surface") if not any(other_edges): raise ValueError("Must specify at least one other edge") if len(adjacent_surfs) == 1: raise ValueError("For continuity enforcement with only one other surface, use 'enforce_g0g1' instead") if len(adjacent_surfs) != len(other_edges): raise ValueError("Must specify one 'other_edge' for every 'adjacent_surf'") # Create a mapping between the surfaces and edges surf_edge_mapping = { SurfaceEdge.u0: (adjacent_surf_u0, other_edge_u0, f_u0_initial), SurfaceEdge.u1: (adjacent_surf_u1, other_edge_u1, f_u1_initial), SurfaceEdge.v0: (adjacent_surf_v0, other_edge_v0, f_v0_initial), SurfaceEdge.v1: (adjacent_surf_v1, other_edge_v1, f_v1_initial) } for self_edge, other_data in surf_edge_mapping.items(): if any(other_data) or all(other_data): continue raise ValueError("Must specify either both an 'adjacent_surf' and an 'other_edge' or neither for every " "edge of the current surface") # Enforce G0 continuity with all surfaces for self_edge in surf_edge_mapping.keys(): data = surf_edge_mapping[self_edge] if data[0] is None: continue self.enforce_g0( data[0], surface_edge=self_edge, other_surface_edge=data[1] ) d1_other = { self_edge: data[0].get_first_derivs_along_edge(data[1], n_points=n_deriv_points) if data[0] else None for self_edge, data in surf_edge_mapping.items() } d2_other = { self_edge: data[0].get_second_derivs_along_edge(data[1], n_points=n_deriv_points) if data[0] else None for self_edge, data in surf_edge_mapping.items() } def get_point_ijs_to_update() -> typing.List[typing.Tuple[int]]: """Gets the indices of the points in the target surface that will be updated during the optimization""" point_ijs_to_update = [] for surface_edge, _data in surf_edge_mapping.items(): # Loop through all the points in the second row starting from the second point and ending at the # second-to-last point for row_index in range(1, self.get_parallel_n_points(surface_edge) - 1): point_ij = self.get_point_ij(row_index, continuity_index=1, surface_edge=surface_edge) if point_ij in point_ijs_to_update: continue point_ijs_to_update.append(point_ij) for surface_edge, _data in surf_edge_mapping.items(): for row_index in range(1, self.get_parallel_n_points(surface_edge) - 1): point_ij_2 = self.get_point_ij(row_index, continuity_index=2, surface_edge=surface_edge) if point_ij_2 in point_ijs_to_update: continue point_ijs_to_update.append(point_ij_2) return point_ijs_to_update f_signs = {self_edge: self._evaluate_f_sign(self_edge, data[1]) for self_edge, data in surf_edge_mapping.items() if data is not None} f_vals = {self_edge: data[2] for self_edge, data in surf_edge_mapping.items() if data is not None} mod_ijs = get_point_ijs_to_update() mod_points = [self.points[i][j] for i, j in mod_ijs] x0 = np.array([p.as_array() for p in mod_points]).flatten() x0 = np.append(x0, np.array(list(f_vals.values()))) def obj_fun_and_jac(x: np.ndarray) -> (float, np.ndarray): """ Computes the objective function as the sum of the squares of the :math:`G^1` continuity error, along with the Jacobian Parameters ---------- x: np.ndarray 1-D array of design variable values Returns ------- float, np.ndarray The objective function value and the Jacobian (a 1-D array of sensitivities) """ x_reshaped = x[:3 * len(mod_ijs)].reshape((len(mod_points), 3)) jac_arr = np.zeros(x.shape) # Update the points in-place for i in range(x_reshaped.shape[0]): mod_points[i].x.m = x_reshaped[i, 0] mod_points[i].y.m = x_reshaped[i, 1] mod_points[i].z.m = x_reshaped[i, 2] # Evaluate the objective function and Jacobian obj_fun_val = 0.0 for edge_idx, (target_edge, multiface_data) in enumerate(surf_edge_mapping.items()): if surf_edge_mapping[target_edge][0] is None: continue f = x[3 * len(mod_ijs) + edge_idx] f_sign = f_signs[target_edge] A = -f_sign * 1 / abs(f) A2 = -f_sign * (1 / f) ** 2 dA = f_sign * (1 / f) ** 2 dA2 = 2 * f_sign * (1 / abs(f)) ** 3 d1_self = self.get_first_derivs_along_edge(target_edge, n_points=n_deriv_points) d2_self = self.get_second_derivs_along_edge(target_edge, n_points=n_deriv_points) # Objective function value update obj_fun_val += np.sum((d1_other[target_edge] + A * d1_self) ** 2) obj_fun_val += np.sum((d2_other[target_edge] + A2 * d2_self) ** 2) # Jacobian update loop start_xyz = 0 # Jacobian array starting index for mod_ij_idx, mod_ij in enumerate(mod_ijs): d1_sens_self = self.get_first_deriv_cp_sens_along_edge( target_edge, mod_ij[0], mod_ij[1], n_points=n_deriv_points ) d2_sens_self = self.get_second_deriv_cp_sens_along_edge( target_edge, mod_ij[0], mod_ij[1], n_points=n_deriv_points ) # Jacobian array update for k in range(3): jac_arr[start_xyz + k] += 2 * np.sum( (d1_other[target_edge][:, k] + A * d1_self[:, k]) * A * d1_sens_self[:, k] + ( d2_other[target_edge][:, k] + A2 * d2_self[:, k] ) * A2 * d2_sens_self[:, k] ) # Increment the starting Jacobian index start_xyz += 3 jac_arr[3 * len(mod_ijs) + edge_idx] = np.sum( 2 * (d1_other[target_edge] + A * d1_self) * dA * d1_self + 2 * (d2_other[target_edge] + A2 * d2_self) * dA2 * d2_self ) return obj_fun_val, jac_arr res = minimize(obj_fun_and_jac, x0, jac=True) return res
[docs] def dSdu(self, u: float, v: float) -> np.ndarray: r""" Evaluates the first derivative with respect to :math:`u` at a single :math:`(u,v)` pair Parameters ---------- u: float Position along :math:`u` in parametric space. Normally in the range :math:`[0,1]` v: float Position along :math:`v` in parametric space. Normally in the range :math:`[0,1]` Returns ------- np.ndarray 1-D array containing the :math:`x`-, :math:`y`-, and :math:`z`-components of the second derivative """ P = self.get_control_point_array() return np.array(rational_bezier_surf_dsdu(P, self.weights, u, v))
[docs] def dSdu_grid(self, Nu: int, Nv: int) -> np.ndarray: r""" Evaluates the first derivative with respect to :math:`u` on a linearly-spaced grid of :math:`u`- and :math:`v`-values. Parameters ---------- Nu: int Number of evenly spaced :math:`u` values Nv: int Number of evenly spaced :math:`v` values Returns ------- np.ndarray Array of size :math:`N_u \times N_v \times 3` """ P = self.get_control_point_array() return np.array(rational_bezier_surf_dsdu_grid(P, self.weights, Nu, Nv))
[docs] def dSdu_uvvecs(self, u: np.ndarray, v: np.ndarray): r""" Evaluates the first derivative of the surface with respect to :math:`u` at arbitrary vectors of :math:`u` and :math:`v`-values. Parameters ---------- u: np.ndarray 1-D array of :math:`u`-parameter values v: np.ndarray 1-D array of :math:`v`-parameter values Returns ------- np.ndarray Array of size :math:`\text{len}(u) \times \text{len}(v) \times 3` """ P = self.get_control_point_array() return np.array(rational_bezier_surf_dsdu_uvvecs(P, self.weights, u, v))
[docs] def dSdv(self, u: float or np.ndarray, v: float or np.ndarray): r""" Evaluates the first derivative with respect to :math:`v` at a single :math:`(u,v)` pair Parameters ---------- u: float Position along :math:`u` in parametric space. Normally in the range :math:`[0,1]` v: float Position along :math:`v` in parametric space. Normally in the range :math:`[0,1]` Returns ------- np.ndarray 1-D array containing the :math:`x`-, :math:`y`-, and :math:`z`-components of the second derivative """ P = self.get_control_point_array() return np.array(rational_bezier_surf_dsdv(P, self.weights, u, v))
[docs] def dSdv_grid(self, Nu: int, Nv: int) -> np.ndarray: r""" Evaluates the first derivative with respect to :math:`v` on a linearly-spaced grid of :math:`u`- and :math:`v`-values. Parameters ---------- Nu: int Number of evenly spaced :math:`u` values Nv: int Number of evenly spaced :math:`v` values Returns ------- np.ndarray Array of size :math:`N_u \times N_v \times 3` """ P = self.get_control_point_array() return np.array(rational_bezier_surf_dsdv_grid(P, self.weights, Nu, Nv))
[docs] def dSdv_uvvecs(self, u: np.ndarray, v: np.ndarray): r""" Evaluates the first derivative of the surface with respect to :math:`v` at arbitrary vectors of :math:`u` and :math:`v`-values. Parameters ---------- u: np.ndarray 1-D array of :math:`u`-parameter values v: np.ndarray 1-D array of :math:`v`-parameter values Returns ------- np.ndarray Array of size :math:`\text{len}(u) \times \text{len}(v) \times 3` """ P = self.get_control_point_array() return np.array(rational_bezier_surf_dsdv_uvvecs(P, self.weights, u, v))
[docs] def d2Sdu2(self, u: float, v: float) -> np.ndarray: r""" Evaluates the second derivative with respect to :math:`u` at a single :math:`(u,v)` pair Parameters ---------- u: float Position along :math:`u` in parametric space. Normally in the range :math:`[0,1]` v: float Position along :math:`v` in parametric space. Normally in the range :math:`[0,1]` Returns ------- np.ndarray 1-D array containing the :math:`x`-, :math:`y`-, and :math:`z`-components of the second derivative """ P = self.get_control_point_array() return np.array(rational_bezier_surf_d2sdu2(P, self.weights, u, v))
[docs] def d2Sdu2_grid(self, Nu: int, Nv: int) -> np.ndarray: r""" Evaluates the second derivative with respect to :math:`u` on a linearly-spaced grid of :math:`u`- and :math:`v`-values. Parameters ---------- Nu: int Number of evenly spaced :math:`u` values Nv: int Number of evenly spaced :math:`v` values Returns ------- np.ndarray Array of size :math:`N_u \times N_v \times 3` """ P = self.get_control_point_array() return np.array(rational_bezier_surf_d2sdu2_grid(P, self.weights, Nu, Nv))
[docs] def d2Sdu2_uvvecs(self, u: np.ndarray, v: np.ndarray): r""" Evaluates the second derivative of the surface with respect to :math:`u` at arbitrary vectors of :math:`u` and :math:`v`-values. Parameters ---------- u: np.ndarray 1-D array of :math:`u`-parameter values v: np.ndarray 1-D array of :math:`v`-parameter values Returns ------- np.ndarray Array of size :math:`\text{len}(u) \times \text{len}(v) \times 3` """ P = self.get_control_point_array() return np.array(rational_bezier_surf_d2sdu2_uvvecs(P, self.weights, u, v))
[docs] def d2Sdv2(self, u: float or np.ndarray, v: float or np.ndarray): r""" Evaluates the second derivative with respect to :math:`v` at a single :math:`(u,v)` pair Parameters ---------- u: float Position along :math:`u` in parametric space. Normally in the range :math:`[0,1]` v: float Position along :math:`v` in parametric space. Normally in the range :math:`[0,1]` Returns ------- np.ndarray 1-D array containing the :math:`x`-, :math:`y`-, and :math:`z`-components of the second derivative """ P = self.get_control_point_array() return np.array(rational_bezier_surf_d2sdv2(P, self.weights, u, v))
[docs] def d2Sdv2_grid(self, Nu: int, Nv: int) -> np.ndarray: r""" Evaluates the second derivative with respect to :math:`v` on a linearly-spaced grid of :math:`u`- and :math:`v`-values. Parameters ---------- Nu: int Number of evenly spaced :math:`u` values Nv: int Number of evenly spaced :math:`v` values Returns ------- np.ndarray Array of size :math:`N_u \times N_v \times 3` """ P = self.get_control_point_array() return np.array(rational_bezier_surf_d2sdv2_grid(P, self.weights, Nu, Nv))
[docs] def d2Sdv2_uvvecs(self, u: np.ndarray, v: np.ndarray): r""" Evaluates the second derivative of the surface with respect to :math:`v` at arbitrary vectors of :math:`u` and :math:`v`-values. Parameters ---------- u: np.ndarray 1-D array of :math:`u`-parameter values v: np.ndarray 1-D array of :math:`v`-parameter values Returns ------- np.ndarray Array of size :math:`\text{len}(u) \times \text{len}(v) \times 3` """ P = self.get_control_point_array() return np.array(rational_bezier_surf_d2sdv2_uvvecs(P, self.weights, u, v))
[docs] def get_edge(self, edge: SurfaceEdge, n_points: int = 10) -> np.ndarray: r""" Evaluates the surface at ``n_points`` parameter locations along a given edge. Parameters ---------- edge: SurfaceEdge Edge along which to evaluate n_points: int Number of evenly-spaced parameter locations at which to evaluate the edge curve. Default: 10 Returns ------- numpy.ndarray 2-D array of size :math:`n_\text{points} \times 3` """ P = self.get_control_point_array() if edge == SurfaceEdge.v1: return np.array(rational_bezier_surf_eval_iso_v(P, self.weights, n_points, 1.0)) elif edge == SurfaceEdge.v0: return np.array(rational_bezier_surf_eval_iso_v(P, self.weights, n_points, 0.0)) elif edge == SurfaceEdge.u1: return np.array(rational_bezier_surf_eval_iso_u(P, self.weights, 1.0, n_points)) elif edge == SurfaceEdge.u0: return np.array(rational_bezier_surf_eval_iso_u(P, self.weights, 0.0, n_points)) else: raise ValueError(f"No edge called {edge}")
[docs] def get_first_derivs_along_edge(self, edge: SurfaceEdge, n_points: int = 10, perp: bool = True) -> np.ndarray: r""" Evaluates the parallel or perpendicular derivative along a surface edge at ``n_points`` parameter locations. The derivative represents either :math:`\frac{\partial \mathbf{S}(u,v)}{\partial u}` or :math:`\frac{\partial \mathbf{S}(u,v)}{\partial v}` depending on which edge is selected and which value is assigned to ``perp``. Parameters ---------- edge: SurfaceEdge Edge along which to evaluate n_points: int Number of evenly-spaced parameter locations at which to evaluate the derivative. Default: 10 perp: bool Whether to evaluate the cross-derivative. If ``False``, the derivative along the parameter direction parallel to the edge will be evaluated instead. Default: ``True`` Returns ------- numpy.ndarray 2-D array of size :math:`n_\text{points} \times 3` """ P = self.get_control_point_array() if edge == SurfaceEdge.v1: return np.array(rational_bezier_surf_dsdv_iso_v(P, self.weights, n_points, 1.0)) if perp else np.array( rational_bezier_surf_dsdu_iso_v(P, self.weights, n_points, 1.0)) elif edge == SurfaceEdge.v0: return np.array(rational_bezier_surf_dsdv_iso_v(P, self.weights, n_points, 0.0)) if perp else np.array( rational_bezier_surf_dsdu_iso_v(P, self.weights, n_points, 0.0)) elif edge == SurfaceEdge.u1: return np.array(rational_bezier_surf_dsdu_iso_u(P, self.weights, 1.0, n_points)) if perp else np.array( rational_bezier_surf_dsdv_iso_u(P, self.weights, 1.0, n_points)) elif edge == SurfaceEdge.u0: return np.array(rational_bezier_surf_dsdu_iso_u(P, self.weights, 0.0, n_points)) if perp else np.array( rational_bezier_surf_dsdv_iso_u(P, self.weights, 0.0, n_points)) else: raise ValueError(f"No edge called {edge}")
[docs] def get_first_deriv_cp_sens_along_edge(self, edge: SurfaceEdge, i: int, j: int, n_points: int = 10, perp: bool = True) -> np.ndarray: r""" Gets the sensitivity of the first :math:`u`- or :math:`v`-derivative along an edge with respect to control point :math:`\mathbf{P}_{i,j}` Parameters ---------- edge: SurfaceEdge Edge along which to evaluate i: int :math:`i`-index of the control point j: int :math:`j`-index of the control point n_points: int Number of evenly-spaced parameter locations at which to evaluate the derivative. Default: 10 perp: bool Whether to evaluate the cross-derivative. If ``False``, the derivative along the parameter direction parallel to the edge will be evaluated instead. Default: ``True`` Returns ------- numpy.ndarray 2-D array of size :math:`n_\text{points} \times 3` """ w = self.weights if edge == SurfaceEdge.v1: return np.array( rational_bezier_surf_dsdv_dp_iso_v(w, i, j, self.n, self.m, 3, n_points, 1.0)) if perp else np.array( rational_bezier_surf_dsdu_dp_iso_v(w, i, j, self.n, self.m, 3, n_points, 1.0)) elif edge == SurfaceEdge.v0: return np.array( rational_bezier_surf_dsdv_dp_iso_v(w, i, j, self.n, self.m, 3, n_points, 0.0)) if perp else np.array( rational_bezier_surf_dsdu_dp_iso_v(w, i, j, self.n, self.m, 3, n_points, 0.0)) elif edge == SurfaceEdge.u1: return np.array( rational_bezier_surf_dsdu_dp_iso_u(w, i, j, self.n, self.m, 3, 1.0, n_points)) if perp else np.array( rational_bezier_surf_dsdv_dp_iso_u(w, i, j, self.n, self.m, 3, 1.0, n_points)) elif edge == SurfaceEdge.u0: return np.array( rational_bezier_surf_dsdu_dp_iso_u(w, i, j, self.n, self.m, 3, 0.0, n_points)) if perp else np.array( rational_bezier_surf_dsdv_dp_iso_u(w, i, j, self.n, self.m, 3, 0.0, n_points)) else: raise ValueError(f"No edge called {edge}")
[docs] def get_second_derivs_along_edge(self, edge: SurfaceEdge, n_points: int = 10, perp: bool = True) -> np.ndarray: r""" Evaluates the parallel or perpendicular second derivative along a surface edge at ``n_points`` parameter locations. The derivative represents either :math:`\frac{\partial^2 \mathbf{S}(u,v)}{\partial u^2}` or :math:`\frac{\partial^2 \mathbf{S}(u,v)}{\partial v^2}` depending on which edge is selected and which value is assigned to ``perp``. Parameters ---------- edge: SurfaceEdge Edge along which to evaluate n_points: int Number of evenly-spaced parameter locations at which to evaluate the second derivative. Default: 10 perp: bool Whether to evaluate the cross-derivative. If ``False``, the second derivative along the parameter direction parallel to the edge will be evaluated instead. Default: ``True`` Returns ------- numpy.ndarray 2-D array of size :math:`n_\text{points} \times 3` """ P = self.get_control_point_array() if edge == SurfaceEdge.v1: return np.array(rational_bezier_surf_d2sdv2_iso_v(P, self.weights, n_points, 1.0)) if perp else np.array( rational_bezier_surf_d2sdu2_iso_v(P, self.weights, n_points, 1.0)) elif edge == SurfaceEdge.v0: return np.array(rational_bezier_surf_d2sdv2_iso_v(P, self.weights, n_points, 0.0)) if perp else np.array( rational_bezier_surf_d2sdu2_iso_v(P, self.weights, n_points, 0.0)) elif edge == SurfaceEdge.u1: return np.array(rational_bezier_surf_d2sdu2_iso_u(P, self.weights, 1.0, n_points)) if perp else np.array( rational_bezier_surf_d2sdv2_iso_u(P, self.weights, 1.0, n_points)) elif edge == SurfaceEdge.u0: return np.array(rational_bezier_surf_d2sdu2_iso_u(P, self.weights, 0.0, n_points)) if perp else np.array( rational_bezier_surf_d2sdv2_iso_u(P, self.weights, 0.0, n_points)) else: raise ValueError(f"No edge called {edge}")
[docs] def get_second_deriv_cp_sens_along_edge(self, edge: SurfaceEdge, i: int, j: int, n_points: int = 10, perp: bool = True) -> np.ndarray: r""" Gets the sensitivity of the second :math:`u`- or :math:`v`-derivative along an edge with respect to control point :math:`\mathbf{P}_{i,j}` Parameters ---------- edge: SurfaceEdge Edge along which to evaluate i: int :math:`i`-index of the control point j: int :math:`j`-index of the control point n_points: int Number of evenly-spaced parameter locations at which to evaluate the derivative. Default: 10 perp: bool Whether to evaluate the cross-derivative. If ``False``, the derivative along the parameter direction parallel to the edge will be evaluated instead. Default: ``True`` Returns ------- numpy.ndarray 2-D array of size :math:`n_\text{points} \times 3` """ w = self.weights if edge == SurfaceEdge.v1: return np.array( rational_bezier_surf_d2sdv2_dp_iso_v(w, i, j, self.n, self.m, 3, n_points, 1.0)) if perp else np.array( rational_bezier_surf_d2sdu2_dp_iso_v(w, i, j, self.n, self.m, 3, n_points, 1.0)) elif edge == SurfaceEdge.v0: return np.array( rational_bezier_surf_d2sdv2_dp_iso_v(w, i, j, self.n, self.m, 3, n_points, 0.0)) if perp else np.array( rational_bezier_surf_d2sdu2_dp_iso_v(w, i, j, self.n, self.m, 3, n_points, 0.0)) elif edge == SurfaceEdge.u1: return np.array( rational_bezier_surf_d2sdu2_dp_iso_u(w, i, j, self.n, self.m, 3, 1.0, n_points)) if perp else np.array( rational_bezier_surf_d2sdv2_dp_iso_u(w, i, j, self.n, self.m, 3, 1.0, n_points)) elif edge == SurfaceEdge.u0: return np.array( rational_bezier_surf_d2sdu2_dp_iso_u(w, i, j, self.n, self.m, 3, 0.0, n_points)) if perp else np.array( rational_bezier_surf_d2sdv2_dp_iso_u(w, i, j, self.n, self.m, 3, 0.0, n_points)) else: raise ValueError(f"No edge called {edge}")
[docs] def verify_g0(self, other: 'RationalBezierSurface', surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge, n_points: int = 10): """ Verifies that two RationalBezierSurfaces are G0 continuous along their shared edge""" self_edge = self.get_edge(surface_edge, n_points=n_points) other_edge = other.get_edge(other_surface_edge, n_points=n_points) assert np.array_equal(self_edge, other_edge)
[docs] def verify_g1(self, other: "RationalBezierSurface", surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge, n_points: int = 10): """ Verifies that two RationalBezierSurfaces are G1 continuous along their shared edge """ # Get the first derivatives at the boundary and perpendicular to the boundary for each surface, # evaluated at "n_points" locations along the boundary self_perp_edge_derivs = self.get_first_derivs_along_edge(surface_edge, n_points=n_points, perp=True) other_perp_edge_derivs = other.get_first_derivs_along_edge(other_surface_edge, n_points=n_points, perp=True) self_perp_edge_derivs[np.absolute(self_perp_edge_derivs) < 1e-6] = 0.0 other_perp_edge_derivs[np.absolute(other_perp_edge_derivs) < 1e-6] = 0.0 # Initialize an array of ratios of magnitude of the derivative values at each point for both sides # of the boundary magnitude_ratios = [] # Loop over each pair of cross-derivatives evaluated along the boundary for point_idx, (self_perp_edge_deriv, other_perp_edge_deriv) in enumerate(zip( self_perp_edge_derivs, other_perp_edge_derivs)): # Ensure that each derivative vector has the same direction along the boundary for each surface try: assert np.allclose( np.nan_to_num(self_perp_edge_deriv / np.linalg.norm(self_perp_edge_deriv)), np.nan_to_num(other_perp_edge_deriv / np.linalg.norm(other_perp_edge_deriv)) ) except AssertionError: assert np.allclose( np.nan_to_num(self_perp_edge_deriv / np.linalg.norm(self_perp_edge_deriv)), np.nan_to_num(-other_perp_edge_deriv / np.linalg.norm(other_perp_edge_deriv)) ) # Compute the ratio of the magnitudes for each derivative vector along the boundary for each surface. # These will be compared at the end. #print(f"{self_perp_edge_deriv=},{other_perp_edge_deriv=}") np.seterr(divide='ignore', invalid='ignore') with np.errstate(divide="ignore"): magnitude_ratios.append(np.nan_to_num(self_perp_edge_deriv / other_perp_edge_deriv, nan=0)) #print("Rational",f"{magnitude_ratios=}") # Assert that the first derivatives along each boundary are proportional current_f = None for magnitude_ratio in magnitude_ratios: for dxdydz_ratio in magnitude_ratio: if np.any(np.isinf(dxdydz_ratio)) or np.any(np.isnan(dxdydz_ratio)) or np.any(dxdydz_ratio == 0.0): continue if current_f is None: current_f = dxdydz_ratio continue assert np.all(np.isclose(dxdydz_ratio, current_f))
[docs] def verify_g2(self, other: "RationalBezierSurface", surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge, n_points: int = 10): """ Verifies that two RationalBezierSurfaces are G2 continuous along their shared edge """ # Get the first derivatives at the boundary and perpendicular to the boundary for each surface, # evaluated at "n_points" locations along the boundary self_perp_edge_derivs = self.get_second_derivs_along_edge(surface_edge, n_points=n_points, perp=True) other_perp_edge_derivs = other.get_second_derivs_along_edge(other_surface_edge, n_points=n_points, perp=True) #print(f"{self_perp_edge_derivs=},{other_perp_edge_derivs=}") self_perp_edge_derivs[np.absolute(self_perp_edge_derivs) < 1e-6] = 0.0 other_perp_edge_derivs[np.absolute(other_perp_edge_derivs) < 1e-6] = 0.0 ratios_other_self = other_perp_edge_derivs / self_perp_edge_derivs #print(f"{ratios_other_self=}") #print(f"{self_perp_edge_derivs=},{other_perp_edge_derivs=}") # Initialize an array of ratios of magnitude of the derivative values at each point for both sides # of the boundary magnitude_ratios = [] # Loop over each pair of cross-derivatives evaluated along the boundary for point_idx, (self_perp_edge_deriv, other_perp_edge_deriv) in enumerate(zip( self_perp_edge_derivs, other_perp_edge_derivs)): # Ensure that each derivative vector has the same direction along the boundary for each surface assert np.allclose( np.nan_to_num(self_perp_edge_deriv / np.linalg.norm(self_perp_edge_deriv)), np.nan_to_num(other_perp_edge_deriv / np.linalg.norm(other_perp_edge_deriv)) ) # Compute the ratio of the magnitudes for each derivative vector along the boundary for each surface. # These will be compared at the end. with np.errstate(divide="ignore"): magnitude_ratios.append(self_perp_edge_deriv / other_perp_edge_deriv) # Assert that the second derivatives along each boundary are proportional current_f = None for magnitude_ratio in magnitude_ratios: for dxdydz_ratio in magnitude_ratio: if np.any(np.isinf(dxdydz_ratio)) or np.any(np.isnan(dxdydz_ratio)) or np.any(dxdydz_ratio == 0.0): continue if current_f is None: current_f = dxdydz_ratio continue assert np.all(np.isclose(dxdydz_ratio, current_f))
[docs] def get_u_or_v_given_uvxyz(self, u: float = None, v: float = None, uv_guess: float = 0.5, x: Length = None, y: Length = None, z: Length = None): """ Computes one parametric value given the other and a specified :math:`x`-, :math:`y`-, or :math:`z`-location. As an example, given a :obj:`~aerocaps.geom.surfaces.RationalBezierSurface` object assigned to the variable ``surf``, the :math:`u`-parameter corresponding to :math:`y=1.4` along the :math:`v=0.8` isoparametric curve can be computed using .. code-block:: python u = surf.get_u_or_v_given_uvxyz(v=0.8, y=1.4) Note that the inputs are keyword arguments to avoid having to specify ``None`` for each of the arguments not used. Parameters ---------- u: float or None Value of :math:`u` to solve for or specify. If left as ``None``, this parameter will be solved for. If ``None``, :math:`v` must be specified. Default: ``None`` v: float or None Value of :math:`v` to solve for or specify. If left as ``None``, this parameter will be solved for. If ``None``, :math:`u` must be specified. Default: ``None`` uv_guess: float Starting guess for the unsolved :math:`u` or :math:`v` parameter. Default: ``0.5`` x: Length or None :math:`x`-location corresponding to the :math:`u` or :math:`v` parameter to be solved. If this value is outside the surface geometry, the root-finder will fail and an error will be raised. If unspecified, either :math:`y` or :math:`z` must be specified. Default: ``None`` y: Length or None :math:`y`-location corresponding to the :math:`u` or :math:`v` parameter to be solved. If this value is outside the surface geometry, the root-finder will fail and an error will be raised. If unspecified, either :math:`x` or :math:`z` must be specified. Default: ``None`` z: Length or None :math:`z`-location corresponding to the :math:`u` or :math:`v` parameter to be solved. If this value is outside the surface geometry, the root-finder will fail and an error will be raised. If unspecified, either :math:`x` or :math:`y` must be specified. Default: ``None`` Returns ------- float The value of :math:`u` if :math:`v` is specified or :math:`v` if :math:`u` is specified """ # Validate inputs if u is None and v is None or (u is not None and v is not None): raise ValueError("Must specify exactly one of either u or v") xyz_spec = (x is not None, y is not None, z is not None) if len([xyz for xyz in xyz_spec if xyz]) != 1: raise ValueError("Must specify exactly one of x, y, or z") if x is not None: xyz, xyz_val = "x", x.m elif y is not None: xyz, xyz_val = "y", y.m elif z is not None: xyz, xyz_val = "z", z.m else: raise ValueError("Did not detect an x, y, or z input") def root_find_func_u(u_current): point = self.evaluate_point3d(u_current, v) return np.array([getattr(point, xyz).m - xyz_val]) def root_find_func_v(v_current): point = self.evaluate_point3d(u, v_current) return np.array([getattr(point, xyz).m - xyz_val]) if v is not None: return fsolve(root_find_func_u, x0=np.array([uv_guess]))[0] if u is not None: return fsolve(root_find_func_v, x0=np.array([uv_guess]))[0] raise ValueError("Did not detect a u or v input")
[docs] def split_at_u(self, u0: float) -> ("RationalBezierSurface", "RationalBezierSurface"): """ Splits the rational Bezier surface at :math:`u=u_0` along the :math:`v`-parametric direction. """ Pw = self.get_homogeneous_control_points() def de_casteljau(i: int, j: int, k: int) -> np.ndarray: """ Based on https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm. Recursive algorithm where the base case is just the value of the ith original control point. Parameters ---------- i: int Lower index j: int Upper index k: int Control point row index Returns ------- np.ndarray A one-dimensional array containing the :math:`x` and :math:`y` values of a control point evaluated at :math:`(i,j)` for a Bézier curve split at the parameter value ``t_split`` """ if j == 0: return Pw[i, k, :] return de_casteljau(i, j - 1, k) * (1 - u0) + de_casteljau(i + 1, j - 1, k) * u0 bez_surf_split_1_Pw = np.array([ [de_casteljau(i=0, j=i, k=k) for i in range(self.n_points_u)] for k in range(self.n_points_v) ]) bez_surf_split_2_Pw = np.array([ [de_casteljau(i=i, j=self.degree_u - i, k=k) for i in range(self.n_points_u)] for k in range(self.n_points_v) ]) transposed_Pw_1 = np.transpose(bez_surf_split_1_Pw, (1, 0, 2)) transposed_Pw_2 = np.transpose(bez_surf_split_2_Pw, (1, 0, 2)) P1, w1 = self.project_homogeneous_control_points(transposed_Pw_1) P2, w2 = self.project_homogeneous_control_points(transposed_Pw_2) return ( RationalBezierSurface(P1, w1), RationalBezierSurface(P2, w2) )
[docs] def split_at_v(self, v0: float) -> ("BezierSurface", "BezierSurface"): """ Splits the rational Bezier surface at :math:`v=v_0` along the :math:`u`-parametric direction. """ Pw = self.get_homogeneous_control_points() def de_casteljau(i: int, j: int, k: int) -> np.ndarray: """ Based on https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm. Recursive algorithm where the base case is just the value of the ith original control point. Parameters ---------- i: int Lower index j: int Upper index k: int Control point row index Returns ------- np.ndarray A one-dimensional array containing the :math:`x` and :math:`y` values of a control point evaluated at :math:`(i,j)` for a Bézier curve split at the parameter value ``t_split`` """ if j == 0: return Pw[k, i, :] return de_casteljau(i, j - 1, k) * (1 - v0) + de_casteljau(i + 1, j - 1, k) * v0 bez_surf_split_1_Pw = np.array([ [de_casteljau(i=0, j=i, k=k) for i in range(self.n_points_v)] for k in range(self.n_points_u) ]) bez_surf_split_2_Pw = np.array([ [de_casteljau(i=i, j=self.degree_v - i, k=k) for i in range(self.n_points_v)] for k in range(self.n_points_u) ]) P1, w1 = self.project_homogeneous_control_points(bez_surf_split_1_Pw) P2, w2 = self.project_homogeneous_control_points(bez_surf_split_2_Pw) return ( RationalBezierSurface(P1, w1), RationalBezierSurface(P2, w2) )
[docs] def transform(self, **transformation_kwargs) -> "RationalBezierSurface": """ Creates a transformed copy of the surface by transforming each of the control points Parameters ---------- transformation_kwargs Keyword arguments passed to :obj:`~aerocaps.geom.transformation.Transformation3D` Returns ------- RationalBezierSurface Transformed surface """ transformation = Transformation3D(**transformation_kwargs) initial_control_points = self.get_control_point_array() return RationalBezierSurface( np.array([transformation.transform(p_arr) for p_arr in initial_control_points]), weights=deepcopy(self.weights), name=self.name, construction=self.construction )
[docs] def generate_control_point_net(self) -> (typing.List[Point3D], typing.List[Line3D]): """ Generates a list of :obj:`~aerocaps.geom.point.Point3D` and :obj:`~aerocaps.geom.curves.Line3D` objects representing the rational Bézier surface's control points and connections between them Returns ------- typing.List[Point3D], typing.List[Line3D] Control points and lines between adjacent control points in flattened lists """ control_points = self.get_control_point_array() points = [] lines = [] for i in range(self.n_points_u): for j in range(self.n_points_v): points.append(Point3D.from_array(control_points[i, j, :])) for i in range(self.n_points_u - 1): for j in range(self.n_points_v - 1): point_obj_1 = Point3D.from_array(control_points[i, j, :]) point_obj_2 = Point3D.from_array(control_points[i + 1, j, :]) point_obj_3 = Point3D.from_array(control_points[i, j + 1, :]) line_1 = Line3D(p0=point_obj_1, p1=point_obj_2) line_2 = Line3D(p0=point_obj_1, p1=point_obj_3) lines.extend([line_1, line_2]) if i < self.n_points_u - 2 and j < self.n_points_v - 2: continue point_obj_4 = Point3D.from_array(control_points[i + 1, j + 1, :]) line_3 = Line3D(p0=point_obj_3, p1=point_obj_4) line_4 = Line3D(p0=point_obj_2, p1=point_obj_4) lines.extend([line_3, line_4]) return points, lines
[docs] def plot_surface(self, plot: pv.Plotter, Nu: int = 50, Nv: int = 50, **mesh_kwargs): """ Plots the rational Bézier surface using the `pyvista <https://pyvista.org/>`_ library Parameters ---------- plot: :obj:`pyvista.Plotter` instance Nu: int Number of points to evaluate in the :math:`u`-parametric direction. Default: ``50`` Nv: int Number of points to evaluate in the :math:`v`-parametric direction. Default: ``50`` mesh_kwargs: Keyword arguments to pass to :obj:`pyvista.Plotter.add_mesh` Returns ------- pyvista.core.pointset.StructuredGrid The evaluated rational Bézier surface """ XYZ = self.evaluate_grid(Nu, Nv) grid = pv.StructuredGrid(XYZ[:, :, 0], XYZ[:, :, 1], XYZ[:, :, 2]) plot.add_mesh(grid, **mesh_kwargs) return grid
[docs] def plot_control_point_mesh_lines(self, plot: pv.Plotter, **line_kwargs) -> pv.Actor: """ Plots the network of lines connecting the rational Bézier surface control points using the `pyvista <https://pyvista.org/>`_ library Parameters ---------- plot: :obj:`pyvista.Plotter` instance line_kwargs: Keyword arguments to pass to the :obj:`pyvista.Plotter.add_lines` Returns ------- pv.Actor The lines actor """ _, line_objs = self.generate_control_point_net() line_arr = np.array([[line_obj.p0.as_array(), line_obj.p1.as_array()] for line_obj in line_objs]) line_arr = line_arr.reshape((len(line_objs) * 2, 3)) line_actor = plot.add_lines(line_arr, **line_kwargs) return line_actor
[docs] def plot_control_points(self, plot: pv.Plotter, **point_kwargs) -> pv.Actor: """ Plots the rational Bézier surface control points using the `pyvista <https://pyvista.org/>`_ library Parameters ---------- plot: :obj:`pyvista.Plotter` instance point_kwargs: Keyword arguments to pass to the :obj:`pyvista.Plotter.add_points` Returns ------- pv.Actor The points actor """ point_objs, _ = self.generate_control_point_net() point_arr = np.array([point_obj.as_array() for point_obj in point_objs]) point_actor = plot.add_points(point_arr, **point_kwargs) return point_actor
def __repr__(self): return (f"{self.name}: {self.degree_u} x {self.degree_v} {self.__class__.__name__} " f"({self.degree_u + 1} x {self.degree_v + 1} control points)")
[docs] class BSplineSurface(Surface): """ B-spline surface class """
[docs] def __init__(self, points: typing.List[typing.List[Point3D]] or np.ndarray, knots_u: np.ndarray, knots_v: np.ndarray, name: str = "BSplineSurface", construction: bool = False ): """ Parameters ---------- points knots_u knots_v name: str Name of the geometric object. May be re-assigned a unique name when added to a :obj:`~aerocaps.geom.geometry_container.GeometryContainer`. Default: 'BSplineSurface' construction: bool Whether this is a geometry used only for construction of other geometries. If ``True``, this geometry will not be exported or plotted. Default: ``False`` """ if isinstance(points, np.ndarray): points = [[Point3D.from_array(pt_row) for pt_row in pt_mat] for pt_mat in points] self.points = points assert knots_u.ndim == 1 assert knots_v.ndim == 1 self.knots_u = deepcopy(knots_u) self.knots_v = deepcopy(knots_v) self._weights = np.ones((len(points), len(points[0]))) super().__init__(name=name, construction=construction)
@property def n_points_u(self) -> int: """Number of control points in the :math:`u`-parametric direction""" return len(self.points) @property def n_points_v(self) -> int: """Number of control points in the :math:`v`-parametric direction""" return len(self.points[0]) @property def degree_u(self) -> int: """Surface degree in the :math:`u`-parametric direction""" return len(self.knots_u) - self.n_points_u - 1 @property def degree_v(self) -> int: """Surface degree in the :math:`v`-parametric direction""" return len(self.knots_v) - self.n_points_v - 1 @property def n(self) -> int: """ Shorthand for :obj:`~aerocaps.geom.surfaces.BSplineSurface.degree_u` Returns ------- int Surface degree in the :math:`u`-parametric direction """ return self.degree_u @property def m(self) -> int: """ Shorthand for :obj:`~aerocaps.geom.surfaces.BSplineSurface.degree_v` Returns ------- int Surface degree in the :math:`v`-parametric direction """ return self.degree_v @property def weights(self) -> np.ndarray: """Weight matrix (all ones for this surface type)""" return self._weights
[docs] def to_iges(self, *args, **kwargs) -> aerocaps.iges.entity.IGESEntity: """ Exports the NURBS surface to an IGES entity """ return aerocaps.iges.surfaces.RationalBSplineSurfaceIGES( control_points=self.get_control_point_array(), knots_u=self.knots_u, knots_v=self.knots_v, weights=self.weights, degree_u=self.degree_u, degree_v=self.degree_v )
[docs] def get_control_point_array(self) -> np.ndarray: r""" Gets the control points in float array form. Returns ------- numpy.ndarray Array of size :math:`N_u \times N_v \times 3` """ return np.array([np.array([p.as_array() for p in p_arr]) for p_arr in self.points])
[docs] def evaluate(self, u: float, v: float) -> np.ndarray: r""" Evaluates the surface at a given :math:`(u,v)` parameter pair. Parameters ---------- u: float Position along :math:`u` in parametric space. Normally in the range :math:`[0,1]` v: float Position along :math:`v` in parametric space. Normally in the range :math:`[0,1]` Returns ------- numpy.ndarray 1-D array of the form ``array([x, y, z])`` representing the evaluated point on the surface """ P = self.get_control_point_array() return np.array(bspline_surf_eval(P, self.knots_u, self.knots_v, u, v))
[docs] def evaluate_point3d(self, u: float, v: float) -> Point3D: r""" Evaluates the B-spline surface at a single :math:`(u,v)` parameter pair and returns a point object. Parameters ---------- u: float Position along :math:`u` in parametric space. Normally in the range :math:`[0,1]` v: float Position along :math:`v` in parametric space. Normally in the range :math:`[0,1]` Returns ------- Point3D Point object corresponding to the :math:`(u,v)` pair """ return Point3D.from_array(self.evaluate(u, v))
[docs] def evaluate_grid(self, Nu: int, Nv: int) -> np.ndarray: r""" Evaluates the B-spline surface on a uniform :math:`N_u \times N_v` grid of parameter values. Parameters ---------- Nu: int Number of uniformly spaced parameter values in the :math:`u`-direction Nv: int Number of uniformly spaced parameter values in the :math:`v`-direction Returns ------- numpy.ndarray Array of size :math:`N_u \times N_v \times 3` """ P = self.get_control_point_array() return np.array(bspline_surf_eval_grid(P, self.knots_u, self.knots_v, Nu, Nv))
[docs] def get_parallel_control_point_length(self, surface_edge: SurfaceEdge) -> int: r""" Gets the number of control points of the curve corresponding to the input surface edge. Parameters ---------- surface_edge: SurfaceEdge Edge along which the number of control points is computed Returns ------- int Number of control points parallel to the edge """ if surface_edge in [SurfaceEdge.v1, SurfaceEdge.v0]: return self.n_points_u return self.n_points_v
[docs] def get_perpendicular_control_point_length(self, surface_edge: SurfaceEdge) -> int: r""" Gets the number of control points in the parametric direction perpendicular to the input surface edge. Parameters ---------- surface_edge: SurfaceEdge Edge along which the number of perpendicular control points is computed Returns ------- int Number of control points perpendicular to the edge """ if surface_edge in [SurfaceEdge.v1, SurfaceEdge.v0]: return self.n_points_v return self.n_points_u
[docs] def get_parallel_degree(self, surface_edge: SurfaceEdge) -> int: r""" Gets the degree of the curve corresponding to the input surface edge. Parameters ---------- surface_edge: SurfaceEdge Edge along which the parallel degree is evaluated Returns ------- int Degree parallel to the edge """ if surface_edge in [SurfaceEdge.v1, SurfaceEdge.v0]: return self.degree_u return self.degree_v
[docs] def get_perpendicular_degree(self, surface_edge: SurfaceEdge) -> int: r""" Gets the degree of the curve in the parametric direction perpendicular to the input surface edge. Parameters ---------- surface_edge: SurfaceEdge Edge along which the perpendicular degree is evaluated Returns ------- int Degree perpendicular to the edge """ if surface_edge in [SurfaceEdge.v1, SurfaceEdge.v0]: return self.degree_v return self.degree_u
[docs] def get_parallel_knots(self, surface_edge: SurfaceEdge) -> np.ndarray: r""" Gets the knots in the parametric direction parallel to the input surface edge. Parameters ---------- surface_edge: SurfaceEdge Edge along which the parallel knots are returned Returns ------- numpy.ndarray Knots parallel to the edge """ if surface_edge in [SurfaceEdge.v1, SurfaceEdge.v0]: return self.knots_u return self.knots_v
[docs] def get_perpendicular_knots(self, surface_edge: SurfaceEdge) -> np.ndarray: r""" Gets the knots in the parametric direction perpendicular to the input surface edge. Parameters ---------- surface_edge: SurfaceEdge Edge along which the perpendicular knots are returned Returns ------- numpy.ndarray Knots perpendicular to the edge """ if surface_edge in [SurfaceEdge.v1, SurfaceEdge.v0]: return self.knots_v return self.knots_u
[docs] def get_point(self, row_index: int, continuity_index: int, surface_edge: SurfaceEdge) -> Point3D: r""" Gets the point corresponding to a particular index along the edge curve with perpendicular index corresponding to the level of continuity being applied. For example, for a :math:`6 \times 5` B-spline surface, the following code .. code-block:: python p = surf.get_point(2, 1, ac.SurfaceEdge.v0) returns the point :math:`\mathbf{P}_{2,1}` and .. code-block:: python p = surf.get_point(2, 1, ac.SurfaceEdge.u1) returns the point :math:`\mathbf{P}_{6-1,2} = \mathbf{P}_{5,2}` if there are no internal knot vectors. If the B-spline surface has internal knot vectors, the actual :math:`i`-index of the point may be different, but the second-to-last point in the third row of control points will still be returned. .. seealso:: :obj:`~aerocaps.geom.surfaces.BSplineSurface.set_point` Setter equivalent of this method Parameters ---------- row_index: int Index along the surface edge control points continuity_index: int Index in the parametric direction perpendicular to the surface edge. Normally either ``0``, ``1``, or ``2`` surface_edge: SurfaceEdge Edge of the surface along which to retrieve the control point Returns ------- Point3D Point used to enforce :math:`G^x` continuity, where :math:`x` is the value of ``continuity_index`` """ if surface_edge == SurfaceEdge.v1: return self.points[row_index][-(continuity_index + 1)] elif surface_edge == SurfaceEdge.v0: return self.points[row_index][continuity_index] elif surface_edge == SurfaceEdge.u1: return self.points[-(continuity_index + 1)][row_index] elif surface_edge == SurfaceEdge.u0: return self.points[continuity_index][row_index] else: raise ValueError("Invalid surface_edge value")
[docs] def set_point(self, point: Point3D, row_index: int, continuity_index: int, surface_edge: SurfaceEdge): r""" Sets the point corresponding to a particular index along the edge curve with perpendicular index corresponding to the level of continuity being applied. For example, for a :math:`6 \times 5` B-spline surface, the following code .. code-block:: python p = ac.Point3D.from_array(np.array([3.0, 4.0, 5.0])) surf.set_point(p, 2, 1, ac.SurfaceEdge.v0) sets the value of point :math:`\mathbf{P}_{2,1}` to :math:`[3,4,5]^T` and .. code-block:: python p = ac.Point3D.from_array(np.array([3.0, 4.0, 5.0])) surf.get_point(p, 2, 1, ac.SurfaceEdge.u1) sets the value of point :math:`\mathbf{P}_{6-1,2} = \mathbf{P}_{5,2}` to :math:`[3,4,5]^T` if there are no internal knot vectors. .. seealso:: :obj:`~aerocaps.geom.surfaces.BSplineSurface.get_point` Getter equivalent of this method Parameters ---------- point: Point3D Point object to apply at the specified indices row_index: int Index along the surface edge control points continuity_index: int Index in the parametric direction perpendicular to the surface edge. Normally either ``0``, ``1``, or ``2`` surface_edge: SurfaceEdge Edge of the surface along which to retrieve the control point """ if surface_edge == SurfaceEdge.v1: self.points[row_index][-(continuity_index + 1)].x.m = point.x.m self.points[row_index][-(continuity_index + 1)].y.m = point.y.m self.points[row_index][-(continuity_index + 1)].z.m = point.z.m elif surface_edge == SurfaceEdge.v0: self.points[row_index][continuity_index].x.m = point.x.m self.points[row_index][continuity_index].y.m = point.y.m self.points[row_index][continuity_index].z.m = point.z.m elif surface_edge == SurfaceEdge.u1: self.points[-(continuity_index + 1)][row_index].x.m = point.x.m self.points[-(continuity_index + 1)][row_index].y.m = point.y.m self.points[-(continuity_index + 1)][row_index].z.m = point.z.m elif surface_edge == SurfaceEdge.u0: self.points[continuity_index][row_index].x.m = point.x.m self.points[continuity_index][row_index].y.m = point.y.m self.points[continuity_index][row_index].z.m = point.z.m else: raise ValueError("Invalid surface_edge value")
[docs] def extract_edge_curve(self, surface_edge: SurfaceEdge) -> BSplineCurve3D: """ Extracts the control points, weights, and knots from one of the four edges of the B-spline surface and outputs a B-spline curve with these control points and weights Parameters ---------- surface_edge: SurfaceEdge Edge along which to extract the curve Returns ------- BSplineCurve3D B-spline curve with control points and knots corresponding to the control points and knots along the edge of the surface """ P = self.get_control_point_array() if surface_edge == SurfaceEdge.u0: return BSplineCurve3D(P[0, :, :], self.knots_v, self.degree_v) if surface_edge == SurfaceEdge.u1: return BSplineCurve3D(P[-1, :, :], self.knots_v, self.degree_v) if surface_edge == SurfaceEdge.v0: return BSplineCurve3D(P[:, 0, :], self.knots_u, self.degree_u) if surface_edge == SurfaceEdge.v1: return BSplineCurve3D(P[:, -1, :], self.knots_u, self.degree_u) raise ValueError(f"Invalid surface edge {surface_edge}")
[docs] def enforce_g0(self, other: "BSplineSurface", surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge): r""" Enforces :math:`G^0` continuity along the input ``surface_edge`` by equating the control points along this edge to the corresponding control points and weights along the ``other_surface_edge`` of the B-spline surface given by ``other``. The control points of the surface from which this method is called are modified in-place, and the control points of ``other`` are left unchanged. .. important:: The parallel degree of the current surface along ``surface_edge`` must be equal to the parallel degree of the ``other`` surface along ``other_surface_edge``, otherwise an ``AssertionError`` will be raised. Additionally, the knot vector along the ``surface_edge`` of the current surface must be equal to the knot vector along the ``other_surface_edge`` of the other surface. .. seealso:: :obj:`~aerocaps.geom.surfaces.BSplineSurface.enforce_c0` Parametric continuity equivalent (:math:`C^0`) Parameters ---------- other: BSplineSurface Another B-spline surface along which an edge will be used for stitching surface_edge: SurfaceEdge The edge of the current surface to modify other_surface_edge: SurfaceEdge Tool edge of surface ``other`` which determines the positions of control points along ``surface_edge`` of the current surface """ # P^b[:, 0] = P^a[:, -1] self_parallel_knots = self.get_parallel_knots(surface_edge) other_parallel_knots = other.get_parallel_knots(other_surface_edge) self_parallel_degree = self.get_parallel_degree(surface_edge) other_parallel_degree = other.get_parallel_degree(other_surface_edge) if len(self_parallel_knots) != len(other_parallel_knots): raise ValueError(f"Length of the knot vector parallel to the edge of the input surface " f"({len(self_parallel_knots)}) is not equal to the length of the knot vector " f"parallel to the edge of the other surface ({len(other_parallel_knots)})") if any([k1 != k2 for k1, k2 in zip(self_parallel_knots, other_parallel_knots)]): raise ValueError(f"Knots parallel to the edge of the input surface ({self_parallel_knots}) " f"are not equal to the knots in the direction parallel to the edge of the other " f"surface ({other_parallel_knots})") if self_parallel_degree != other_parallel_degree: raise ValueError(f"Degree parallel to the edge of the input surface ({self_parallel_degree}) does " f"not match the degree parallel to the edge of the other surface " f"({other_parallel_degree})") for row_index in range(self.get_parallel_control_point_length(surface_edge)): self.set_point(other.get_point(row_index, 0, other_surface_edge), row_index, 0, surface_edge)
[docs] def enforce_c0(self, other: "BSplineSurface", surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge): """ For zeroth-degree continuity, there is no difference between geometric (:math:`G^0`) and parametric (:math:`C^0`) continuity. Because this method is simply a convenience method that calls :obj:`~aerocaps.geom.surfaces.BSplineSurface.enforce_g0`, see the documentation for that method for more detailed documentation. Parameters ---------- other: BSplineSurface Another B-spline surface along which an edge will be used for stitching surface_edge: SurfaceEdge The edge of the current surface to modify other_surface_edge: SurfaceEdge Tool edge of surface ``other`` which determines the positions of control points along ``surface_edge`` of the current surface """ self.enforce_g0(other, surface_edge, other_surface_edge)
[docs] def enforce_g0g1(self, other: "BSplineSurface", f: float, surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge): r""" First enforces :math:`G^0` continuity, then tangent (:math:`G^1`) continuity is enforced according to the following equation: .. math:: \mathcal{P}^{b,\mathcal{E}_b}_{k,1} = \mathcal{P}^{b,\mathcal{E}_b}_{k,0} + f \frac{p_{\perp}^{a,\mathcal{E}_a}}{p_{\perp}^{b,\mathcal{E}_b}} \left[\mathcal{P}^{a,\mathcal{E}_a}_{k,0} - \mathcal{P}^{a,\mathcal{E}_a}_{k,1} \right] \text{ for }k=0,1,\ldots,p_{\parallel}^{b,\mathcal{E}_b} Here, :math:`b` corresponds to the current surface, and :math:`a` corresponds to the ``other`` surface. The control points of the surface from which this method is called are modified in-place, and the control points of ``other`` are left unchanged. .. seealso:: :obj:`~aerocaps.geom.surfaces.BSplineSurface.enforce_g0` Geometric point continuity enforcement (:math:`G^0`) :obj:`~aerocaps.geom.surfaces.BSplineSurface.enforce_c0c1` Parametric continuity equivalent (:math:`C^1`) Parameters ---------- other: BSplineSurface Another B-spline surface along which an edge will be used for stitching f: float Tangent proportionality factor surface_edge: SurfaceEdge The edge of the current surface to modify other_surface_edge: SurfaceEdge Tool edge of surface ``other`` which determines the positions of control points along ``surface_edge`` of the current surface """ self.enforce_g0(other, surface_edge, other_surface_edge) n_ratio = other.get_perpendicular_degree(other_surface_edge) / self.get_perpendicular_degree(surface_edge) for row_index in range(self.get_parallel_control_point_length(surface_edge)): P_i0_b = self.get_point(row_index, 0, surface_edge) P_im_a = other.get_point(row_index, 0, other_surface_edge) P_im1_a = other.get_point(row_index, 1, other_surface_edge) P_i1_b = P_i0_b + f * n_ratio * (P_im_a - P_im1_a) self.set_point(P_i1_b, row_index, 1, surface_edge)
[docs] def enforce_c0c1(self, other: "BSplineSurface", surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge): r""" Equivalent to calling :obj:`~aerocaps.geom.surfaces.BSplineSurface.enforce_g0g1` with ``f=1.0``. See that method for more detailed documentation. Parameters ---------- other: BSplineSurface Another B-spline surface along which an edge will be used for stitching surface_edge: SurfaceEdge The edge of the current surface to modify other_surface_edge: SurfaceEdge Tool edge of surface ``other`` which determines the positions of control points along ``surface_edge`` of the current surface """ self.enforce_g0g1(other, 1.0, surface_edge, other_surface_edge)
[docs] def enforce_g0g1g2(self, other: "BSplineSurface", f: float, surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge): r""" First enforces :math:`G^0` and :math:`G^1` continuity, then curvature (:math:`G^2`) continuity is enforced according to the following equation: .. math:: \mathcal{P}^{b,\mathcal{E}_b}_{k,2} = 2 \mathcal{P}^{b,\mathcal{E}_b}_{k,1} - \mathcal{P}^{b,\mathcal{E}_b}_{k,0} + f^2 \frac{p_{\perp}^{a,\mathcal{E}_a}(p_{\perp}^{a,\mathcal{E}_a}-1)}{p_{\perp}^{b,\mathcal{E}_b}(p_{\perp}^{b,\mathcal{E}_b}-1)} \left[ \mathcal{P}^{a,\mathcal{E}_a}_{k,0} - 2 \mathcal{P}^{a,\mathcal{E}_a}_{k,1} + \mathcal{P}^{a,\mathcal{E}_a}_{k,2} \right] \text{ for }k=0,1,\ldots,p_{\parallel}^{b,\mathcal{E}_b} Here, :math:`b` corresponds to the current surface, and :math:`a` corresponds to the ``other`` surface. The control points of the surface from which this method is called are modified in-place, and the control points of ``other`` are left unchanged. .. seealso:: :obj:`~aerocaps.geom.surfaces.BSplineSurface.enforce_g0` Geometric point continuity enforcement (:math:`G^0`) :obj:`~aerocaps.geom.surfaces.BSplineSurface.enforce_g0g1` Geometric tangent continuity enforcement (:math:`G^1`) :obj:`~aerocaps.geom.surfaces.BSplineSurface.enforce_c0c1c2` Parametric continuity equivalent (:math:`C^2`) Parameters ---------- other: BSplineSurface Another B-spline surface along which an edge will be used for stitching f: float Tangent proportionality factor surface_edge: SurfaceEdge The edge of the current surface to modify other_surface_edge: SurfaceEdge Tool edge of surface ``other`` which determines the positions of control points along ``surface_edge`` of the current surface """ self.enforce_g0g1(other, f, surface_edge, other_surface_edge) p_perp_a = other.get_perpendicular_degree(other_surface_edge) p_perp_b = self.get_perpendicular_degree(surface_edge) n_ratio = (p_perp_a ** 2 - p_perp_a) / (p_perp_b ** 2 - p_perp_b) for row_index in range(self.get_parallel_control_point_length(surface_edge)): P_i0_b = self.get_point(row_index, 0, surface_edge) P_i1_b = self.get_point(row_index, 1, surface_edge) P_im_a = other.get_point(row_index, 0, other_surface_edge) P_im1_a = other.get_point(row_index, 1, other_surface_edge) P_im2_a = other.get_point(row_index, 2, other_surface_edge) P_i2_b = (2.0 * P_i1_b - P_i0_b) + f ** 2 * n_ratio * (P_im_a - 2.0 * P_im1_a + P_im2_a) self.set_point(P_i2_b, row_index, 2, surface_edge)
[docs] def enforce_c0c1c2(self, other: "BSplineSurface", surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge): r""" Equivalent to calling :obj:`~aerocaps.geom.surfaces.BSplineSurface.enforce_g0g1g2` with ``f=1.0``. See that method for more detailed documentation. Parameters ---------- other: BSplineSurface Another B-spline surface along which an edge will be used for stitching surface_edge: SurfaceEdge The edge of the current surface to modify other_surface_edge: SurfaceEdge Tool edge of surface ``other`` which determines the positions of control points along ``surface_edge`` of the current surface """ self.enforce_g0g1g2(other, 1.0, surface_edge, other_surface_edge)
[docs] def dSdu(self, u: float, v: float) -> np.ndarray: r""" Evaluates the first derivative with respect to :math:`u` at a single :math:`(u,v)` pair Parameters ---------- u: float Position along :math:`u` in parametric space. Normally in the range :math:`[0,1]` v: float Position along :math:`v` in parametric space. Normally in the range :math:`[0,1]` Returns ------- np.ndarray 1-D array containing the :math:`x`-, :math:`y`-, and :math:`z`-components of the second derivative """ P = self.get_control_point_array() return np.array(bspline_surf_dsdu(P, self.knots_u, self.knots_v, u, v))
[docs] def dSdu_grid(self, Nu: int, Nv: int) -> np.ndarray: r""" Evaluates the first derivative with respect to :math:`u` on a linearly-spaced grid of :math:`u`- and :math:`v`-values. Parameters ---------- Nu: int Number of evenly spaced :math:`u` values Nv: int Number of evenly spaced :math:`v` values Returns ------- np.ndarray Array of size :math:`N_u \times N_v \times 3` """ P = self.get_control_point_array() return np.array(bspline_surf_dsdu_grid(P, self.knots_u, self.knots_v, Nu, Nv))
[docs] def dSdu_uvvecs(self, u: np.ndarray, v: np.ndarray): r""" Evaluates the first derivative of the surface with respect to :math:`u` at arbitrary vectors of :math:`u` and :math:`v`-values. Parameters ---------- u: np.ndarray 1-D array of :math:`u`-parameter values v: np.ndarray 1-D array of :math:`v`-parameter values Returns ------- np.ndarray Array of size :math:`\text{len}(u) \times \text{len}(v) \times 3` """ P = self.get_control_point_array() return np.array(bspline_surf_dsdu_uvvecs(P, self.knots_u, self.knots_v, u, v))
[docs] def dSdv(self, u: float or np.ndarray, v: float or np.ndarray): r""" Evaluates the first derivative with respect to :math:`v` at a single :math:`(u,v)` pair Parameters ---------- u: float Position along :math:`u` in parametric space. Normally in the range :math:`[0,1]` v: float Position along :math:`v` in parametric space. Normally in the range :math:`[0,1]` Returns ------- np.ndarray 1-D array containing the :math:`x`-, :math:`y`-, and :math:`z`-components of the second derivative """ P = self.get_control_point_array() return np.array(bspline_surf_dsdv(P, self.knots_u, self.knots_v, u, v))
[docs] def dSdv_grid(self, Nu: int, Nv: int) -> np.ndarray: r""" Evaluates the first derivative with respect to :math:`v` on a linearly-spaced grid of :math:`u`- and :math:`v`-values. Parameters ---------- Nu: int Number of evenly spaced :math:`u` values Nv: int Number of evenly spaced :math:`v` values Returns ------- np.ndarray Array of size :math:`N_u \times N_v \times 3` """ P = self.get_control_point_array() return np.array(bspline_surf_dsdv_grid(P, self.knots_u, self.knots_v, Nu, Nv))
[docs] def dSdv_uvvecs(self, u: np.ndarray, v: np.ndarray): r""" Evaluates the first derivative of the surface with respect to :math:`v` at arbitrary vectors of :math:`u` and :math:`v`-values. Parameters ---------- u: np.ndarray 1-D array of :math:`u`-parameter values v: np.ndarray 1-D array of :math:`v`-parameter values Returns ------- np.ndarray Array of size :math:`\text{len}(u) \times \text{len}(v) \times 3` """ P = self.get_control_point_array() return np.array(bspline_surf_dsdv_uvvecs(P, self.knots_u, self.knots_v, u, v))
[docs] def d2Sdu2(self, u: float, v: float) -> np.ndarray: r""" Evaluates the second derivative with respect to :math:`u` at a single :math:`(u,v)` pair Parameters ---------- u: float Position along :math:`u` in parametric space. Normally in the range :math:`[0,1]` v: float Position along :math:`v` in parametric space. Normally in the range :math:`[0,1]` Returns ------- np.ndarray 1-D array containing the :math:`x`-, :math:`y`-, and :math:`z`-components of the second derivative """ P = self.get_control_point_array() return np.array(bspline_surf_d2sdu2(P, self.knots_u, self.knots_v, u, v))
[docs] def d2Sdu2_grid(self, Nu: int, Nv: int) -> np.ndarray: r""" Evaluates the second derivative with respect to :math:`u` on a linearly-spaced grid of :math:`u`- and :math:`v`-values. Parameters ---------- Nu: int Number of evenly spaced :math:`u` values Nv: int Number of evenly spaced :math:`v` values Returns ------- np.ndarray Array of size :math:`N_u \times N_v \times 3` """ P = self.get_control_point_array() return np.array(bspline_surf_d2sdu2_grid(P, self.knots_u, self.knots_v, Nu, Nv))
[docs] def d2Sdu2_uvvecs(self, u: np.ndarray, v: np.ndarray): r""" Evaluates the second derivative of the surface with respect to :math:`u` at arbitrary vectors of :math:`u` and :math:`v`-values. Parameters ---------- u: np.ndarray 1-D array of :math:`u`-parameter values v: np.ndarray 1-D array of :math:`v`-parameter values Returns ------- np.ndarray Array of size :math:`\text{len}(u) \times \text{len}(v) \times 3` """ P = self.get_control_point_array() return np.array(bspline_surf_d2sdu2_uvvecs(P, self.knots_u, self.knots_v, u, v))
[docs] def d2Sdv2(self, u: float or np.ndarray, v: float or np.ndarray): r""" Evaluates the second derivative with respect to :math:`v` at a single :math:`(u,v)` pair Parameters ---------- u: float Position along :math:`u` in parametric space. Normally in the range :math:`[0,1]` v: float Position along :math:`v` in parametric space. Normally in the range :math:`[0,1]` Returns ------- np.ndarray 1-D array containing the :math:`x`-, :math:`y`-, and :math:`z`-components of the second derivative """ P = self.get_control_point_array() return np.array(bspline_surf_d2sdv2(P, self.knots_u, self.knots_v, u, v))
[docs] def d2Sdv2_grid(self, Nu: int, Nv: int) -> np.ndarray: r""" Evaluates the second derivative with respect to :math:`v` on a linearly-spaced grid of :math:`u`- and :math:`v`-values. Parameters ---------- Nu: int Number of evenly spaced :math:`u` values Nv: int Number of evenly spaced :math:`v` values Returns ------- np.ndarray Array of size :math:`N_u \times N_v \times 3` """ P = self.get_control_point_array() return np.array(bspline_surf_d2sdv2_grid(P, self.knots_u, self.knots_v, Nu, Nv))
[docs] def d2Sdv2_uvvecs(self, u: np.ndarray, v: np.ndarray): r""" Evaluates the second derivative of the surface with respect to :math:`v` at arbitrary vectors of :math:`u` and :math:`v`-values. Parameters ---------- u: np.ndarray 1-D array of :math:`u`-parameter values v: np.ndarray 1-D array of :math:`v`-parameter values Returns ------- np.ndarray Array of size :math:`\text{len}(u) \times \text{len}(v) \times 3` """ P = self.get_control_point_array() return np.array(bspline_surf_d2sdv2_uvvecs(P, self.knots_u, self.knots_v, u, v))
[docs] def get_edge(self, edge: SurfaceEdge, n_points: int = 10) -> np.ndarray: r""" Evaluates the surface at ``n_points`` parameter locations along a given edge. Parameters ---------- edge: SurfaceEdge Edge along which to evaluate n_points: int Number of evenly-spaced parameter locations at which to evaluate the edge curve. Default: 10 Returns ------- numpy.ndarray 2-D array of size :math:`n_\text{points} \times 3` """ P = self.get_control_point_array() if edge == SurfaceEdge.v1: return np.array(bspline_surf_eval_iso_v(P, self.knots_u, self.knots_v, n_points, 1.0)) elif edge == SurfaceEdge.v0: return np.array(bspline_surf_eval_iso_v(P, self.knots_u, self.knots_v, n_points, 0.0)) elif edge == SurfaceEdge.u1: return np.array(bspline_surf_eval_iso_u(P, self.knots_u, self.knots_v, 1.0, n_points)) elif edge == SurfaceEdge.u0: return np.array(bspline_surf_eval_iso_u(P, self.knots_u, self.knots_v, 0.0, n_points)) else: raise ValueError(f"No edge called {edge}")
[docs] def get_first_derivs_along_edge(self, edge: SurfaceEdge, n_points: int = 10, perp: bool = True) -> np.ndarray: r""" Evaluates the parallel or perpendicular derivative along a surface edge at ``n_points`` parameter locations. The derivative represents either :math:`\frac{\partial \mathbf{S}(u,v)}{\partial u}` or :math:`\frac{\partial \mathbf{S}(u,v)}{\partial v}` depending on which edge is selected and which value is assigned to ``perp``. Parameters ---------- edge: SurfaceEdge Edge along which to evaluate n_points: int Number of evenly-spaced parameter locations at which to evaluate the derivative. Default: 10 perp: bool Whether to evaluate the cross-derivative. If ``False``, the derivative along the parameter direction parallel to the edge will be evaluated instead. Default: ``True`` Returns ------- numpy.ndarray 2-D array of size :math:`n_\text{points} \times 3` """ P = self.get_control_point_array() if edge == SurfaceEdge.v1: return np.array(bspline_surf_dsdv_iso_v( P, self.knots_u, self.knots_v, n_points, 1.0)) if perp else np.array( bspline_surf_dsdu_iso_v(P, self.knots_u, self.knots_v, n_points, 1.0)) elif edge == SurfaceEdge.v0: return np.array(bspline_surf_dsdv_iso_v( P, self.knots_u, self.knots_v, n_points, 0.0)) if perp else np.array( bspline_surf_dsdu_iso_v(P, self.knots_u, self.knots_v, n_points, 0.0)) elif edge == SurfaceEdge.u1: return np.array(bspline_surf_dsdu_iso_u( P, self.knots_u, self.knots_v, 1.0, n_points)) if perp else np.array( bspline_surf_dsdv_iso_u(P, self.knots_u, self.knots_v, 1.0, n_points)) elif edge == SurfaceEdge.u0: return np.array(bspline_surf_dsdu_iso_u( P, self.knots_u, self.knots_v, 0.0, n_points)) if perp else np.array( bspline_surf_dsdv_iso_u(P, self.knots_u, self.knots_v, 0.0, n_points)) else: raise ValueError(f"No edge called {edge}")
[docs] def get_second_derivs_along_edge(self, edge: SurfaceEdge, n_points: int = 10, perp: bool = True) -> np.ndarray: r""" Evaluates the parallel or perpendicular second derivative along a surface edge at ``n_points`` parameter locations. The derivative represents either :math:`\frac{\partial^2 \mathbf{S}(u,v)}{\partial u^2}` or :math:`\frac{\partial^2 \mathbf{S}(u,v)}{\partial v^2}` depending on which edge is selected and which value is assigned to ``perp``. Parameters ---------- edge: SurfaceEdge Edge along which to evaluate n_points: int Number of evenly-spaced parameter locations at which to evaluate the second derivative. Default: 10 perp: bool Whether to evaluate the cross-derivative. If ``False``, the second derivative along the parameter direction parallel to the edge will be evaluated instead. Default: ``True`` Returns ------- numpy.ndarray 2-D array of size :math:`n_\text{points} \times 3` """ P = self.get_control_point_array() if edge == SurfaceEdge.v1: return np.array(bspline_surf_d2sdv2_iso_v( P, self.knots_u, self.knots_v, n_points, 1.0)) if perp else np.array( bspline_surf_d2sdu2_iso_v(P, self.knots_u, self.knots_v, n_points, 1.0)) elif edge == SurfaceEdge.v0: return np.array(bspline_surf_d2sdv2_iso_v( P, self.knots_u, self.knots_v, n_points, 0.0)) if perp else np.array( bspline_surf_d2sdu2_iso_v(P, self.knots_u, self.knots_v, n_points, 0.0)) elif edge == SurfaceEdge.u1: return np.array(bspline_surf_d2sdu2_iso_u( P, self.knots_u, self.knots_v, 1.0, n_points)) if perp else np.array( bspline_surf_d2sdv2_iso_u(P, self.knots_u, self.knots_v, 1.0, n_points)) elif edge == SurfaceEdge.u0: return np.array(bspline_surf_d2sdu2_iso_u( P, self.knots_u, self.knots_v, 0.0, n_points)) if perp else np.array( bspline_surf_d2sdv2_iso_u(P, self.knots_u, self.knots_v, 0.0, n_points)) else: raise ValueError(f"No edge called {edge}")
[docs] def verify_g0(self, other: "BSplineSurface", surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge, n_points: int = 10): """ Verifies that two NURBS Surfaces are G0 continuous along their shared edge""" self_edge = self.get_edge(surface_edge, n_points=n_points) other_edge = other.get_edge(other_surface_edge, n_points=n_points) assert np.array_equal(self_edge, other_edge)
[docs] def verify_g1(self, other: "BSplineSurface", surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge, n_points: int = 10): """ Verifies that two NURBSSurfaces are G1 continuous along their shared edge """ # Get the first derivatives at the boundary and perpendicular to the boundary for each surface, # evaluated at "n_points" locations along the boundary self_perp_edge_derivs = self.get_first_derivs_along_edge(surface_edge, n_points=n_points, perp=True) other_perp_edge_derivs = other.get_first_derivs_along_edge(other_surface_edge, n_points=n_points, perp=True) print(f"{self_perp_edge_derivs = }") print(f"{other_perp_edge_derivs = }") self_perp_edge_derivs[np.absolute(self_perp_edge_derivs) < 1e-6] = 0.0 other_perp_edge_derivs[np.absolute(other_perp_edge_derivs) < 1e-6] = 0.0 # Initialize an array of ratios of magnitude of the derivative values at each point for both sides # of the boundary magnitude_ratios = [] # Loop over each pair of cross-derivatives evaluated along the boundary for point_idx, (self_perp_edge_deriv, other_perp_edge_deriv) in enumerate(zip( self_perp_edge_derivs, other_perp_edge_derivs)): # Ensure that each derivative vector has the same direction along the boundary for each surface try: assert np.allclose( np.nan_to_num(self_perp_edge_deriv / np.linalg.norm(self_perp_edge_deriv)), np.nan_to_num(other_perp_edge_deriv / np.linalg.norm(other_perp_edge_deriv)) ) except AssertionError: assert np.allclose( np.nan_to_num(self_perp_edge_deriv / np.linalg.norm(self_perp_edge_deriv)), np.nan_to_num(-other_perp_edge_deriv / np.linalg.norm(other_perp_edge_deriv)) ) # Compute the ratio of the magnitudes for each derivative vector along the boundary for each surface. # These will be compared at the end. #print(f"{self_perp_edge_deriv=},{other_perp_edge_deriv=}") np.seterr(divide='ignore', invalid='ignore') with np.errstate(divide="ignore"): magnitude_ratios.append(np.nan_to_num(self_perp_edge_deriv / other_perp_edge_deriv, nan=0)) #print("Rational",f"{magnitude_ratios=}") # Assert that the first derivatives along each boundary are proportional current_f = None for magnitude_ratio in magnitude_ratios: for dxdydz_ratio in magnitude_ratio: if np.any(np.isinf(dxdydz_ratio)) or np.any(np.isnan(dxdydz_ratio)) or np.any(dxdydz_ratio == 0.0): continue if current_f is None: current_f = dxdydz_ratio continue assert np.all(np.isclose(dxdydz_ratio, current_f))
[docs] def verify_g2(self, other: "BSplineSurface", surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge, n_points: int = 10): """ Verifies that two B-spline surfaces are G2 continuous along their shared edge """ # Get the first derivatives at the boundary and perpendicular to the boundary for each surface, # evaluated at "n_points" locations along the boundary self_perp_edge_derivs = self.get_second_derivs_along_edge(surface_edge, n_points=n_points, perp=True) other_perp_edge_derivs = other.get_second_derivs_along_edge(other_surface_edge, n_points=n_points, perp=True) print(f"{self_perp_edge_derivs=},{other_perp_edge_derivs=}") self_perp_edge_derivs[np.absolute(self_perp_edge_derivs) < 1e-6] = 0.0 other_perp_edge_derivs[np.absolute(other_perp_edge_derivs) < 1e-6] = 0.0 ratios_other_self = other_perp_edge_derivs / self_perp_edge_derivs #print(f"{ratios_other_self=}") #print(f"{self_perp_edge_derivs=},{other_perp_edge_derivs=}") # Initialize an array of ratios of magnitude of the derivative values at each point for both sides # of the boundary magnitude_ratios = [] # Loop over each pair of cross-derivatives evaluated along the boundary for point_idx, (self_perp_edge_deriv, other_perp_edge_deriv) in enumerate(zip( self_perp_edge_derivs, other_perp_edge_derivs)): # Ensure that each derivative vector has the same direction along the boundary for each surface assert np.allclose( np.nan_to_num(self_perp_edge_deriv / np.linalg.norm(self_perp_edge_deriv)), np.nan_to_num(other_perp_edge_deriv / np.linalg.norm(other_perp_edge_deriv)) ) # Compute the ratio of the magnitudes for each derivative vector along the boundary for each surface. # These will be compared at the end. with np.errstate(divide="ignore"): magnitude_ratios.append(self_perp_edge_deriv / other_perp_edge_deriv) # Assert that the second derivatives along each boundary are proportional current_f = None for magnitude_ratio in magnitude_ratios: for dxdydz_ratio in magnitude_ratio: if np.any(np.isinf(dxdydz_ratio)) or np.any(np.isnan(dxdydz_ratio)) or np.any(dxdydz_ratio == 0.0): continue if current_f is None: current_f = dxdydz_ratio continue assert np.all(np.isclose(dxdydz_ratio, current_f))
[docs] def transform(self, **transformation_kwargs) -> "BSplineSurface": """ Creates a transformed copy of the surface by transforming each of the control points Parameters ---------- transformation_kwargs Keyword arguments passed to :obj:`~aerocaps.geom.transformation.Transformation3D` Returns ------- BSplineSurface Transformed surface """ transformation = Transformation3D(**transformation_kwargs) initial_control_points = self.get_control_point_array() return BSplineSurface( np.array([transformation.transform(p_arr) for p_arr in initial_control_points]), knots_u=deepcopy(self.knots_u), knots_v=deepcopy(self.knots_v), name=self.name, construction=self.construction )
[docs] def generate_control_point_net(self) -> (typing.List[Point3D], typing.List[Line3D]): """ Generates a list of :obj:`~aerocaps.geom.point.Point3D` and :obj:`~aerocaps.geom.curves.Line3D` objects representing the NURBS surface's control points and connections between them Returns ------- typing.List[Point3D], typing.List[Line3D] Control points and lines between adjacent control points in flattened lists """ control_points = self.get_control_point_array() points = [] lines = [] for i in range(self.n_points_u): for j in range(self.n_points_v): points.append(Point3D.from_array(control_points[i, j, :])) for i in range(self.n_points_u - 1): for j in range(self.n_points_v - 1): point_obj_1 = Point3D.from_array(control_points[i, j, :]) point_obj_2 = Point3D.from_array(control_points[i + 1, j, :]) point_obj_3 = Point3D.from_array(control_points[i, j + 1, :]) line_1 = Line3D(p0=point_obj_1, p1=point_obj_2) line_2 = Line3D(p0=point_obj_1, p1=point_obj_3) lines.extend([line_1, line_2]) if i < self.n_points_u - 2 and j < self.n_points_v - 2: continue point_obj_4 = Point3D.from_array(control_points[i + 1, j + 1, :]) line_3 = Line3D(p0=point_obj_3, p1=point_obj_4) line_4 = Line3D(p0=point_obj_2, p1=point_obj_4) lines.extend([line_3, line_4]) return points, lines
[docs] def plot_surface(self, plot: pv.Plotter, Nu: int = 50, Nv: int = 50, **mesh_kwargs): """ Plots the B-spline surface using the `pyvista <https://pyvista.org/>`_ library Parameters ---------- plot: :obj:`pyvista.Plotter` instance Nu: int Number of points to evaluate in the :math:`u`-parametric direction. Default: ``50`` Nv: int Number of points to evaluate in the :math:`v`-parametric direction. Default: ``50`` mesh_kwargs: Keyword arguments to pass to :obj:`pyvista.Plotter.add_mesh` Returns ------- pyvista.core.pointset.StructuredGrid The evaluated B-spline surface """ XYZ = self.evaluate_grid(Nu, Nv) grid = pv.StructuredGrid(XYZ[:, :, 0], XYZ[:, :, 1], XYZ[:, :, 2]) plot.add_mesh(grid, **mesh_kwargs) return grid
[docs] def plot_control_point_mesh_lines(self, plot: pv.Plotter, **line_kwargs) -> pv.Actor: """ Plots the network of lines connecting the B-spline surface control points using the `pyvista <https://pyvista.org/>`_ library Parameters ---------- plot: :obj:`pyvista.Plotter` instance line_kwargs: Keyword arguments to pass to the :obj:`pyvista.Plotter.add_lines` Returns ------- pv.Actor The lines actor """ _, line_objs = self.generate_control_point_net() line_arr = np.array([[line_obj.p0.as_array(), line_obj.p1.as_array()] for line_obj in line_objs]) line_arr = line_arr.reshape((len(line_objs) * 2, 3)) line_actor = plot.add_lines(line_arr, **line_kwargs) return line_actor
[docs] def plot_control_points(self, plot: pv.Plotter, **point_kwargs) -> pv.Actor: """ Plots the B-spline surface control points using the `pyvista <https://pyvista.org/>`_ library Parameters ---------- plot: :obj:`pyvista.Plotter` instance point_kwargs: Keyword arguments to pass to the :obj:`pyvista.Plotter.add_points` Returns ------- pv.Actor The points actor """ point_objs, _ = self.generate_control_point_net() point_arr = np.array([point_obj.as_array() for point_obj in point_objs]) point_actor = plot.add_points(point_arr, **point_kwargs) return point_actor
def __repr__(self): return (f"{self.name}: {self.degree_u} x {self.degree_v} {self.__class__.__name__} " f"({self.n_points_u} x {self.n_points_v} control points)")
[docs] class NURBSSurface(Surface): """ NURBS surface class """
[docs] def __init__(self, points: typing.List[typing.List[Point3D]] or np.ndarray, knots_u: np.ndarray, knots_v: np.ndarray, weights: np.ndarray, name: str = "NURBSSurface", construction: bool = False ): """ Parameters ---------- points knots_u knots_v weights name: str Name of the geometric object. May be re-assigned a unique name when added to a :obj:`~aerocaps.geom.geometry_container.GeometryContainer`. Default: 'NURBSSurface' construction: bool Whether this is a geometry used only for construction of other geometries. If ``True``, this geometry will not be exported or plotted. Default: ``False`` """ if isinstance(points, np.ndarray): points = [[Point3D.from_array(pt_row) for pt_row in pt_mat] for pt_mat in points] self.points = points assert knots_u.ndim == 1 assert knots_v.ndim == 1 assert weights.ndim == 2 assert len(points) == weights.shape[0] assert len(points[0]) == weights.shape[1] # Negative weight check for weight_row in weights: for weight in weight_row: if weight < 0: raise NegativeWeightError("All weights must be non-negative") self.knots_u = deepcopy(knots_u) self.knots_v = deepcopy(knots_v) self.weights = deepcopy(weights) super().__init__(name=name, construction=construction)
@property def n_points_u(self) -> int: """Number of control points in the :math:`u`-parametric direction""" return len(self.points) @property def n_points_v(self) -> int: """Number of control points in the :math:`v`-parametric direction""" return len(self.points[0]) @property def degree_u(self) -> int: """Surface degree in the :math:`u`-parametric direction""" return len(self.knots_u) - self.n_points_u - 1 @property def degree_v(self) -> int: """Surface degree in the :math:`v`-parametric direction""" return len(self.knots_v) - self.n_points_v - 1 @property def n(self) -> int: """ Shorthand for :obj:`~aerocaps.geom.surfaces.NURBSSurface.degree_u` Returns ------- int Surface degree in the :math:`u`-parametric direction """ return self.degree_u @property def m(self) -> int: """ Shorthand for :obj:`~aerocaps.geom.surfaces.NURBSSurface.degree_v` Returns ------- int Surface degree in the :math:`v`-parametric direction """ return self.degree_v
[docs] def to_iges(self, *args, **kwargs) -> aerocaps.iges.entity.IGESEntity: """ Exports the NURBS surface to an IGES entity """ return aerocaps.iges.surfaces.RationalBSplineSurfaceIGES( control_points=self.get_control_point_array(), knots_u=self.knots_u, knots_v=self.knots_v, weights=self.weights, degree_u=self.degree_u, degree_v=self.degree_v )
[docs] def get_control_point_array(self) -> np.ndarray: r""" Gets the control points in float array form. Returns ------- numpy.ndarray Array of size :math:`N_u \times N_v \times 3` """ return np.array([np.array([p.as_array() for p in p_arr]) for p_arr in self.points])
[docs] def get_homogeneous_control_points(self) -> np.ndarray: r""" Gets the array of control points in homogeneous coordinates, :math:`\mathbf{P}_{i,j} \cdot w_{i,j}` Returns ------- numpy.ndarray Array of size :math:`N_u \times N_v \times 4`. The four elements of the last array dimension are, in order, the :math:`x`-coordinate, :math:`y`-coordinate, :math:`z`-coordinate, and weight of each control point. """ return np.dstack(( self.get_control_point_array() * np.repeat(self.weights[:, :, np.newaxis], 3, axis=2), self.weights ))
[docs] @staticmethod def project_homogeneous_control_points(homogeneous_points: np.ndarray) -> (np.ndarray, np.ndarray): """ Projects the homogeneous coordinates onto the :math:`w=1` hyperplane. Returns ------- numpy.ndarray, numpy.ndarray The projected coordinates in three-dimensional space followed by the weight array """ P = homogeneous_points[:, :, :3] / np.repeat(homogeneous_points[:, :, -1][:, :, np.newaxis], 3, axis=2) w = homogeneous_points[:, :, -1] return P, w
[docs] @classmethod def from_bezier_revolve(cls, bezier: BezierCurve3D, axis: Line3D, start_angle: Angle, end_angle: Angle) -> "NURBSSurface": """ Creates a NURBS surface from the revolution of a Bézier curve about an axis. Parameters ---------- bezier: BezierCurve3D Bézier curve to revolve axis: Line3D Axis of revolution start_angle: Angle Starting angle for the revolve end_angle: Angle Ending angle for the revolve Returns ------- NURBSSurface Surface of revolution """ def _determine_angle_distribution() -> typing.List[Angle]: angle_diff = abs(end_angle.rad - start_angle.rad) if angle_diff == 0.0: raise InvalidGeometryError("Starting and ending angles cannot be the same for a " "NURBSSurface from revolved Bezier curve") if angle_diff % (0.5 * np.pi) == 0.0: # If angle difference is a multiple of 90 degrees N_angles = 2 * int(angle_diff // (0.5 * np.pi)) + 1 else: N_angles = 2 * int(angle_diff // (0.5 * np.pi)) + 3 rad_dist = np.linspace(start_angle.rad, end_angle.rad, N_angles) return [Angle(rad=r) for r in rad_dist] control_points = [] weights = [] angles = _determine_angle_distribution() for point in bezier.control_points: axis_projection = project_point_onto_line(point, axis) radius = measure_distance_point_line(point, axis) if radius == 0.0: new_points = [point for _ in angles] else: new_points = [rotate_point_about_axis(point, axis, angle) for angle in angles] for idx, rotated_point in enumerate(new_points): if idx == 0: weights.append([]) if not idx % 2: # Skip even indices (these represent the "through" control points) weights[-1].append(1.0) continue sine_half_angle = np.sin(0.5 * np.pi - 0.5 * (angles[idx + 1].rad - angles[idx - 1].rad)) if radius != 0.0: distance = radius / sine_half_angle # radius / sin(half angle) vector = Vector3D(p0=axis_projection, p1=rotated_point) new_points[idx] = axis_projection + Point3D.from_array( distance * np.array(vector.normalized_value())) weights[-1].append(sine_half_angle) control_points.append(np.array([new_point.as_array() for new_point in new_points])) control_points = np.array(control_points) weights = np.array(weights) knots_v = np.array([0.0, 0.0, 0.0, 1.0, 1.0, 1.0]) n_knots_to_insert = len(angles) - 3 if n_knots_to_insert > 0: delta = 1.0 / (n_knots_to_insert / 2 + 1) for idx in range(n_knots_to_insert): new_knot = knots_v[2 + idx] if idx % 2 else knots_v[2 + idx] + delta knots_v = np.insert(knots_v, 3 + idx, new_knot) knots_u = np.array([0.0 for _ in bezier.control_points] + [1.0 for _ in bezier.control_points]) return cls(control_points, knots_u, knots_v, weights)
[docs] def evaluate(self, u: float, v: float) -> np.ndarray: r""" Evaluates the surface at a given :math:`(u,v)` parameter pair. Parameters ---------- u: float Position along :math:`u` in parametric space. Normally in the range :math:`[0,1]` v: float Position along :math:`v` in parametric space. Normally in the range :math:`[0,1]` Returns ------- numpy.ndarray 1-D array of the form ``array([x, y, z])`` representing the evaluated point on the surface """ P = self.get_control_point_array() return np.array(nurbs_surf_eval(P, self.weights, self.knots_u, self.knots_v, u, v))
[docs] def evaluate_point3d(self, u: float, v: float) -> Point3D: r""" Evaluates the NURBS surface at a single :math:`(u,v)` parameter pair and returns a point object. Parameters ---------- u: float Position along :math:`u` in parametric space. Normally in the range :math:`[0,1]` v: float Position along :math:`v` in parametric space. Normally in the range :math:`[0,1]` Returns ------- Point3D Point object corresponding to the :math:`(u,v)` pair """ return Point3D.from_array(self.evaluate(u, v))
[docs] def evaluate_grid(self, Nu: int, Nv: int) -> np.ndarray: r""" Evaluates the NURBS surface on a uniform :math:`N_u \times N_v` grid of parameter values. Parameters ---------- Nu: int Number of uniformly spaced parameter values in the :math:`u`-direction Nv: int Number of uniformly spaced parameter values in the :math:`v`-direction Returns ------- numpy.ndarray Array of size :math:`N_u \times N_v \times 3` """ P = self.get_control_point_array() return np.array(nurbs_surf_eval_grid(P, self.weights, self.knots_u, self.knots_v, Nu, Nv))
[docs] def get_parallel_control_point_length(self, surface_edge: SurfaceEdge) -> int: r""" Gets the number of control points of the curve corresponding to the input surface edge. Parameters ---------- surface_edge: SurfaceEdge Edge along which the number of control points is computed Returns ------- int Number of control points parallel to the edge """ if surface_edge in [SurfaceEdge.v1, SurfaceEdge.v0]: return self.n_points_u return self.n_points_v
[docs] def get_perpendicular_control_point_length(self, surface_edge: SurfaceEdge) -> int: r""" Gets the number of control points in the parametric direction perpendicular to the input surface edge. Parameters ---------- surface_edge: SurfaceEdge Edge along which the number of perpendicular control points is computed Returns ------- int Number of control points perpendicular to the edge """ if surface_edge in [SurfaceEdge.v1, SurfaceEdge.v0]: return self.n_points_v return self.n_points_u
[docs] def get_parallel_degree(self, surface_edge: SurfaceEdge) -> int: r""" Gets the degree of the curve corresponding to the input surface edge. Parameters ---------- surface_edge: SurfaceEdge Edge along which the parallel degree is evaluated Returns ------- int Degree parallel to the edge """ if surface_edge in [SurfaceEdge.v1, SurfaceEdge.v0]: return self.degree_u return self.degree_v
[docs] def get_perpendicular_degree(self, surface_edge: SurfaceEdge) -> int: r""" Gets the degree of the curve in the parametric direction perpendicular to the input surface edge. Parameters ---------- surface_edge: SurfaceEdge Edge along which the perpendicular degree is evaluated Returns ------- int Degree perpendicular to the edge """ if surface_edge in [SurfaceEdge.v1, SurfaceEdge.v0]: return self.degree_v return self.degree_u
[docs] def get_parallel_knots(self, surface_edge: SurfaceEdge) -> np.ndarray: r""" Gets the knots in the parametric direction parallel to the input surface edge. Parameters ---------- surface_edge: SurfaceEdge Edge along which the parallel knots are returned Returns ------- numpy.ndarray Knots parallel to the edge """ if surface_edge in [SurfaceEdge.v1, SurfaceEdge.v0]: return self.knots_u return self.knots_v
[docs] def get_perpendicular_knots(self, surface_edge: SurfaceEdge) -> np.ndarray: r""" Gets the knots in the parametric direction perpendicular to the input surface edge. Parameters ---------- surface_edge: SurfaceEdge Edge along which the perpendicular knots are returned Returns ------- numpy.ndarray Knots perpendicular to the edge """ if surface_edge in [SurfaceEdge.v1, SurfaceEdge.v0]: return self.knots_v return self.knots_u
[docs] def get_point(self, row_index: int, continuity_index: int, surface_edge: SurfaceEdge) -> Point3D: r""" Gets the point corresponding to a particular index along the edge curve with perpendicular index corresponding to the level of continuity being applied. For example, for a :math:`6 \times 5` NURBS surface, the following code .. code-block:: python p = surf.get_point(2, 1, ac.SurfaceEdge.v0) returns the point :math:`\mathbf{P}_{2,1}` and .. code-block:: python p = surf.get_point(2, 1, ac.SurfaceEdge.u1) returns the point :math:`\mathbf{P}_{6-1,2} = \mathbf{P}_{5,2}` if there are no internal knot vectors. If the NURBS surface has internal knot vectors, the actual :math:`i`-index of the point may be different, but the second-to-last point in the third row of control points will still be returned. .. seealso:: :obj:`~aerocaps.geom.surfaces.NURBSSurface.set_point` Setter equivalent of this method Parameters ---------- row_index: int Index along the surface edge control points continuity_index: int Index in the parametric direction perpendicular to the surface edge. Normally either ``0``, ``1``, or ``2`` surface_edge: SurfaceEdge Edge of the surface along which to retrieve the control point Returns ------- Point3D Point used to enforce :math:`G^x` continuity, where :math:`x` is the value of ``continuity_index`` """ if surface_edge == SurfaceEdge.v1: return self.points[row_index][-(continuity_index + 1)] elif surface_edge == SurfaceEdge.v0: return self.points[row_index][continuity_index] elif surface_edge == SurfaceEdge.u1: return self.points[-(continuity_index + 1)][row_index] elif surface_edge == SurfaceEdge.u0: return self.points[continuity_index][row_index] else: raise ValueError("Invalid surface_edge value")
[docs] def set_point(self, point: Point3D, row_index: int, continuity_index: int, surface_edge: SurfaceEdge): r""" Sets the point corresponding to a particular index along the edge curve with perpendicular index corresponding to the level of continuity being applied. For example, for a :math:`6 \times 5` NURBS surface, the following code .. code-block:: python p = ac.Point3D.from_array(np.array([3.0, 4.0, 5.0])) surf.set_point(p, 2, 1, ac.SurfaceEdge.v0) sets the value of point :math:`\mathbf{P}_{2,1}` to :math:`[3,4,5]^T` and .. code-block:: python p = ac.Point3D.from_array(np.array([3.0, 4.0, 5.0])) surf.get_point(p, 2, 1, ac.SurfaceEdge.u1) sets the value of point :math:`\mathbf{P}_{6-1,2} = \mathbf{P}_{5,2}` to :math:`[3,4,5]^T` if there are no internal knot vectors. .. seealso:: :obj:`~aerocaps.geom.surfaces.NURBSSurface.get_point` Getter equivalent of this method Parameters ---------- point: Point3D Point object to apply at the specified indices row_index: int Index along the surface edge control points continuity_index: int Index in the parametric direction perpendicular to the surface edge. Normally either ``0``, ``1``, or ``2`` surface_edge: SurfaceEdge Edge of the surface along which to retrieve the control point """ if surface_edge == SurfaceEdge.v1: self.points[row_index][-(continuity_index + 1)].x.m = point.x.m self.points[row_index][-(continuity_index + 1)].y.m = point.y.m self.points[row_index][-(continuity_index + 1)].z.m = point.z.m elif surface_edge == SurfaceEdge.v0: self.points[row_index][continuity_index].x.m = point.x.m self.points[row_index][continuity_index].y.m = point.y.m self.points[row_index][continuity_index].z.m = point.z.m elif surface_edge == SurfaceEdge.u1: self.points[-(continuity_index + 1)][row_index].x.m = point.x.m self.points[-(continuity_index + 1)][row_index].y.m = point.y.m self.points[-(continuity_index + 1)][row_index].z.m = point.z.m elif surface_edge == SurfaceEdge.u0: self.points[continuity_index][row_index].x.m = point.x.m self.points[continuity_index][row_index].y.m = point.y.m self.points[continuity_index][row_index].z.m = point.z.m else: raise ValueError("Invalid surface_edge value")
[docs] def get_weight(self, row_index: int, continuity_index: int, surface_edge: SurfaceEdge): r""" Gets the weight corresponding to a particular index along the edge curve with perpendicular index corresponding to the level of continuity being applied. For example, for a :math:`6 \times 5` NURBS surface, the following code .. code-block:: python w = surf.get_weight(2, 1, ac.SurfaceEdge.v0) returns the weight :math:`w_{2,1}` and .. code-block:: python w = surf.get_weight(2, 1, ac.SurfaceEdge.u1) returns the weight :math:`w_{6-1,2} = w_{5,2}`. .. seealso:: :obj:`~aerocaps.geom.surfaces.NURBSSurface.set_weight` Setter equivalent of this method Parameters ---------- row_index: int Index along the surface edge weights continuity_index: int Index in the parametric direction perpendicular to the surface edge. Normally either ``0``, ``1``, or ``2`` surface_edge: SurfaceEdge Edge of the surface along which to retrieve the weight Returns ------- float Weight used to enforce :math:`G^x` continuity, where :math:`x` is the value of ``continuity_index`` """ if surface_edge == SurfaceEdge.v1: return self.weights[row_index][-(continuity_index + 1)] elif surface_edge == SurfaceEdge.v0: return self.weights[row_index][continuity_index] elif surface_edge == SurfaceEdge.u1: return self.weights[-(continuity_index + 1)][row_index] elif surface_edge == SurfaceEdge.u0: return self.weights[continuity_index][row_index] else: raise ValueError("Invalid surface_edge value")
[docs] def set_weight(self, weight: float, row_index: int, continuity_index: int, surface_edge: SurfaceEdge): r""" Sets the weight corresponding to a particular index along the edge curve with perpendicular index corresponding to the level of continuity being applied. For example, for a :math:`6 \times 5` NURBS surface, the following code .. code-block:: python surf.set_weight(0.9, 2, 1, ac.SurfaceEdge.v0) sets the value of weight :math:`w_{2,1}` to :math:`0.9` and .. code-block:: python surf.set_weight(1.1, 2, 1, ac.SurfaceEdge.u1) sets the value of weight :math:`w_{6-1,2} = w_{5,2}` to :math:`1.1`. .. seealso:: :obj:`~aerocaps.geom.surfaces.NURBSSurface.get_weight` Getter equivalent of this method Parameters ---------- weight: float Weight to apply at the specified indices row_index: int Index along the surface edge weights continuity_index: int Index in the parametric direction perpendicular to the surface edge. Normally either ``0``, ``1``, or ``2`` surface_edge: SurfaceEdge Edge of the surface along which to retrieve the weight """ if surface_edge == SurfaceEdge.v1: self.weights[row_index][-(continuity_index + 1)] = weight elif surface_edge == SurfaceEdge.v0: self.weights[row_index][continuity_index] = weight elif surface_edge == SurfaceEdge.u1: self.weights[-(continuity_index + 1)][row_index] = weight elif surface_edge == SurfaceEdge.u0: self.weights[continuity_index][row_index] = weight else: raise ValueError("Invalid surface_edge value")
[docs] def extract_edge_curve(self, surface_edge: SurfaceEdge) -> NURBSCurve3D: """ Extracts the control points, weights, and knots from one of the four edges of the NURBS surface and outputs a NURBS curve with these control points and weights Parameters ---------- surface_edge: SurfaceEdge Edge along which to extract the curve Returns ------- NURBSCurve3D NURBS curve with control points, weights, and knots corresponding to the control points, weights, and knots along the edge of the surface """ P = self.get_control_point_array() w = self.weights if surface_edge == SurfaceEdge.u0: return NURBSCurve3D(P[0, :, :], w[0, :], self.knots_v, self.degree_v) if surface_edge == SurfaceEdge.u1: return NURBSCurve3D(P[-1, :, :], w[-1, :], self.knots_v, self.degree_v) if surface_edge == SurfaceEdge.v0: return NURBSCurve3D(P[:, 0, :], w[:, 0], self.knots_u, self.degree_u) if surface_edge == SurfaceEdge.v1: return NURBSCurve3D(P[:, -1, :], w[:, -1], self.knots_u, self.degree_u) raise ValueError(f"Invalid surface edge {surface_edge}")
[docs] def is_clamped(self, surface_edge: SurfaceEdge) -> bool: """ Checks if the NURBS surface is clamped along an edge Parameters ---------- surface_edge: SurfaceEdge Edge where the perpendicular knots will be inspected Returns ------- bool Whether the surface is clamped at the given edge """ p = self.get_perpendicular_degree(surface_edge) knots = self.get_perpendicular_knots(surface_edge) if surface_edge in (SurfaceEdge.u0, SurfaceEdge.v0): start_knot = knots[0] if np.all(knots[:(p + 1)] == start_knot): return True return False end_knot = knots[-1] if np.all(knots[-(p + 1):] == end_knot): return True return False
[docs] def has_internal_knots(self, direction: str) -> bool: """ Whether the surface has internal knots in the specified direction Parameters ---------- direction: str Either 'u' or 'v' Returns ------- bool """ if direction == "u": return len(set(self.knots_u)) > 2 elif direction == "v": return len(set(self.knots_v)) > 2 else: raise ValueError(f"Invalid direction {direction}. Must be either 'u' or 'v'")
[docs] def enforce_g0(self, other: "NURBSSurface", surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge): r""" Enforces :math:`G^0` continuity along the input ``surface_edge`` by equating the control points and weights along this edge to the corresponding control points and weights along the ``other_surface_edge`` of the NURBS surface given by ``other``. The control points of the surface from which this method is called are modified in-place, and the control points of ``other`` are left unchanged. .. important:: The parallel degree of the current surface along ``surface_edge`` must be equal to the parallel degree of the ``other`` surface along ``other_surface_edge``, otherwise an ``AssertionError`` will be raised. Additionally, the knot vector along the ``surface_edge`` of the current surface must be equal to the knot vector along the ``other_surface_edge`` of the other surface. .. seealso:: :obj:`~aerocaps.geom.surfaces.NURBSSurface.enforce_c0` Parametric continuity equivalent (:math:`C^0`) Parameters ---------- other: NURBSSurface Another NURBS surface along which an edge will be used for stitching surface_edge: SurfaceEdge The edge of the current surface to modify other_surface_edge: SurfaceEdge Tool edge of surface ``other`` which determines the positions of control points along ``surface_edge`` of the current surface """ # P^b[:, 0] = P^a[:, -1] self_parallel_knots = self.get_parallel_knots(surface_edge) other_parallel_knots = other.get_parallel_knots(other_surface_edge) self_parallel_degree = self.get_parallel_degree(surface_edge) other_parallel_degree = other.get_parallel_degree(other_surface_edge) # Check clamped for self: if not self.is_clamped(surface_edge): raise ValueError(" The self surface is not clamped on the edge that has to be stitched") #Check clamped for other: if not other.is_clamped(other_surface_edge): raise ValueError(" The self surface is not clamped on the edge that has to be stitched") if len(self_parallel_knots) != len(other_parallel_knots): raise ValueError(f"Length of the knot vector parallel to the edge of the input surface " f"({len(self_parallel_knots)}) is not equal to the length of the knot vector " f"parallel to the edge of the other surface ({len(other_parallel_knots)})") if any([k1 != k2 for k1, k2 in zip(self_parallel_knots, other_parallel_knots)]): raise ValueError(f"Knots parallel to the edge of the input surface ({self_parallel_knots}) " f"are not equal to the knots in the direction parallel to the edge of the other " f"surface ({other_parallel_knots})") if self_parallel_degree != other_parallel_degree: raise ValueError(f"Degree parallel to the edge of the input surface ({self_parallel_degree}) does " f"not match the degree parallel to the edge of the other surface " f"({other_parallel_degree})") for row_index in range(self.get_parallel_control_point_length(surface_edge)): self.set_point(other.get_point(row_index, 0, other_surface_edge), row_index, 0, surface_edge) self.set_weight(other.get_weight(row_index, 0, other_surface_edge), row_index, 0, surface_edge)
[docs] def enforce_c0(self, other: "NURBSSurface", surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge): """ For zeroth-degree continuity, there is no difference between geometric (:math:`G^0`) and parametric (:math:`C^0`) continuity. Because this method is simply a convenience method that calls :obj:`~aerocaps.geom.surfaces.NURBSSurface.enforce_g0`, see the documentation for that method for more detailed documentation. Parameters ---------- other: NURBSSurface Another NURBS surface along which an edge will be used for stitching surface_edge: SurfaceEdge The edge of the current surface to modify other_surface_edge: SurfaceEdge Tool edge of surface ``other`` which determines the positions of control points along ``surface_edge`` of the current surface """ self.enforce_g0(other, surface_edge, other_surface_edge)
[docs] def enforce_g0g1(self, other: "NURBSSurface", f: float, surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge): r""" First enforces :math:`G^0` continuity, then tangent (:math:`G^1`) continuity is enforced according to the following equations: .. math:: \mathcal{W}^{b,\mathcal{E}_b}_{k,1} = \mathcal{W}^{b,\mathcal{E}_b}_{k,0} + f \frac{p_{\perp}^{a,\mathcal{E}_a}}{p_{\perp}^{b,\mathcal{E}_b}} \left( \mathcal{W}^{a,\mathcal{E}_a}_{k,0} - \mathcal{W}^{a,\mathcal{E}_a}_{k,1} \right) \text{ for }k=0,1,\ldots,p_{\parallel}^{b,\mathcal{E}_b} .. math:: \mathcal{P}^{b,\mathcal{E}_b}_{k,1} = \frac{\mathcal{W}^{b,\mathcal{E}_b}_{k,0}}{\mathcal{W}^{b,\mathcal{E}_b}_{k,1}} \mathcal{P}^{b,\mathcal{E}_b}_{k,0} + f \frac{p_{\perp}^{a,\mathcal{E}_a}}{p_{\perp}^{b,\mathcal{E}_b}} \frac{1}{\mathcal{W}^{b,\mathcal{E}_b}_{k,1}} \left[\mathcal{W}^{a,\mathcal{E}_a}_{k,0} \mathcal{P}^{a,\mathcal{E}_a}_{k,0} - \mathcal{P}^{a,\mathcal{E}_a}_{k,1} \mathcal{W}^{a,\mathcal{E}_a}_{k,1} \right] \text{ for }k=0,1,\ldots,p_{\parallel}^{b,\mathcal{E}_b} Here, :math:`b` corresponds to the current surface, and :math:`a` corresponds to the ``other`` surface. The control points of the surface from which this method is called are modified in-place, and the control points of ``other`` are left unchanged. .. seealso:: :obj:`~aerocaps.geom.surfaces.NURBSSurface.enforce_g0` Geometric point continuity enforcement (:math:`G^0`) :obj:`~aerocaps.geom.surfaces.NURBSSurface.enforce_c0c1` Parametric continuity equivalent (:math:`C^1`) Parameters ---------- other: NURBSSurface Another NURBS surface along which an edge will be used for stitching f: float Tangent proportionality factor surface_edge: SurfaceEdge The edge of the current surface to modify other_surface_edge: SurfaceEdge Tool edge of surface ``other`` which determines the positions of control points along ``surface_edge`` of the current surface """ self.enforce_g0(other, surface_edge, other_surface_edge) n_ratio = other.get_perpendicular_degree(other_surface_edge) / self.get_perpendicular_degree(surface_edge) for row_index in range(self.get_parallel_control_point_length(surface_edge)): w_i0_b = self.get_weight(row_index, 0, surface_edge) w_im_a = other.get_weight(row_index, 0, other_surface_edge) w_im1_a = other.get_weight(row_index, 1, other_surface_edge) w_i1_b = w_i0_b + f * n_ratio * (w_im_a - w_im1_a) if w_i1_b < 0: raise NegativeWeightError("G1 enforcement generated a negative weight") self.set_weight(w_i1_b, row_index, 1, surface_edge) P_i0_b = self.get_point(row_index, 0, surface_edge) P_im_a = other.get_point(row_index, 0, other_surface_edge) P_im1_a = other.get_point(row_index, 1, other_surface_edge) P_i1_b = w_i0_b / w_i1_b * P_i0_b + f * n_ratio / w_i1_b * (w_im_a * P_im_a - w_im1_a * P_im1_a) self.set_point(P_i1_b, row_index, 1, surface_edge)
[docs] def enforce_c0c1(self, other: "NURBSSurface", surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge): r""" Equivalent to calling :obj:`~aerocaps.geom.surfaces.NURBSSurface.enforce_g0g1` with ``f=1.0``. See that method for more detailed documentation. Parameters ---------- other: NURBSSurface Another NURBS surface along which an edge will be used for stitching surface_edge: SurfaceEdge The edge of the current surface to modify other_surface_edge: SurfaceEdge Tool edge of surface ``other`` which determines the positions of control points along ``surface_edge`` of the current surface """ self.enforce_g0g1(other, 1.0, surface_edge, other_surface_edge)
[docs] def enforce_g0g1g2(self, other: "NURBSSurface", f: float, surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge): r""" First enforces :math:`G^0` and :math:`G^1` continuity, then curvature (:math:`G^2`) continuity is enforced according to the following equations: .. math:: \mathcal{W}^{b,\mathcal{E}_b}_{k,2} = 2 \mathcal{W}^{b,\mathcal{E}_b}_{k,1} - \mathcal{W}^{b,\mathcal{E}_b}_{k,0} + f^2 \frac{p_{\perp}^{a,\mathcal{E}_a}(p_{\perp}^{a,\mathcal{E}_a}-1)}{p_{\perp}^{b,\mathcal{E}_b}(p_{\perp}^{b,\mathcal{E}_b}-1)} \left[ \mathcal{W}^{a,\mathcal{E}_a}_{k,0} - 2 \mathcal{W}^{a,\mathcal{E}_a}_{k,1} + \mathcal{W}^{a,\mathcal{E}_a}_{k,2} \right] \text{ for }k=0,1,\ldots,p_{\parallel}^{b,\mathcal{E}_b} .. math:: \mathcal{P}^{b,\mathcal{E}_b}_{k,2} = 2 \frac{\mathcal{W}^{b,\mathcal{E}_b}_{k,1}}{\mathcal{W}^{b,\mathcal{E}_b}_{k,2}} \mathcal{P}^{b,\mathcal{E}_b}_{k,1} - \frac{\mathcal{W}^{b,\mathcal{E}_b}_{k,0}}{\mathcal{W}^{b,\mathcal{E}_b}_{k,2}} \mathcal{P}^{b,\mathcal{E}_b}_{k,0} + f^2 \frac{p_{\perp}^{a,\mathcal{E}_a}(p_{\perp}^{a,\mathcal{E}_a}-1)}{p_{\perp}^{b,\mathcal{E}_b}(p_{\perp}^{b,\mathcal{E}_b}-1)} \frac{1}{\mathcal{W}^{b,\mathcal{E}_b}_{k,2}} \left[ \mathcal{W}^{a,\mathcal{E}_a}_{k,1} \mathcal{P}^{a,\mathcal{E}_a}_{k,0} - 2 \mathcal{W}^{a,\mathcal{E}_a}_{k,1} \mathcal{P}^{a,\mathcal{E}_a}_{k,1} + \mathcal{W}^{a,\mathcal{E}_a}_{k,2} \mathcal{P}^{a,\mathcal{E}_a}_{k,2} \right] \text{ for }k=0,1,\ldots,p_{\parallel}^{b,\mathcal{E}_b} Here, :math:`b` corresponds to the current surface, and :math:`a` corresponds to the ``other`` surface. The control points of the surface from which this method is called are modified in-place, and the control points of ``other`` are left unchanged. .. seealso:: :obj:`~aerocaps.geom.surfaces.NURBSSurface.enforce_g0` Geometric point continuity enforcement (:math:`G^0`) :obj:`~aerocaps.geom.surfaces.NURBSSurface.enforce_g0g1` Geometric tangent continuity enforcement (:math:`G^1`) :obj:`~aerocaps.geom.surfaces.NURBSSurface.enforce_c0c1c2` Parametric continuity equivalent (:math:`C^2`) Parameters ---------- other: NURBSSurface Another NURBS surface along which an edge will be used for stitching f: float Tangent proportionality factor surface_edge: SurfaceEdge The edge of the current surface to modify other_surface_edge: SurfaceEdge Tool edge of surface ``other`` which determines the positions of control points along ``surface_edge`` of the current surface """ self.enforce_g0g1(other, f, surface_edge, other_surface_edge) n_ratio = (other.get_perpendicular_degree(other_surface_edge) ** 2 - other.get_perpendicular_degree(other_surface_edge)) / ( self.get_perpendicular_degree(surface_edge) ** 2 - self.get_perpendicular_degree( surface_edge)) for row_index in range(self.get_parallel_control_point_length(surface_edge)): w_i0_b = self.get_weight(row_index, 0, surface_edge) w_i1_b = self.get_weight(row_index, 1, surface_edge) w_im_a = other.get_weight(row_index, 0, other_surface_edge) w_im1_a = other.get_weight(row_index, 1, other_surface_edge) w_im2_a = other.get_weight(row_index, 2, other_surface_edge) w_i2_b = 2.0 * w_i1_b - w_i0_b + f ** 2 * n_ratio * (w_im_a - 2.0 * w_im1_a + w_im2_a) if w_i2_b < 0: raise NegativeWeightError("G2 enforcement generated a negative weight") self.set_weight(w_i2_b, row_index, 2, surface_edge) P_i0_b = self.get_point(row_index, 0, surface_edge) P_i1_b = self.get_point(row_index, 1, surface_edge) P_im_a = other.get_point(row_index, 0, other_surface_edge) P_im1_a = other.get_point(row_index, 1, other_surface_edge) P_im2_a = other.get_point(row_index, 2, other_surface_edge) P_i2_b = (2.0 * w_i1_b / w_i2_b * P_i1_b - w_i0_b / w_i2_b * P_i0_b) + f ** 2 * n_ratio * ( 1 / w_i2_b) * ( w_im_a * P_im_a - 2.0 * w_im1_a * P_im1_a + w_im2_a * P_im2_a) self.set_point(P_i2_b, row_index, 2, surface_edge)
[docs] def enforce_c0c1c2(self, other: "NURBSSurface", surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge): r""" Equivalent to calling :obj:`~aerocaps.geom.surfaces.NURBSSurface.enforce_g0g1g2` with ``f=1.0``. See that method for more detailed documentation. Parameters ---------- other: NURBSSurface Another NURBS surface along which an edge will be used for stitching surface_edge: SurfaceEdge The edge of the current surface to modify other_surface_edge: SurfaceEdge Tool edge of surface ``other`` which determines the positions of control points along ``surface_edge`` of the current surface """ self.enforce_g0g1g2(other, 1.0, surface_edge, other_surface_edge)
[docs] def dSdu(self, u: float, v: float) -> np.ndarray: r""" Evaluates the first derivative with respect to :math:`u` at a single :math:`(u,v)` pair Parameters ---------- u: float Position along :math:`u` in parametric space. Normally in the range :math:`[0,1]` v: float Position along :math:`v` in parametric space. Normally in the range :math:`[0,1]` Returns ------- np.ndarray 1-D array containing the :math:`x`-, :math:`y`-, and :math:`z`-components of the second derivative """ P = self.get_control_point_array() return np.array(nurbs_surf_dsdu(P, self.weights, self.knots_u, self.knots_v, u, v))
[docs] def dSdu_grid(self, Nu: int, Nv: int) -> np.ndarray: r""" Evaluates the first derivative with respect to :math:`u` on a linearly-spaced grid of :math:`u`- and :math:`v`-values. Parameters ---------- Nu: int Number of evenly spaced :math:`u` values Nv: int Number of evenly spaced :math:`v` values Returns ------- np.ndarray Array of size :math:`N_u \times N_v \times 3` """ P = self.get_control_point_array() return np.array(nurbs_surf_dsdu_grid(P, self.weights, self.knots_u, self.knots_v, Nu, Nv))
[docs] def dSdu_uvvecs(self, u: np.ndarray, v: np.ndarray): r""" Evaluates the first derivative of the surface with respect to :math:`u` at arbitrary vectors of :math:`u` and :math:`v`-values. Parameters ---------- u: np.ndarray 1-D array of :math:`u`-parameter values v: np.ndarray 1-D array of :math:`v`-parameter values Returns ------- np.ndarray Array of size :math:`\text{len}(u) \times \text{len}(v) \times 3` """ P = self.get_control_point_array() return np.array(nurbs_surf_dsdu_uvvecs(P, self.weights, self.knots_u, self.knots_v, u, v))
[docs] def dSdv(self, u: float or np.ndarray, v: float or np.ndarray): r""" Evaluates the first derivative with respect to :math:`v` at a single :math:`(u,v)` pair Parameters ---------- u: float Position along :math:`u` in parametric space. Normally in the range :math:`[0,1]` v: float Position along :math:`v` in parametric space. Normally in the range :math:`[0,1]` Returns ------- np.ndarray 1-D array containing the :math:`x`-, :math:`y`-, and :math:`z`-components of the second derivative """ P = self.get_control_point_array() return np.array(nurbs_surf_dsdv(P, self.weights, self.knots_u, self.knots_v, u, v))
[docs] def dSdv_grid(self, Nu: int, Nv: int) -> np.ndarray: r""" Evaluates the first derivative with respect to :math:`v` on a linearly-spaced grid of :math:`u`- and :math:`v`-values. Parameters ---------- Nu: int Number of evenly spaced :math:`u` values Nv: int Number of evenly spaced :math:`v` values Returns ------- np.ndarray Array of size :math:`N_u \times N_v \times 3` """ P = self.get_control_point_array() return np.array(nurbs_surf_dsdv_grid(P, self.weights, self.knots_u, self.knots_v, Nu, Nv))
[docs] def dSdv_uvvecs(self, u: np.ndarray, v: np.ndarray): r""" Evaluates the first derivative of the surface with respect to :math:`v` at arbitrary vectors of :math:`u` and :math:`v`-values. Parameters ---------- u: np.ndarray 1-D array of :math:`u`-parameter values v: np.ndarray 1-D array of :math:`v`-parameter values Returns ------- np.ndarray Array of size :math:`\text{len}(u) \times \text{len}(v) \times 3` """ P = self.get_control_point_array() return np.array(nurbs_surf_dsdv_uvvecs(P, self.weights, self.knots_u, self.knots_v, u, v))
[docs] def d2Sdu2(self, u: float, v: float) -> np.ndarray: r""" Evaluates the second derivative with respect to :math:`u` at a single :math:`(u,v)` pair Parameters ---------- u: float Position along :math:`u` in parametric space. Normally in the range :math:`[0,1]` v: float Position along :math:`v` in parametric space. Normally in the range :math:`[0,1]` Returns ------- np.ndarray 1-D array containing the :math:`x`-, :math:`y`-, and :math:`z`-components of the second derivative """ P = self.get_control_point_array() return np.array(nurbs_surf_d2sdu2(P, self.weights, self.knots_u, self.knots_v, u, v))
[docs] def d2Sdu2_grid(self, Nu: int, Nv: int) -> np.ndarray: r""" Evaluates the second derivative with respect to :math:`u` on a linearly-spaced grid of :math:`u`- and :math:`v`-values. Parameters ---------- Nu: int Number of evenly spaced :math:`u` values Nv: int Number of evenly spaced :math:`v` values Returns ------- np.ndarray Array of size :math:`N_u \times N_v \times 3` """ P = self.get_control_point_array() return np.array(nurbs_surf_d2sdu2_grid(P, self.weights, self.knots_u, self.knots_v, Nu, Nv))
[docs] def d2Sdu2_uvvecs(self, u: np.ndarray, v: np.ndarray): r""" Evaluates the second derivative of the surface with respect to :math:`u` at arbitrary vectors of :math:`u` and :math:`v`-values. Parameters ---------- u: np.ndarray 1-D array of :math:`u`-parameter values v: np.ndarray 1-D array of :math:`v`-parameter values Returns ------- np.ndarray Array of size :math:`\text{len}(u) \times \text{len}(v) \times 3` """ P = self.get_control_point_array() return np.array(nurbs_surf_d2sdu2_uvvecs(P, self.weights, self.knots_u, self.knots_v, u, v))
[docs] def d2Sdv2(self, u: float or np.ndarray, v: float or np.ndarray): r""" Evaluates the second derivative with respect to :math:`v` at a single :math:`(u,v)` pair Parameters ---------- u: float Position along :math:`u` in parametric space. Normally in the range :math:`[0,1]` v: float Position along :math:`v` in parametric space. Normally in the range :math:`[0,1]` Returns ------- np.ndarray 1-D array containing the :math:`x`-, :math:`y`-, and :math:`z`-components of the second derivative """ P = self.get_control_point_array() return np.array(nurbs_surf_d2sdv2(P, self.weights, self.knots_u, self.knots_v, u, v))
[docs] def d2Sdv2_grid(self, Nu: int, Nv: int) -> np.ndarray: r""" Evaluates the second derivative with respect to :math:`v` on a linearly-spaced grid of :math:`u`- and :math:`v`-values. Parameters ---------- Nu: int Number of evenly spaced :math:`u` values Nv: int Number of evenly spaced :math:`v` values Returns ------- np.ndarray Array of size :math:`N_u \times N_v \times 3` """ P = self.get_control_point_array() return np.array(nurbs_surf_d2sdv2_grid(P, self.weights, self.knots_u, self.knots_v, Nu, Nv))
[docs] def d2Sdv2_uvvecs(self, u: np.ndarray, v: np.ndarray): r""" Evaluates the second derivative of the surface with respect to :math:`v` at arbitrary vectors of :math:`u` and :math:`v`-values. Parameters ---------- u: np.ndarray 1-D array of :math:`u`-parameter values v: np.ndarray 1-D array of :math:`v`-parameter values Returns ------- np.ndarray Array of size :math:`\text{len}(u) \times \text{len}(v) \times 3` """ P = self.get_control_point_array() return np.array(nurbs_surf_d2sdv2_uvvecs(P, self.weights, self.knots_u, self.knots_v, u, v))
[docs] def get_edge(self, edge: SurfaceEdge, n_points: int = 10) -> np.ndarray: r""" Evaluates the surface at ``n_points`` parameter locations along a given edge. Parameters ---------- edge: SurfaceEdge Edge along which to evaluate n_points: int Number of evenly-spaced parameter locations at which to evaluate the edge curve. Default: 10 Returns ------- numpy.ndarray 2-D array of size :math:`n_\text{points} \times 3` """ P = self.get_control_point_array() if edge == SurfaceEdge.v1: return np.array(nurbs_surf_eval_iso_v(P, self.weights, self.knots_u, self.knots_v, n_points, 1.0)) elif edge == SurfaceEdge.v0: return np.array(nurbs_surf_eval_iso_v(P, self.weights, self.knots_u, self.knots_v, n_points, 0.0)) elif edge == SurfaceEdge.u1: return np.array(nurbs_surf_eval_iso_u(P, self.weights, self.knots_u, self.knots_v, 1.0, n_points)) elif edge == SurfaceEdge.u0: return np.array(nurbs_surf_eval_iso_u(P, self.weights, self.knots_u, self.knots_v, 0.0, n_points)) else: raise ValueError(f"No edge called {edge}")
[docs] def get_first_derivs_along_edge(self, edge: SurfaceEdge, n_points: int = 10, perp: bool = True) -> np.ndarray: r""" Evaluates the parallel or perpendicular derivative along a surface edge at ``n_points`` parameter locations. The derivative represents either :math:`\frac{\partial \mathbf{S}(u,v)}{\partial u}` or :math:`\frac{\partial \mathbf{S}(u,v)}{\partial v}` depending on which edge is selected and which value is assigned to ``perp``. Parameters ---------- edge: SurfaceEdge Edge along which to evaluate n_points: int Number of evenly-spaced parameter locations at which to evaluate the derivative. Default: 10 perp: bool Whether to evaluate the cross-derivative. If ``False``, the derivative along the parameter direction parallel to the edge will be evaluated instead. Default: ``True`` Returns ------- numpy.ndarray 2-D array of size :math:`n_\text{points} \times 3` """ P = self.get_control_point_array() if edge == SurfaceEdge.v1: return np.array(nurbs_surf_dsdv_iso_v( P, self.weights, self.knots_u, self.knots_v, n_points, 1.0)) if perp else np.array( nurbs_surf_dsdu_iso_v(P, self.weights, self.knots_u, self.knots_v, n_points, 1.0)) elif edge == SurfaceEdge.v0: return np.array(nurbs_surf_dsdv_iso_v( P, self.weights, self.knots_u, self.knots_v, n_points, 0.0)) if perp else np.array( nurbs_surf_dsdu_iso_v(P, self.weights, self.knots_u, self.knots_v, n_points, 0.0)) elif edge == SurfaceEdge.u1: return np.array(nurbs_surf_dsdu_iso_u( P, self.weights, self.knots_u, self.knots_v, 1.0, n_points)) if perp else np.array( nurbs_surf_dsdv_iso_u(P, self.weights, self.knots_u, self.knots_v, 1.0, n_points)) elif edge == SurfaceEdge.u0: return np.array(nurbs_surf_dsdu_iso_u( P, self.weights, self.knots_u, self.knots_v, 0.0, n_points)) if perp else np.array( nurbs_surf_dsdv_iso_u(P, self.weights, self.knots_u, self.knots_v, 0.0, n_points)) else: raise ValueError(f"No edge called {edge}")
[docs] def get_second_derivs_along_edge(self, edge: SurfaceEdge, n_points: int = 10, perp: bool = True) -> np.ndarray: r""" Evaluates the parallel or perpendicular second derivative along a surface edge at ``n_points`` parameter locations. The derivative represents either :math:`\frac{\partial^2 \mathbf{S}(u,v)}{\partial u^2}` or :math:`\frac{\partial^2 \mathbf{S}(u,v)}{\partial v^2}` depending on which edge is selected and which value is assigned to ``perp``. Parameters ---------- edge: SurfaceEdge Edge along which to evaluate n_points: int Number of evenly-spaced parameter locations at which to evaluate the second derivative. Default: 10 perp: bool Whether to evaluate the cross-derivative. If ``False``, the second derivative along the parameter direction parallel to the edge will be evaluated instead. Default: ``True`` Returns ------- numpy.ndarray 2-D array of size :math:`n_\text{points} \times 3` """ P = self.get_control_point_array() if edge == SurfaceEdge.v1: return np.array(nurbs_surf_d2sdv2_iso_v( P, self.weights, self.knots_u, self.knots_v, n_points, 1.0)) if perp else np.array( nurbs_surf_d2sdu2_iso_v(P, self.weights, self.knots_u, self.knots_v, n_points, 1.0)) elif edge == SurfaceEdge.v0: return np.array(nurbs_surf_d2sdv2_iso_v( P, self.weights, self.knots_u, self.knots_v, n_points, 0.0)) if perp else np.array( nurbs_surf_d2sdu2_iso_v(P, self.weights, self.knots_u, self.knots_v, n_points, 0.0)) elif edge == SurfaceEdge.u1: return np.array(nurbs_surf_d2sdu2_iso_u( P, self.weights, self.knots_u, self.knots_v, 1.0, n_points)) if perp else np.array( nurbs_surf_d2sdv2_iso_u(P, self.weights, self.knots_u, self.knots_v, 1.0, n_points)) elif edge == SurfaceEdge.u0: return np.array(nurbs_surf_d2sdu2_iso_u( P, self.weights, self.knots_u, self.knots_v, 0.0, n_points)) if perp else np.array( nurbs_surf_d2sdv2_iso_u(P, self.weights, self.knots_u, self.knots_v, 0.0, n_points)) else: raise ValueError(f"No edge called {edge}")
[docs] def verify_g0(self, other: 'NURBSSurface', surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge, n_points: int = 10): """ Verifies that two NURBS Surfaces are G0 continuous along their shared edge""" self_edge = self.get_edge(surface_edge, n_points=n_points) other_edge = other.get_edge(other_surface_edge, n_points=n_points) assert np.array_equal(self_edge, other_edge)
[docs] def verify_g1(self, other: "NURBSSurface", surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge, n_points: int = 10): """ Verifies that two NURBSSurfaces are G1 continuous along their shared edge """ # Get the first derivatives at the boundary and perpendicular to the boundary for each surface, # evaluated at "n_points" locations along the boundary self_perp_edge_derivs = self.get_first_derivs_along_edge(surface_edge, n_points=n_points, perp=True) other_perp_edge_derivs = other.get_first_derivs_along_edge(other_surface_edge, n_points=n_points, perp=True) self_perp_edge_derivs[np.absolute(self_perp_edge_derivs) < 1e-6] = 0.0 other_perp_edge_derivs[np.absolute(other_perp_edge_derivs) < 1e-6] = 0.0 # Initialize an array of ratios of magnitude of the derivative values at each point for both sides # of the boundary magnitude_ratios = [] # Loop over each pair of cross-derivatives evaluated along the boundary for point_idx, (self_perp_edge_deriv, other_perp_edge_deriv) in enumerate(zip( self_perp_edge_derivs, other_perp_edge_derivs)): # Ensure that each derivative vector has the same direction along the boundary for each surface try: assert np.allclose( np.nan_to_num(self_perp_edge_deriv / np.linalg.norm(self_perp_edge_deriv)), np.nan_to_num(other_perp_edge_deriv / np.linalg.norm(other_perp_edge_deriv)) ) except AssertionError: assert np.allclose( np.nan_to_num(self_perp_edge_deriv / np.linalg.norm(self_perp_edge_deriv)), np.nan_to_num(-other_perp_edge_deriv / np.linalg.norm(other_perp_edge_deriv)) ) # Compute the ratio of the magnitudes for each derivative vector along the boundary for each surface. # These will be compared at the end. #print(f"{self_perp_edge_deriv=},{other_perp_edge_deriv=}") np.seterr(divide='ignore', invalid='ignore') with np.errstate(divide="ignore"): magnitude_ratios.append(np.nan_to_num(self_perp_edge_deriv / other_perp_edge_deriv, nan=0)) #print("Rational",f"{magnitude_ratios=}") # Assert that the first derivatives along each boundary are proportional current_f = None for magnitude_ratio in magnitude_ratios: for dxdydz_ratio in magnitude_ratio: if np.any(np.isinf(dxdydz_ratio)) or np.any(np.isnan(dxdydz_ratio)) or np.any(dxdydz_ratio == 0.0): continue if current_f is None: current_f = dxdydz_ratio continue assert np.all(np.isclose(dxdydz_ratio, current_f))
[docs] def verify_g2(self, other: "NURBSSurface", surface_edge: SurfaceEdge, other_surface_edge: SurfaceEdge, n_points: int = 10): """ Verifies that two NURBSSurfaces are G2 continuous along their shared edge """ # Get the first derivatives at the boundary and perpendicular to the boundary for each surface, # evaluated at "n_points" locations along the boundary self_perp_edge_derivs = self.get_second_derivs_along_edge(surface_edge, n_points=n_points, perp=True) other_perp_edge_derivs = other.get_second_derivs_along_edge(other_surface_edge, n_points=n_points, perp=True) print(f"{self_perp_edge_derivs=},{other_perp_edge_derivs=}") self_perp_edge_derivs[np.absolute(self_perp_edge_derivs) < 1e-6] = 0.0 other_perp_edge_derivs[np.absolute(other_perp_edge_derivs) < 1e-6] = 0.0 ratios_other_self = other_perp_edge_derivs / self_perp_edge_derivs #print(f"{ratios_other_self=}") #print(f"{self_perp_edge_derivs=},{other_perp_edge_derivs=}") # Initialize an array of ratios of magnitude of the derivative values at each point for both sides # of the boundary magnitude_ratios = [] # Loop over each pair of cross-derivatives evaluated along the boundary for point_idx, (self_perp_edge_deriv, other_perp_edge_deriv) in enumerate(zip( self_perp_edge_derivs, other_perp_edge_derivs)): # Ensure that each derivative vector has the same direction along the boundary for each surface assert np.allclose( np.nan_to_num(self_perp_edge_deriv / np.linalg.norm(self_perp_edge_deriv)), np.nan_to_num(other_perp_edge_deriv / np.linalg.norm(other_perp_edge_deriv)) ) # Compute the ratio of the magnitudes for each derivative vector along the boundary for each surface. # These will be compared at the end. with np.errstate(divide="ignore"): magnitude_ratios.append(self_perp_edge_deriv / other_perp_edge_deriv) # Assert that the second derivatives along each boundary are proportional current_f = None for magnitude_ratio in magnitude_ratios: for dxdydz_ratio in magnitude_ratio: if np.any(np.isinf(dxdydz_ratio)) or np.any(np.isnan(dxdydz_ratio)) or np.any(dxdydz_ratio == 0.0): continue if current_f is None: current_f = dxdydz_ratio continue assert np.all(np.isclose(dxdydz_ratio, current_f))
[docs] def get_u_or_v_given_uvxyz(self, u: float = None, v: float = None, uv_guess: float = 0.5, x: Length = None, y: Length = None, z: Length = None): """ Computes one parametric value given the other and a specified :math:`x`-, :math:`y`-, or :math:`z`-location. As an example, given a :obj:`~aerocaps.geom.surfaces.NURBSSurface` object assigned to the variable ``surf``, the :math:`u`-parameter corresponding to :math:`y=1.4` along the :math:`v=0.8` isoparametric curve can be computed using .. code-block:: python u = surf.get_u_or_v_given_uvxyz(v=0.8, y=1.4) Note that the inputs are keyword arguments to avoid having to specify ``None`` for each of the arguments not used. Parameters ---------- u: float or None Value of :math:`u` to solve for or specify. If left as ``None``, this parameter will be solved for. If ``None``, :math:`v` must be specified. Default: ``None`` v: float or None Value of :math:`v` to solve for or specify. If left as ``None``, this parameter will be solved for. If ``None``, :math:`u` must be specified. Default: ``None`` uv_guess: float Starting guess for the unsolved :math:`u` or :math:`v` parameter. Default: ``0.5`` x: Length or None :math:`x`-location corresponding to the :math:`u` or :math:`v` parameter to be solved. If this value is outside the surface geometry, the root-finder will fail and an error will be raised. If unspecified, either :math:`y` or :math:`z` must be specified. Default: ``None`` y: Length or None :math:`y`-location corresponding to the :math:`u` or :math:`v` parameter to be solved. If this value is outside the surface geometry, the root-finder will fail and an error will be raised. If unspecified, either :math:`x` or :math:`z` must be specified. Default: ``None`` z: Length or None :math:`z`-location corresponding to the :math:`u` or :math:`v` parameter to be solved. If this value is outside the surface geometry, the root-finder will fail and an error will be raised. If unspecified, either :math:`x` or :math:`y` must be specified. Default: ``None`` Returns ------- float The value of :math:`u` if :math:`v` is specified or :math:`v` if :math:`u` is specified """ # Validate inputs if u is None and v is None or (u is not None and v is not None): raise ValueError("Must specify exactly one of either u or v") xyz_spec = (x is not None, y is not None, z is not None) if len([xyz for xyz in xyz_spec if xyz]) != 1: raise ValueError("Must specify exactly one of x, y, or z") if x is not None: xyz, xyz_val = "x", x.m elif y is not None: xyz, xyz_val = "y", y.m elif z is not None: xyz, xyz_val = "z", z.m else: raise ValueError("Did not detect an x, y, or z input") def root_find_func_u(u_current): point = self.evaluate_point3d(u_current, v) return np.array([getattr(point, xyz).m - xyz_val]) def root_find_func_v(v_current): point = self.evaluate_point3d(u, v_current) return np.array([getattr(point, xyz).m - xyz_val]) if v is not None: return fsolve(root_find_func_u, x0=np.array([uv_guess]))[0] if u is not None: return fsolve(root_find_func_v, x0=np.array([uv_guess]))[0] raise ValueError("Did not detect a u or v input")
[docs] def split_at_u(self, u0: float) -> ("NURBSSurface", "NURBSSurface"): """ Splits the NURBS surface at :math:`u=u_0` along the :math:`v`-parametric direction. """ if self.has_internal_knots("u"): raise NotImplementedError( "Curve splitting perpendicular to an edge with internal knots is not yet implemented" ) Pw = self.get_homogeneous_control_points() def de_casteljau(i: int, j: int, k: int) -> np.ndarray: """ Based on https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm. Recursive algorithm where the base case is just the value of the ith original control point. Parameters ---------- i: int Lower index j: int Upper index k: int Control point row index Returns ------- np.ndarray A one-dimensional array containing the :math:`x` and :math:`y` values of a control point evaluated at :math:`(i,j)` for a Bézier curve split at the parameter value ``t_split`` """ if j == 0: return Pw[i, k, :] return de_casteljau(i, j - 1, k) * (1 - u0) + de_casteljau(i + 1, j - 1, k) * u0 bez_surf_split_1_Pw = np.array([ [de_casteljau(i=0, j=i, k=k) for i in range(self.n_points_u)] for k in range(self.n_points_v) ]) bez_surf_split_2_Pw = np.array([ [de_casteljau(i=i, j=self.degree_u - i, k=k) for i in range(self.n_points_u)] for k in range(self.n_points_v) ]) transposed_Pw_1 = np.transpose(bez_surf_split_1_Pw, (1, 0, 2)) transposed_Pw_2 = np.transpose(bez_surf_split_2_Pw, (1, 0, 2)) P1, w1 = self.project_homogeneous_control_points(transposed_Pw_1) P2, w2 = self.project_homogeneous_control_points(transposed_Pw_2) return ( NURBSSurface(P1, self.knots_u, self.knots_v, w1), NURBSSurface(P2, self.knots_u, self.knots_v, w2) )
[docs] def split_at_v(self, v0: float) -> ("NURBSSurface", "NURBSSurface"): """ Splits the NURBS surface at :math:`v=v_0` along the :math:`u`-parametric direction. """ if self.has_internal_knots("v"): raise NotImplementedError( "Curve splitting perpendicular to an edge with internal knots is not yet implemented" ) Pw = self.get_homogeneous_control_points() def de_casteljau(i: int, j: int, k: int) -> np.ndarray: """ Based on https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm. Recursive algorithm where the base case is just the value of the ith original control point. Parameters ---------- i: int Lower index j: int Upper index k: int Control point row index Returns ------- np.ndarray A one-dimensional array containing the :math:`x` and :math:`y` values of a control point evaluated at :math:`(i,j)` for a Bézier curve split at the parameter value ``t_split`` """ if j == 0: return Pw[k, i, :] return de_casteljau(i, j - 1, k) * (1 - v0) + de_casteljau(i + 1, j - 1, k) * v0 bez_surf_split_1_Pw = np.array([ [de_casteljau(i=0, j=i, k=k) for i in range(self.n_points_v)] for k in range(self.n_points_u) ]) bez_surf_split_2_Pw = np.array([ [de_casteljau(i=i, j=self.degree_v - i, k=k) for i in range(self.n_points_v)] for k in range(self.n_points_u) ]) P1, w1 = self.project_homogeneous_control_points(bez_surf_split_1_Pw) P2, w2 = self.project_homogeneous_control_points(bez_surf_split_2_Pw) return ( NURBSSurface(P1, self.knots_u, self.knots_v, w1), NURBSSurface(P2, self.knots_u, self.knots_v, w2) )
[docs] def transform(self, **transformation_kwargs) -> "NURBSSurface": """ Creates a transformed copy of the surface by transforming each of the control points Parameters ---------- transformation_kwargs Keyword arguments passed to :obj:`~aerocaps.geom.transformation.Transformation3D` Returns ------- BSplineSurface Transformed surface """ transformation = Transformation3D(**transformation_kwargs) initial_control_points = self.get_control_point_array() return NURBSSurface( np.array([transformation.transform(p_arr) for p_arr in initial_control_points]), weights=deepcopy(self.weights), knots_u=deepcopy(self.knots_u), knots_v=deepcopy(self.knots_v), name=self.name, construction=self.construction )
[docs] def generate_control_point_net(self) -> (typing.List[Point3D], typing.List[Line3D]): """ Generates a list of :obj:`~aerocaps.geom.point.Point3D` and :obj:`~aerocaps.geom.curves.Line3D` objects representing the NURBS surface's control points and connections between them Returns ------- typing.List[Point3D], typing.List[Line3D] Control points and lines between adjacent control points in flattened lists """ control_points = self.get_control_point_array() points = [] lines = [] for i in range(self.n_points_u): for j in range(self.n_points_v): points.append(Point3D.from_array(control_points[i, j, :])) for i in range(self.n_points_u - 1): for j in range(self.n_points_v - 1): point_obj_1 = Point3D.from_array(control_points[i, j, :]) point_obj_2 = Point3D.from_array(control_points[i + 1, j, :]) point_obj_3 = Point3D.from_array(control_points[i, j + 1, :]) line_1 = Line3D(p0=point_obj_1, p1=point_obj_2) line_2 = Line3D(p0=point_obj_1, p1=point_obj_3) lines.extend([line_1, line_2]) if i < self.n_points_u - 2 and j < self.n_points_v - 2: continue point_obj_4 = Point3D.from_array(control_points[i + 1, j + 1, :]) line_3 = Line3D(p0=point_obj_3, p1=point_obj_4) line_4 = Line3D(p0=point_obj_2, p1=point_obj_4) lines.extend([line_3, line_4]) return points, lines
[docs] def plot_surface(self, plot: pv.Plotter, Nu: int = 50, Nv: int = 50, **mesh_kwargs): """ Plots the NURBS surface using the `pyvista <https://pyvista.org/>`_ library Parameters ---------- plot: :obj:`pyvista.Plotter` instance Nu: int Number of points to evaluate in the :math:`u`-parametric direction. Default: ``50`` Nv: int Number of points to evaluate in the :math:`v`-parametric direction. Default: ``50`` mesh_kwargs: Keyword arguments to pass to :obj:`pyvista.Plotter.add_mesh` Returns ------- pyvista.core.pointset.StructuredGrid The evaluated NURBS surface """ XYZ = self.evaluate_grid(Nu, Nv) grid = pv.StructuredGrid(XYZ[:, :, 0], XYZ[:, :, 1], XYZ[:, :, 2]) plot.add_mesh(grid, **mesh_kwargs) return grid
[docs] def plot_control_point_mesh_lines(self, plot: pv.Plotter, **line_kwargs) -> pv.Actor: """ Plots the network of lines connecting the NURBS surface control points using the `pyvista <https://pyvista.org/>`_ library Parameters ---------- plot: :obj:`pyvista.Plotter` instance line_kwargs: Keyword arguments to pass to the :obj:`pyvista.Plotter.add_lines` Returns ------- pv.Actor The lines actor """ _, line_objs = self.generate_control_point_net() line_arr = np.array([[line_obj.p0.as_array(), line_obj.p1.as_array()] for line_obj in line_objs]) line_arr = line_arr.reshape((len(line_objs) * 2, 3)) line_actor = plot.add_lines(line_arr, **line_kwargs) return line_actor
[docs] def plot_control_points(self, plot: pv.Plotter, **point_kwargs) -> pv.Actor: """ Plots the NURBS surface control points using the `pyvista <https://pyvista.org/>`_ library Parameters ---------- plot: :obj:`pyvista.Plotter` instance point_kwargs: Keyword arguments to pass to the :obj:`pyvista.Plotter.add_points` Returns ------- pv.Actor The points actor """ point_objs, _ = self.generate_control_point_net() point_arr = np.array([point_obj.as_array() for point_obj in point_objs]) point_actor = plot.add_points(point_arr, **point_kwargs) return point_actor
def __repr__(self): return (f"{self.name}: {self.degree_u} x {self.degree_v} {self.__class__.__name__} " f"({self.n_points_u} x {self.n_points_v} control points)")
[docs] class TrimmedSurface(Surface):
[docs] def __init__(self, untrimmed_surface: Surface, outer_boundary: CompositeCurve3D, outer_boundary_para: CompositeCurve3D, outer_curve_on_parametric_surf_para: CurveOnParametricSurface, inner_boundaries: typing.List[CompositeCurve3D] = None, inner_boundaries_para: typing.List[CurveOnParametricSurface] = None, name: str = "TrimmedSurface", construction: bool = False): """ Parameters ---------- untrimmed_surface: Surface outer_boundary: CompositeCurve3D outer_boundary_para: CompositeCurve3D outer_curve_on_parametric_surf_para: CurveOnParametricSurface inner_boundaries: typing.List[CompositeCurve3D] or None inner_boundaries_para: typing.List[CurveOnParametricSurface] or None name: str Name of the geometric object. May be re-assigned a unique name when added to a :obj:`~aerocaps.geom.geometry_container.GeometryContainer` construction: bool Whether this is a geometry used only for construction of other geometries. If ``True``, this geometry will not be exported or plotted. Default: ``False`` """ self.untrimmed_surface = untrimmed_surface self.outer_boundary = outer_boundary self.outer_boundary_para = outer_boundary_para self.outer_curve_on_parametric_surf_para = outer_curve_on_parametric_surf_para self.inner_boundaries = inner_boundaries self.inner_boundaries_para = inner_boundaries_para super().__init__(name=name, construction=construction)
@classmethod def from_planar_boundary_curves(cls, outer_boundary: CompositeCurve3D) -> "TrimmedSurface": composite_para, planar_surf = cls._get_envelope(outer_boundary) curve_on_parametric_surface = CurveOnParametricSurface( planar_surf, composite_para, outer_boundary ) # Create the trimmed surface object return cls(planar_surf, outer_boundary, composite_para, curve_on_parametric_surface) @staticmethod def _get_envelope(outer_curves: CompositeCurve3D) -> (CompositeCurve3D, BezierSurface): control_point_loop = [] for c_idx, c in enumerate(outer_curves.ordered_curves): if c_idx == 0: control_point_loop.extend(c.control_points) else: control_point_loop.extend(c.control_points[1:]) loop_array = np.array([p.as_array() for p in control_point_loop]) parametric_curves = [] # Need to convert to 2-D to use shapely. Get the coordinate system of the plane containing the points # using cross products of vectors described by the points v1, v2 = None, None v3 = Vector3D(p0=Point3D.from_array(np.array([0.0, 0.0, 0.0])), p1=Point3D.from_array(np.array([0.0, 0.0, 0.0]))) t = 0.1 while True: if not v3.is_zero_vector(): break if t > 0.9: break v1 = Vector3D( outer_curves.ordered_curves[0].evaluate_point3d(0.0), outer_curves.ordered_curves[0].evaluate_point3d(t) ) v2 = Vector3D( outer_curves.ordered_curves[0].evaluate_point3d(0.0), outer_curves.ordered_curves[1].evaluate_point3d(t) if len( outer_curves.ordered_curves) > 1 else outer_curves.ordered_curves[0].evaluate_point3d(t + 0.1) ) v3 = v1.cross(v2) t += 0.1 else: raise ValueError("Could not compute curve loop plane from input curves") v4 = v1.cross(v3) # The coordinate system is now fully described by v1, v3, and v4. v1 and v4 are the in-plane components, # while v3 is the out-of-plane component. The origin of this coordinate system is at control_point_loop[0]. loop_array_transformed = transform_points_into_coordinate_system( loop_array, [v1, v4, v3], [IHat3D(), JHat3D(), KHat3D()] ) # Make sure that all the curves are coplanar if not all([np.isclose(z, loop_array_transformed[0, 2]) for z in loop_array_transformed[1:, 2]]): raise ValueError("Curves are not all coplanar!") loop_array_2d = loop_array_transformed[:, :2] z_plane = loop_array_transformed[0, 2] # Create the polygon and find a point representing the center of the polygon while guaranteed to be inside # the polygon polygon = shapely.Polygon(loop_array_2d) envelope_2d = np.array(shapely.envelope(polygon).exterior.coords) x_min, x_max = envelope_2d[:, 0].min(), envelope_2d[:, 0].max() y_min, y_max = envelope_2d[:, 1].min(), envelope_2d[:, 1].max() dx, dy = (x_max - x_min), (y_max - y_min) ds = max(dx, dy) envelope_2d = np.array([ [x_min, y_min], [x_min + ds, y_min], [x_min + ds, y_min + ds], [x_min, y_min + ds], [x_min, y_min] ]) # envelope_2d[:,] # Get parametric curves in the plane defined by the envelope for each curve in the ordered curve list for curve in outer_curves.ordered_curves: cps_transformed = transform_points_into_coordinate_system( curve.get_control_point_array(), [v1, v4, v3], [IHat3D(), JHat3D(), KHat3D()] ) cps_x = cps_transformed[:, 0] cps_y = cps_transformed[:, 1] u = [(cp_x - x_min) / ds for cp_x in cps_x] v = [(cp_y - y_min) / ds for cp_y in cps_y] # u = [cp_x - x_min for cp_x in cps_x] # v = [cp_y - y_min for cp_y in cps_y] uv = np.array([u, v]).T uv0 = np.column_stack((uv, np.zeros(uv.shape[0]))) if isinstance(curve, Line3D): parametric_curve = curve.__class__(p0=Point3D.from_array(uv0[0, :]), p1=Point3D.from_array(uv0[1, :])) elif isinstance(curve, BezierCurve3D): parametric_curve = curve.__class__(uv0) elif isinstance(curve, BSplineCurve3D): parametric_curve = curve.__class__(uv0, curve.knot_vector, curve.degree) elif isinstance(curve, RationalBezierCurve3D): # w = np.array([np.min(curve.weights) + ds * (np.max(curve.weights) - np.min(curve.weights)) for weight in curve.weights]) parametric_curve = curve.__class__(uv0, curve.weights * ds) # * ds elif isinstance(curve, NURBSCurve3D): parametric_curve = curve.__class__(uv0, curve.weights, curve.knot_vector, curve.degree) else: raise ValueError(f"Invalid curve type {type(curve)}") parametric_curves.append(parametric_curve) envelope_3d = np.column_stack((envelope_2d, z_plane * np.ones(envelope_2d.shape[0]))) # Transform the newly created envelope back into the original coordinate system reverse_transformed_envelope_3d = transform_points_into_coordinate_system( envelope_3d, [IHat3D(), JHat3D(), KHat3D()], [v1, v4, v3] ) # Create a planar rectangular surface from the transformed points pa = Point3D.from_array(reverse_transformed_envelope_3d[0, :]) pb = Point3D.from_array(reverse_transformed_envelope_3d[1, :]) pc = Point3D.from_array(reverse_transformed_envelope_3d[2, :]) pd = Point3D.from_array(reverse_transformed_envelope_3d[3, :]) planar_surf = BezierSurface([[pa, pd], [pb, pc]]) return CompositeCurve3D(parametric_curves), planar_surf
[docs] def transform(self, **transformation_kwargs) -> "TrimmedSurface": """ Creates a transformed copy of the surface by transforming both the outer boundary in geometric space and the untrimmed surface. The parameteric trimming curves are left unmodified. Parameters ---------- transformation_kwargs Keyword arguments passed to :obj:`~aerocaps.geom.transformation.Transformation3D` Returns ------- TrimmedSurface Transformed surface """ return TrimmedSurface( untrimmed_surface=self.untrimmed_surface.transform(**transformation_kwargs), outer_boundary=self.outer_boundary.transform(**transformation_kwargs), outer_boundary_para=self.outer_boundary_para, outer_curve_on_parametric_surf_para=self.outer_curve_on_parametric_surf_para, inner_boundaries=[curve.transform(**transformation_kwargs) for curve in self.inner_boundaries] if self.inner_boundaries is not None else None, inner_boundaries_para=self.inner_boundaries_para if self.inner_boundaries_para is not None else None, name=self.name, construction=self.construction )
def evaluate(self, Nt: int) -> np.ndarray: if self.inner_boundaries is not None: raise NotImplementedError("Evaluation not yet implemented for trimmed surfaces with inner loops") uv_locs = self.outer_boundary_para.evaluate(Nt)[:, :2] points, lines = concave_hull(uv_locs) surf_points = np.array([self.untrimmed_surface.evaluate(p[0], p[1]) for p in points]) return surf_points, lines
[docs] def plot_surface(self, plot: pv.Plotter, Nt: int = 100, **mesh_kwargs): """ Plots the trimmed surface using the `pyvista <https://pyvista.org/>`_ library Parameters ---------- plot: :obj:`pyvista.Plotter` instance Nt: int Number of points to evaluate on each boundary curve. Default: ``100`` mesh_kwargs: Keyword arguments to pass to :obj:`pyvista.Plotter.add_mesh` Returns ------- pyvista.core.pointset.StructuredGrid The evaluated rational Bézier surface """ surf_points, lines = self.evaluate(Nt) faces = np.hstack(np.insert(lines, 0, 3, axis=1)) mesh = pv.PolyData(surf_points, faces=faces) plot.add_mesh(mesh, **mesh_kwargs) return mesh
[docs] def to_iges(self, *args, **kwargs) -> typing.List[aerocaps.iges.entity.IGESEntity]: # Compile the list of entities K1 = len(self.outer_boundary.ordered_curves) K2 = K1 + len(self.outer_boundary_para.ordered_curves) entities = [curve.to_iges() for curve in self.outer_boundary.ordered_curves] entities.extend([curve.to_iges() for curve in self.outer_boundary_para.ordered_curves]) entities.append(self.outer_boundary.to_iges(entities[0:K1])) entities.append(self.outer_boundary_para.to_iges(entities[K1:K2])) entities.append(self.untrimmed_surface.to_iges()) entities.append(self.outer_curve_on_parametric_surf_para.to_iges(entities[K2 + 2], entities[K2 + 1], entities[K2])) entities.append(aerocaps.iges.surfaces.TrimmedSurfaceIGES( entities[K2 + 2], entities[K2 + 3], outer_boundary_is_boundary_of_surface=True )) return entities