diff --git a/src/ansys/geometry/core/primitives/__init__.py b/src/ansys/geometry/core/primitives/__init__.py index 1dc0ddf827..a05fdc90a0 100644 --- a/src/ansys/geometry/core/primitives/__init__.py +++ b/src/ansys/geometry/core/primitives/__init__.py @@ -1,10 +1,16 @@ """Provides the PyGeometry ``primitives`` subpackage.""" from ansys.geometry.core.primitives.circle import Circle, CircleEvaluation -from ansys.geometry.core.primitives.cone import Cone -from ansys.geometry.core.primitives.cylinder import Cylinder +from ansys.geometry.core.primitives.cone import Cone, ConeEvaluation +from ansys.geometry.core.primitives.cylinder import Cylinder, CylinderEvaluation from ansys.geometry.core.primitives.ellipse import Ellipse, EllipseEvaluation -from ansys.geometry.core.primitives.line import Line -from ansys.geometry.core.primitives.sphere import Sphere -from ansys.geometry.core.primitives.surface_evaluation import ParamUV, SurfaceEvaluation +from ansys.geometry.core.primitives.line import Line, LineEvaluation +from ansys.geometry.core.primitives.parameterization import ( + Parameterization, + ParamForm, + ParamType, + ParamUV, +) +from ansys.geometry.core.primitives.sphere import Sphere, SphereEvaluation +from ansys.geometry.core.primitives.surface_evaluation import SurfaceEvaluation from ansys.geometry.core.primitives.torus import Torus diff --git a/src/ansys/geometry/core/primitives/circle.py b/src/ansys/geometry/core/primitives/circle.py index 63319de8bc..7a5939241c 100644 --- a/src/ansys/geometry/core/primitives/circle.py +++ b/src/ansys/geometry/core/primitives/circle.py @@ -8,6 +8,12 @@ from ansys.geometry.core.math import UNITVECTOR3D_X, UNITVECTOR3D_Z, Point3D, UnitVector3D, Vector3D from ansys.geometry.core.misc import Accuracy, Distance from ansys.geometry.core.primitives.curve_evaluation import CurveEvaluation +from ansys.geometry.core.primitives.parameterization import ( + Interval, + Parameterization, + ParamForm, + ParamType, +) from ansys.geometry.core.typing import Real, RealSequence @@ -99,12 +105,36 @@ def __eq__(self, other: "Circle") -> bool: and self._axis == other._axis ) - def evaluate(self, parameter: float) -> "CircleEvaluation": - """Evaluate the circle at the given parameter.""" + def evaluate(self, parameter: Real) -> "CircleEvaluation": + """ + Evaluate the circle at the given parameter. + + Parameters + ---------- + parameter : Real + The parameter at which to evaluate the circle. + + Returns + ------- + CircleEvaluation + The resulting evaluation. + """ return CircleEvaluation(self, parameter) def project_point(self, point: Point3D) -> "CircleEvaluation": - """Project a point onto the circle and return its ``CircleEvaluation``.""" + """ + Project a point onto the circle and return its ``CircleEvaluation``. + + Parameters + ---------- + point : Point3D + The point to project onto the circle. + + Returns + ------- + CircleEvaluation + The resulting evaluation. + """ origin_to_point = point - self.origin dir_in_plane = UnitVector3D.from_points( Point3D([0, 0, 0]), origin_to_point - ((origin_to_point * self.dir_z) * self.dir_z) @@ -116,13 +146,38 @@ def project_point(self, point: Point3D) -> "CircleEvaluation": return CircleEvaluation(self, t) def is_coincident_circle(self, other: "Circle") -> bool: - """Determine if this circle is coincident with another.""" + """ + Determine if this circle is coincident with another. + + Parameters + ---------- + other : Circle + The circle to determine coincidence with. + + Returns + ------- + bool + Returns true if this circle is coincident with the other. + """ return ( Accuracy.length_is_equal(self.radius.m, other.radius.m) and self.origin == other.origin and self.dir_z == other.dir_z ) + def get_parameterization(self) -> Parameterization: + """ + The parameter of a circle specifies the clockwise angle around the axis + (right hand corkscrew law), with a zero parameter at `dir_x` and a period + of 2*pi. + + Returns + ------- + Parameterization + Information about how a circle is parameterized. + """ + return Parameterization(ParamForm.PERIODIC, ParamType.CIRCULAR, Interval(0, 2 * np.pi)) + class CircleEvaluation(CurveEvaluation): """ @@ -132,11 +187,12 @@ class CircleEvaluation(CurveEvaluation): ---------- circle: ~ansys.geometry.core.primitives.circle.Circle The ``Circle`` object to be evaluated. - parameter: float, int + parameter: Real The parameter at which the ``Circle`` evaluation is requested. """ def __init__(self, circle: Circle, parameter: Real) -> None: + """``CircleEvaluation`` class constructor.""" self._circle = circle self._parameter = parameter @@ -151,7 +207,14 @@ def parameter(self) -> Real: return self._parameter def position(self) -> Point3D: - """The position of the evaluation.""" + """ + The position of the evaluation. + + Returns + ------- + Point3D + The point that lies on the circle at this evaluation. + """ return ( self.circle.origin + ((self.circle.radius * np.cos(self.parameter)) * self.circle.dir_x).m @@ -159,23 +222,53 @@ def position(self) -> Point3D: ) def tangent(self) -> UnitVector3D: - """The tangent of the evaluation.""" + """ + The tangent of the evaluation. + + Returns + ------- + UnitVector3D + The tangent unit vector to the circle at this evaluation. + """ return ( np.cos(self.parameter) * self.circle.dir_y - np.sin(self.parameter) * self.circle.dir_x ) def first_derivative(self) -> Vector3D: - """The first derivative of the evaluation.""" + """ + The first derivative of the evaluation. The first derivative is in the direction of the + tangent and has a magnitude equal to the velocity (rate of change of position) at that + point. + + Returns + ------- + Vector3D + The first derivative of this evaluation. + """ return self.circle.radius.m * ( np.cos(self.parameter) * self.circle.dir_y - np.sin(self.parameter) * self.circle.dir_x ) def second_derivative(self) -> Vector3D: - """The second derivative of the evaluation.""" + """ + The second derivative of the evaluation. + + Returns + ------- + Vector3D + The second derivative of this evaluation. + """ return -self.circle.radius.m * ( np.cos(self.parameter) * self.circle.dir_x + np.sin(self.parameter) * self.circle.dir_y ) def curvature(self) -> Real: - """The curvature of the evaluation.""" + """ + The curvature of the circle. + + Returns + ------- + Real + The curvature of the circle. + """ return 1 / np.abs(self.circle.radius.m) diff --git a/src/ansys/geometry/core/primitives/cone.py b/src/ansys/geometry/core/primitives/cone.py index 8389967aa1..d4098fbca9 100644 --- a/src/ansys/geometry/core/primitives/cone.py +++ b/src/ansys/geometry/core/primitives/cone.py @@ -9,7 +9,14 @@ from ansys.geometry.core.math import UNITVECTOR3D_X, UNITVECTOR3D_Z, Point3D, UnitVector3D, Vector3D from ansys.geometry.core.misc import Angle, Distance from ansys.geometry.core.primitives.line import Line -from ansys.geometry.core.primitives.surface_evaluation import ParamUV, SurfaceEvaluation +from ansys.geometry.core.primitives.parameterization import ( + Interval, + Parameterization, + ParamForm, + ParamType, + ParamUV, +) +from ansys.geometry.core.primitives.surface_evaluation import SurfaceEvaluation from ansys.geometry.core.typing import Real, RealSequence @@ -123,11 +130,35 @@ def __eq__(self, other: "Cone") -> bool: ) def evaluate(self, parameter: ParamUV) -> "ConeEvaluation": - """Evaluate the cone at the given parameters.""" + """ + Evaluate the cone at the given parameters. + + Parameters + ---------- + parameter : ParamUV + The parameters (u,v) at which to evaluate the cone. + + Returns + ------- + ConeEvaluation + The resulting evaluation. + """ return ConeEvaluation(self, parameter) def project_point(self, point: Point3D) -> "ConeEvaluation": - """Project a point onto the cone and return its ``ConeEvaluation``.""" + """ + Project a point onto the cone and return its ``ConeEvaluation``. + + Parameters + ---------- + point : Point3D + The point to project onto the cone. + + Returns + ------- + ConeEvaluation + The resulting evaluation. + """ u = np.arctan2(self.dir_y.dot(point - self.origin), self.dir_x.dot(point - self.origin)) while u < 0: u += 2 * np.pi @@ -144,6 +175,35 @@ def project_point(self, point: Point3D) -> "ConeEvaluation": return ConeEvaluation(self, ParamUV(u, v)) + def get_u_parameterization(self) -> Parameterization: + """ + The U parameter specifies the clockwise angle around the axis (right hand corkscrew law), + with a zero parameter at `dir_x`, and a period of 2*pi. + + Returns + ------- + Parameterization + Information about how a cone's u parameter is parameterized. + """ + return Parameterization(ParamForm.PERIODIC, ParamType.CIRCULAR, Interval(0, 2 * np.pi)) + + def get_v_parameterization(self) -> Parameterization: + """ + The V parameter specifies the distance along the axis, + with a zero parameter at the XY plane of the Cone. + + Returns + ------- + Parameterization + Information about how a cone's v parameter is parameterized. + """ + + # V parameter interval depends on which way the cone opens + start, end = ( + (self.apex_param, np.inf) if self.apex_param < 0 else (np.NINF, self.apex_param) + ) + return Parameterization(ParamForm.OPEN, ParamType.LINEAR, Interval(start, end)) + class ConeEvaluation(SurfaceEvaluation): """ @@ -173,7 +233,14 @@ def parameter(self) -> ParamUV: return self._parameter def position(self) -> Point3D: - """The point on the cone, based on the evaluation.""" + """ + The position of the evaluation. + + Returns + ------- + Point3D + The point that lies on the cone at this evaluation. + """ return ( self.cone.origin + self.parameter.v * self.cone.dir_z @@ -181,7 +248,14 @@ def position(self) -> Point3D: ) def normal(self) -> UnitVector3D: - """The normal to the surface.""" + """ + The normal to the surface. + + Returns + ------- + UnitVector3D + The normal unit vector to the cone at this evaluation. + """ return UnitVector3D( self.__cone_normal() * np.cos(self.cone.half_angle.m) - self.cone.dir_z * np.sin(self.cone.half_angle.m) @@ -204,37 +278,100 @@ def __cone_tangent(self) -> Vector3D: ) def u_derivative(self) -> Vector3D: - """The first derivative with respect to u.""" + """ + The first derivative with respect to u. + + Returns + ------- + Vector3D + The first derivative with respect to u. + """ return self.__radius_v() * self.__cone_tangent() def v_derivative(self) -> Vector3D: - """The first derivative with respect to v.""" + """ + The first derivative with respect to v. + + Returns + ------- + Vector3D + The first derivative with respect to v. + """ return self.cone.dir_z + np.tan(self.cone.half_angle.m) * self.__cone_normal() def uu_derivative(self) -> Vector3D: - """The second derivative with respect to u.""" + """ + The second derivative with respect to u. + + Returns + ------- + Vector3D + The second derivative with respect to u. + """ return -self.__radius_v() * self.__cone_normal() def uv_derivative(self) -> Vector3D: - """The second derivative with respect to u and v.""" + """ + The second derivative with respect to u and v. + + Returns + ------- + Vector3D + The second derivative with respect to u and v. + """ return np.tan(self.cone.half_angle.m) * self.__cone_tangent() def vv_derivative(self) -> Vector3D: - """The second derivative with respect to v.""" + """ + The second derivative with respect to v. + + Returns + ------- + Vector3D + The second derivative with respect to v. + """ return Vector3D([0, 0, 0]) def min_curvature(self) -> Real: - """The minimum curvature.""" + """ + The minimum curvature of the cone. + + Returns + ------- + Real + The minimum curvature of the cone. + """ return 0 def min_curvature_direction(self) -> UnitVector3D: - """The minimum curvature direction.""" + """ + The minimum curvature direction. + + Returns + ------- + UnitVector3D + The minimum curvature direction. + """ return UnitVector3D(self.v_derivative()) def max_curvature(self) -> Real: - """The maximum curvature.""" + """ + The maximum curvature of the cone. + + Returns + ------- + Real + The maximum curvature of the cone. + """ return 1.0 / self.__radius_v() def max_curvature_direction(self) -> UnitVector3D: - """The maximum curvature direction.""" + """ + The maximum curvature direction. + + Returns + ------- + UnitVector3D + The maximum curvature direction. + """ return UnitVector3D(self.u_derivative()) diff --git a/src/ansys/geometry/core/primitives/cylinder.py b/src/ansys/geometry/core/primitives/cylinder.py index ed555976ef..62e8326956 100644 --- a/src/ansys/geometry/core/primitives/cylinder.py +++ b/src/ansys/geometry/core/primitives/cylinder.py @@ -9,6 +9,13 @@ from ansys.geometry.core.misc import Distance from ansys.geometry.core.primitives.circle import Circle from ansys.geometry.core.primitives.line import Line +from ansys.geometry.core.primitives.parameterization import ( + Interval, + Parameterization, + ParamForm, + ParamType, + ParamUV, +) from ansys.geometry.core.primitives.surface_evaluation import ParamUV, SurfaceEvaluation from ansys.geometry.core.typing import Real, RealSequence @@ -78,7 +85,22 @@ def dir_z(self) -> UnitVector3D: return self._axis def surface_area(self, height: Union[Quantity, Distance, Real]) -> Quantity: - """Surface area of the cylinder.""" + """ + Surface area of the cylinder. + + Parameters + ---------- + height : Union[Quantity, Distance, Real] + By nature, a cylinder is infinite. If you want to get the surface area, + you must bound it by a height. Normally a cylinder surface is not closed + (does not have "caps" on the ends). This method will assume it is closed + for the purpose of getting the surface area. + + Returns + ------- + Quantity + The surface area of the temporarily bounded cylinder. + """ height = height if isinstance(height, Distance) else Distance(height) if height.value <= 0: raise ValueError("Height must be a real positive value.") @@ -86,7 +108,22 @@ def surface_area(self, height: Union[Quantity, Distance, Real]) -> Quantity: return 2 * np.pi * self.radius * height.value + 2 * np.pi * self.radius**2 def volume(self, height: Union[Quantity, Distance, Real]) -> Quantity: - """Volume of the cylinder.""" + """ + Volume of the cylinder. + + Parameters + ---------- + height : Union[Quantity, Distance, Real] + By nature, a cylinder is infinite. If you want to get the volume, + you must bound it by a height. Normally a cylinder surface is not closed + (does not have "caps" on the ends). This method will assume it is closed + for the purpose of getting the volume. + + Returns + ------- + Quantity + The volume of the temporarily bounded cylinder. + """ height = height if isinstance(height, Distance) else Distance(height) if height.value <= 0: raise ValueError("Height must be a real positive value.") @@ -104,11 +141,35 @@ def __eq__(self, other: "Cylinder") -> bool: ) def evaluate(self, parameter: ParamUV) -> "CylinderEvaluation": - """Evaluate the cylinder at the given parameters.""" + """ + Evaluate the cylinder at the given parameters. + + Parameters + ---------- + parameter : ParamUV + The parameters (u,v) at which to evaluate the cylinder. + + Returns + ------- + CylinderEvaluation + The resulting evaluation. + """ return CylinderEvaluation(self, parameter) def project_point(self, point: Point3D) -> "CylinderEvaluation": - """Project a point onto the cylinder and return its ``CylinderEvaluation``.""" + """ + Project a point onto the cylinder and return its ``CylinderEvaluation``. + + Parameters + ---------- + point : Point3D + The point to project onto the cylinder. + + Returns + ------- + CylinderEvaluation + The resulting evaluation. + """ circle = Circle(self.origin, self.radius, self.dir_x, self.dir_z) u = circle.project_point(point).parameter @@ -117,6 +178,30 @@ def project_point(self, point: Point3D) -> "CylinderEvaluation": return CylinderEvaluation(self, ParamUV(u, v)) + def get_u_parameterization(self) -> Parameterization: + """ + The U parameter specifies the clockwise angle around the axis (right hand corkscrew law), + with a zero parameter at `dir_x`, and a period of 2*pi. + + Returns + ------- + Parameterization + Information about how a cylinder's u parameter is parameterized. + """ + return Parameterization(ParamForm.PERIODIC, ParamType.CIRCULAR, Interval(0, 2 * np.pi)) + + def get_v_parameterization(self) -> Parameterization: + """ + The V parameter specifies the distance along the axis, + with a zero parameter at the XY plane of the Cylinder. + + Returns + ------- + Parameterization + Information about how a cylinders's v parameter is parameterized. + """ + return Parameterization(ParamForm.OPEN, ParamType.LINEAR, Interval(np.NINF, np.inf)) + class CylinderEvaluation(SurfaceEvaluation): """ @@ -146,7 +231,14 @@ def parameter(self) -> ParamUV: return self._parameter def position(self) -> Point3D: - """The point on the cylinder, based on the evaluation.""" + """ + The position of the evaluation. + + Returns + ------- + Point3D + The point that lies on the cylinder at this evaluation. + """ return ( self.cylinder.origin + self.cylinder.radius.m * self.__cylinder_normal() @@ -154,55 +246,132 @@ def position(self) -> Point3D: ) def normal(self) -> UnitVector3D: - """The normal to the surface.""" + """ + The normal to the surface. + + Returns + ------- + UnitVector3D + The normal unit vector to the cylinder at this evaluation. + """ return UnitVector3D(self.__cylinder_normal()) def __cylinder_normal(self) -> Vector3D: - """The normal to the cylinder.""" + """ + The normal to the surface. + + Returns + ------- + UnitVector3D + The normal unit vector to the cylinder at this evaluation. + """ return ( np.cos(self.parameter.u) * self.cylinder.dir_x + np.sin(self.parameter.u) * self.cylinder.dir_y ) def __cylinder_tangent(self) -> Vector3D: - """The tangent to the cylinder.""" + """Private tangent helper method.""" return ( -np.sin(self.parameter.u) * self.cylinder.dir_x + np.cos(self.parameter.u) * self.cylinder.dir_y ) def u_derivative(self) -> Vector3D: - """The first derivative with respect to u.""" + """ + The first derivative with respect to u. + + Returns + ------- + Vector3D + The first derivative with respect to u. + """ return self.cylinder.radius.m * self.__cylinder_tangent() def v_derivative(self) -> Vector3D: - """The first derivative with respect to v.""" + """ + The first derivative with respect to v. + + Returns + ------- + Vector3D + The first derivative with respect to v. + """ return self.cylinder.dir_z def uu_derivative(self) -> Vector3D: - """The second derivative with respect to u.""" + """ + The second derivative with respect to u. + + Returns + ------- + Vector3D + The second derivative with respect to u. + """ return -self.cylinder.radius.m * self.__cylinder_normal() def uv_derivative(self) -> Vector3D: - """The second derivative with respect to u and v.""" + """ + The second derivative with respect to u and v. + + Returns + ------- + Vector3D + The second derivative with respect to u and v. + """ return Vector3D([0, 0, 0]) def vv_derivative(self) -> Vector3D: - """The second derivative with respect to v.""" + """ + The second derivative with respect to v. + + Returns + ------- + Vector3D + The second derivative with respect to v. + """ return Vector3D([0, 0, 0]) def min_curvature(self) -> Real: - """The minimum curvature.""" + """ + The minimum curvature of the cylinder. + + Returns + ------- + Real + The minimum curvature of the cylinder. + """ return 0 def min_curvature_direction(self) -> UnitVector3D: - """The minimum curvature direction.""" + """ + The minimum curvature direction. + + Returns + ------- + UnitVector3D + The minimum curvature direction. + """ return UnitVector3D(self.cylinder.dir_z) def max_curvature(self) -> Real: - """The maximum curvature.""" + """ + The maximum curvature of the cylinder. + + Returns + ------- + Real + The maximum curvature of the cylinder. + """ return 1.0 / self.cylinder.radius.m def max_curvature_direction(self) -> UnitVector3D: - """The maximum curvature direction.""" + """ + The maximum curvature direction. + + Returns + ------- + UnitVector3D + The maximum curvature direction. + """ return UnitVector3D(self.u_derivative()) diff --git a/src/ansys/geometry/core/primitives/ellipse.py b/src/ansys/geometry/core/primitives/ellipse.py index 58099f1ef7..d6376b2fff 100644 --- a/src/ansys/geometry/core/primitives/ellipse.py +++ b/src/ansys/geometry/core/primitives/ellipse.py @@ -9,6 +9,12 @@ from ansys.geometry.core.math import UNITVECTOR3D_X, UNITVECTOR3D_Z, Point3D, UnitVector3D, Vector3D from ansys.geometry.core.misc import Accuracy, Distance from ansys.geometry.core.primitives.curve_evaluation import CurveEvaluation +from ansys.geometry.core.primitives.parameterization import ( + Interval, + Parameterization, + ParamForm, + ParamType, +) from ansys.geometry.core.typing import Real, RealSequence @@ -113,11 +119,35 @@ def __eq__(self, other: "Ellipse") -> bool: ) def evaluate(self, parameter: Real) -> "EllipseEvaluation": - """Evaluate the ellipse at the given parameter.""" + """ + Evaluate the ellipse at the given parameter. + + Parameters + ---------- + parameter : Real + The parameter at which to evaluate the ellipse. + + Returns + ------- + EllipseEvaluation + The resulting evaluation. + """ return EllipseEvaluation(self, parameter) def project_point(self, point: Point3D) -> "EllipseEvaluation": - """Project a point onto the ellipse and return its ``EllipseEvaluation``.""" + """ + Project a point onto the ellipse and return its ``EllipseEvaluation``. + + Parameters + ---------- + point : Point3D + The point to project onto the ellipse. + + Returns + ------- + EllipseEvaluation + The resulting evaluation. + """ origin_to_point = point - self.origin dir_in_plane = UnitVector3D.from_points( Point3D([0, 0, 0]), origin_to_point - ((origin_to_point * self.dir_z) * self.dir_z) @@ -132,7 +162,19 @@ def project_point(self, point: Point3D) -> "EllipseEvaluation": return EllipseEvaluation(self, t) def is_coincident_ellipse(self, other: "Ellipse") -> bool: - """Determine if this ellipse is coincident with another.""" + """ + Determine if this ellipse is coincident with another. + + Parameters + ---------- + other : Ellipse + The ellipse to determine coincidence with. + + Returns + ------- + bool + Returns ``True`` if this ellipse is coincident with the other. + """ return ( Accuracy.length_is_equal(self.major_radius.m, other.major_radius.m) and Accuracy.length_is_equal(self.minor_radius.m, other.minor_radius.m) @@ -180,6 +222,19 @@ def area(self) -> Quantity: """Area of the ellipse.""" return np.pi * self.major_radius * self.minor_radius + def get_parameterization(self) -> Parameterization: + """ + The parameter of an ellipse specifies the clockwise angle around the axis + (right hand corkscrew law), with a zero parameter at `dir_x` and a period + of 2*pi. + + Returns + ------- + Parameterization + Information about how an ellipse is parameterized. + """ + return Parameterization(ParamForm.PERIODIC, ParamType.OTHER, Interval(0, 2 * np.pi)) + class EllipseEvaluation(CurveEvaluation): """ @@ -194,6 +249,7 @@ class EllipseEvaluation(CurveEvaluation): """ def __init__(self, ellipse: Ellipse, parameter: Real) -> None: + """``EllipseEvaluation`` class constructor.""" self._ellipse = ellipse self._parameter = parameter @@ -208,7 +264,14 @@ def parameter(self) -> Real: return self._parameter def position(self) -> Point3D: - """The position of the evaluation.""" + """ + The position of the evaluation. + + Returns + ------- + Point3D + The point that lies on the ellipse at this evaluation. + """ return ( self.ellipse.origin + ((self.ellipse.major_radius * np.cos(self.parameter)) * self.ellipse.dir_x).m @@ -216,25 +279,55 @@ def position(self) -> Point3D: ) def tangent(self) -> UnitVector3D: - """The tangent of the evaluation.""" + """ + The tangent of the evaluation. + + Returns + ------- + UnitVector3D + The tangent unit vector to the ellipse at this evaluation. + """ return ( (self.ellipse.minor_radius * np.cos(self.parameter) * self.ellipse.dir_y).m - (self.ellipse.major_radius * np.sin(self.parameter) * self.ellipse.dir_x).m ).normalize() def first_derivative(self) -> Vector3D: - """The first derivative of the evaluation.""" + """ + The first derivative of the evaluation. The first derivative is in the direction of the + tangent and has a magnitude equal to the velocity (rate of change of position) at that + point. + + Returns + ------- + Vector3D + The first derivative of this evaluation. + """ return (self.ellipse.minor_radius * np.cos(self.parameter) * self.ellipse.dir_y).m - ( self.ellipse.major_radius * np.sin(self.parameter) * self.ellipse.dir_x ).m def second_derivative(self) -> Vector3D: - """The second derivative of the evaluation.""" + """ + The second derivative of the evaluation. + + Returns + ------- + Vector3D + The second derivative of this evaluation. + """ return ( -self.ellipse.major_radius * np.cos(self.parameter) * self.ellipse.dir_x - self.ellipse.minor_radius * np.sin(self.parameter) * self.ellipse.dir_y ).m def curvature(self) -> Real: - """The curvature of the evaluation.""" + """ + The curvature of the ellipse. + + Returns + ------- + Real + The curvature of the ellipse. + """ return self.second_derivative().magnitude / np.power(self.first_derivative().magnitude, 2) diff --git a/src/ansys/geometry/core/primitives/line.py b/src/ansys/geometry/core/primitives/line.py index 1c6e644018..2c942e5bd8 100644 --- a/src/ansys/geometry/core/primitives/line.py +++ b/src/ansys/geometry/core/primitives/line.py @@ -9,6 +9,12 @@ from ansys.geometry.core.math import Point3D, UnitVector3D, Vector3D from ansys.geometry.core.misc.accuracy import LENGTH_ACCURACY from ansys.geometry.core.primitives.curve_evaluation import CurveEvaluation +from ansys.geometry.core.primitives.parameterization import ( + Interval, + Parameterization, + ParamForm, + ParamType, +) from ansys.geometry.core.typing import RealSequence @@ -52,17 +58,53 @@ def __eq__(self, other: object) -> bool: return False def evaluate(self, parameter: float) -> "LineEvaluation": - """Evaluates the line with the given parameter and returns the evaluation.""" + """ + Evaluate the line at the given parameter. + + Parameters + ---------- + parameter : Real + The parameter at which to evaluate the line. + + Returns + ------- + LineEvaluation + The resulting evaluation. + """ return LineEvaluation(self, parameter) def project_point(self, point: Point3D) -> "LineEvaluation": - """Project a point onto the line and return its ``LineEvaluation``.""" + """ + Project a point onto the line and return its ``LineEvaluation``. + + Parameters + ---------- + point : Point3D + The point to project onto the line. + + Returns + ------- + LineEvaluation + The resulting evaluation. + """ origin_to_point = point - self.origin t = origin_to_point.dot(self.direction) return LineEvaluation(self, t) def is_coincident_line(self, other: "Line") -> bool: - """Returns ``True`` if both lines are coincident.""" + """ + Determine if this line is coincident with another. + + Parameters + ---------- + other : Line + The line to determine coincidence with. + + Returns + ------- + bool + Returns ``True`` if this line is coincident with the other. + """ if self == other: return True if not self.direction.is_parallel_to(other.direction): @@ -75,16 +117,41 @@ def is_coincident_line(self, other: "Line") -> bool: ) and math.pow((self.direction % between).magnitude, 2) <= math.pow(LENGTH_ACCURACY, 2) def is_opposite_line(self, other: "Line") -> bool: - """Returns ``True`` if lines are opposite each other.""" + """ + Determine if this line is opposite another. + + Parameters + ---------- + other : Line + The line to determine opposition with. + + Returns + ------- + bool + Returns ``True`` if this line is opposite to the other. + """ if self.is_coincident_line(other): return self.direction.is_opposite(other.direction) return False + def get_parameterization(self) -> Parameterization: + """ + The parameter of a line specifies the distance from the `origin` in the + direction of `direction`. + + Returns + ------- + Parameterization + Information about how a line is parameterized. + """ + return Parameterization(ParamForm.OPEN, ParamType.LINEAR, Interval(np.NINF, np.inf)) + class LineEvaluation(CurveEvaluation): """Provides result class when evaluating a line.""" def __init__(self, line: Line, parameter: float = None) -> None: + """``LineEvaluation`` class constructor.""" self._line = line self._parameter = parameter @@ -99,21 +166,56 @@ def parameter(self) -> float: return self._parameter def position(self) -> Point3D: - """The position of the evaluation.""" + """ + The position of the evaluation. + + Returns + ------- + Point3D + The point that lies on the line at this evaluation. + """ return self.line.origin + self.parameter * self.line.direction def tangent(self) -> UnitVector3D: - """The tangent of the evaluation.""" + """ + The tangent of the evaluation. This is always equal to the direction of the line. + + Returns + ------- + UnitVector3D + The tangent unit vector to the line at this evaluation. + """ return self.line.direction def first_derivative(self) -> Vector3D: - """The first derivative of the evaluation.""" + """ + The first derivative of the evaluation. This is always equal to the direction of the line. + + Returns + ------- + Vector3D + The first derivative of this evaluation. + """ return self.line.direction def second_derivative(self) -> Vector3D: - """The second derivative of the evaluation.""" + """ + The second derivative of the evaluation. This is always equal to a zero vector. + + Returns + ------- + Vector3D + The second derivative of this evaluation. Always ``Vector3D([0, 0, 0])``. + """ return Vector3D([0, 0, 0]) def curvature(self) -> float: - """The curvature of the evaluation.""" + """ + The curvature of the line. This will always be 0. + + Returns + ------- + Real + The curvature of the line. Always 0. + """ return 0 diff --git a/src/ansys/geometry/core/primitives/parameterization.py b/src/ansys/geometry/core/primitives/parameterization.py new file mode 100644 index 0000000000..bd18d6cbe3 --- /dev/null +++ b/src/ansys/geometry/core/primitives/parameterization.py @@ -0,0 +1,232 @@ +from enum import Enum + +from beartype import beartype as check_input_types +import numpy as np + +from ansys.geometry.core.typing import Real + + +class ParamUV: + """ + Parameter class containing 2 parameters: (u, v). Likened to a 2D point in UV space + Used as an argument in parametric surface evaluations. This matches the service + implementation for the Geometry service. + + Parameters + ---------- + u : Real + u-parameter. + v : Real + v-parameter. + """ + + def __init__(self, u: Real, v: Real) -> None: + self._u = u + self._v = v + + @property + def u(self) -> Real: + """u-parameter.""" + return self._u + + @property + def v(self) -> Real: + """v-parameter.""" + return self._v + + @check_input_types + def __add__(self, other: "ParamUV") -> "ParamUV": + """ + Adds the u and v components of the other ParamUV to this ParamUV. + + Parameters + ---------- + other : ParamUV + The parameters to add these parameters. + + Returns + ------- + ParamUV + The sum of the parameters. + """ + return ParamUV(self._u + other._u, self._v + other._v) + + @check_input_types + def __sub__(self, other: "ParamUV") -> "ParamUV": + """ + Subtracts the u and v components of the other ParamUV from this ParamUV. + + Parameters + ---------- + other : ParamUV + The parameters to subtract from these parameters. + + Returns + ------- + ParamUV + The difference of the parameters. + """ + return ParamUV(self._u - other._u, self._v - other._v) + + @check_input_types + def __mul__(self, other: "ParamUV") -> "ParamUV": + """ + Multiplies the u and v components of this ParamUV by the other ParamUV. + + Parameters + ---------- + other : ParamUV + The parameters to multiply by these parameters. + + Returns + ------- + ParamUV + The product of the parameters. + """ + return ParamUV(self._u * other._u, self._v * other._v) + + @check_input_types + def __truediv__(self, other: "ParamUV") -> "ParamUV": + """ + Divides the u and v components of this ParamUV by the other ParamUV. + + Parameters + ---------- + other : ParamUV + The parameters to divide these parameters by. + + Returns + ------- + ParamUV + The quotient of the parameters. + """ + return ParamUV(self._u / other._u, self._v / other._v) + + def __repr__(self) -> str: + return f"ParamUV(u={self.u}, v={self.v})" + + +class Interval: + """ + Interval class that defines a range of values. + + Parameters + ---------- + start : Real + Start value of the interval. + end : Real + End value of the interval. + """ + + @check_input_types + def __init__(self, start: Real, end: Real) -> None: + if end < start: + raise ValueError("Start value must be less than end value") + + self._start = start + self._end = end + + @property + def start(self) -> Real: + """Start value of the interval.""" + return self._start + + @property + def end(self) -> Real: + """End value of the interval.""" + return self._end + + def is_open(self) -> bool: + """ + If the interval is open (-inf, inf). + + Returns + ------- + bool + True if both ends of the interval are negative and positive infinity respectively. + """ + return np.isneginf(self.start) and np.isinf(self.end) + + def is_closed(self) -> bool: + """ + If the interval is closed. Neither value is inf or -inf. + + Returns + ------- + bool + True if neither bound of the interval is infinite. + """ + return self.start > np.NINF and self.end < np.inf + + def get_span(self) -> Real: + """ + Returns the quantity contained by the interval. Interval must be closed. + + Returns + ------- + Real + The difference between the end and start of the interval. + """ + if not self.is_closed(): + raise ValueError("Interval must be closed to get the span.") + + return self.end - self.start + + def __repr__(self) -> str: + return f"Interval(start={self.start}, end={self.end})" + + +class ParamForm(Enum): + """ParamForm enum class that defines the form of a Parameterization.""" + + OPEN = 1 + CLOSED = 2 + PERIODIC = 3 + OTHER = 4 + + +class ParamType(Enum): + """ParamType enum class that defines the type of a Parameterization.""" + + LINEAR = 1 + CIRCULAR = 2 + OTHER = 3 + + +class Parameterization: + """ + Parameterization class describes the parameters of a specific geometry. + + Parameters + ---------- + form : ParamForm + Form of the parameterization. + type : ParamType + Type of the parameterization. + interval : Interval + Interval of the parameterization. + """ + + @check_input_types + def __init__(self, form: ParamForm, type: ParamType, interval: Interval) -> None: + self._form = form + self._type = type + self._interval = interval + + @property + def form(self) -> ParamForm: + """The form of the parameterization.""" + return self._form + + @property + def type(self) -> ParamType: + """The type of the parameterization.""" + return self._type + + @property + def interval(self) -> Interval: + """The interval of the parameterization.""" + return self._interval + + def __repr__(self) -> str: + return f"Parameterization(form={self.form}, type={self.type}, interval={self.interval})" diff --git a/src/ansys/geometry/core/primitives/sphere.py b/src/ansys/geometry/core/primitives/sphere.py index 2e41bda33e..54a1163470 100644 --- a/src/ansys/geometry/core/primitives/sphere.py +++ b/src/ansys/geometry/core/primitives/sphere.py @@ -7,7 +7,14 @@ from ansys.geometry.core.math import UNITVECTOR3D_X, UNITVECTOR3D_Z, Point3D, UnitVector3D, Vector3D from ansys.geometry.core.misc import Distance -from ansys.geometry.core.primitives.surface_evaluation import ParamUV, SurfaceEvaluation +from ansys.geometry.core.primitives.parameterization import ( + Interval, + Parameterization, + ParamForm, + ParamType, + ParamUV, +) +from ansys.geometry.core.primitives.surface_evaluation import SurfaceEvaluation from ansys.geometry.core.typing import Real, RealSequence @@ -96,11 +103,35 @@ def __eq__(self, other: "Sphere") -> bool: ) def evaluate(self, parameter: ParamUV) -> "SphereEvaluation": - """Evaluate the sphere at the given parameters.""" + """ + Evaluate the sphere at the given parameters. + + Parameters + ---------- + parameter : ParamUV + The parameters (u,v) at which to evaluate the sphere. + + Returns + ------- + SphereEvaluation + The resulting evaluation. + """ return SphereEvaluation(self, parameter) def project_point(self, point: Point3D) -> "SphereEvaluation": - """Project a point onto the sphere and return its ``SphereEvaluation``.""" + """ + Project a point onto the sphere and return its ``SphereEvaluation``. + + Parameters + ---------- + point : Point3D + The point to project onto the sphere. + + Returns + ------- + SphereEvaluation + The resulting evaluation. + """ origin_to_point = point - self.origin x = origin_to_point.dot(self.dir_x) y = origin_to_point.dot(self.dir_y) @@ -112,6 +143,30 @@ def project_point(self, point: Point3D) -> "SphereEvaluation": v = np.arctan2(z, np.sqrt(x * x + y * y)) return SphereEvaluation(self, ParamUV(u, v)) + def get_u_parameterization(self) -> Parameterization: + """ + The U parameter specifies the longitude angle, increasing clockwise (East) about `dir_z` + (right hand corkscrew law). It has a zero parameter at `dir_x`, and a period of 2*pi. + + Returns + ------- + Parameterization + Information about how a sphere's u parameter is parameterized. + """ + return Parameterization(ParamForm.PERIODIC, ParamType.CIRCULAR, Interval(0, 2 * np.pi)) + + def get_v_parameterization(self) -> Parameterization: + """ + The V parameter specifies the latitude, increasing North, with a zero parameter at the + equator, and a range of [-pi/2, pi/2]. + + Returns + ------- + Parameterization + Information about how a sphere's v parameter is parameterized. + """ + return Parameterization(ParamForm.CLOSED, ParamType.OTHER, Interval(-np.pi / 2, np.pi / 2)) + class SphereEvaluation(SurfaceEvaluation): """ @@ -126,6 +181,7 @@ class SphereEvaluation(SurfaceEvaluation): """ def __init__(self, sphere: Sphere, parameter: ParamUV) -> None: + """``SphereEvaluation`` class constructor.""" self._sphere = sphere self._parameter = parameter @@ -140,11 +196,25 @@ def parameter(self) -> ParamUV: return self._parameter def position(self) -> Point3D: - """The point on the sphere, based on the evaluation.""" + """ + The position of the evaluation. + + Returns + ------- + Point3D + The point that lies on the sphere at this evaluation. + """ return self.sphere.origin + self.sphere.radius.m * self.normal() def normal(self) -> UnitVector3D: - """The normal to the surface.""" + """ + The normal to the surface. + + Returns + ------- + UnitVector3D + The normal unit vector to the sphere at this evaluation. + """ return UnitVector3D( np.cos(self.parameter.v) * self.__cylinder_normal() + np.sin(self.parameter.v) * self.sphere.dir_z @@ -165,43 +235,106 @@ def __cylinder_tangent(self) -> Vector3D: ) def u_derivative(self) -> Vector3D: - """The first derivative with respect to u.""" + """ + The first derivative with respect to u. + + Returns + ------- + Vector3D + The first derivative with respect to u. + """ return np.cos(self.parameter.v) * self.sphere.radius.m * self.__cylinder_tangent() def v_derivative(self) -> Vector3D: - """The first derivative with respect to v.""" + """ + The first derivative with respect to v. + + Returns + ------- + Vector3D + The first derivative with respect to v. + """ return self.sphere.radius.m * ( np.cos(self.parameter.v) * self.sphere.dir_z - np.sin(self.parameter.v) * self.__cylinder_normal() ) def uu_derivative(self) -> Vector3D: - """The second derivative with respect to u.""" + """ + The second derivative with respect to u. + + Returns + ------- + Vector3D + The second derivative with respect to u. + """ return -np.cos(self.parameter.v) * self.sphere.radius.m * self.__cylinder_normal() def uv_derivative(self) -> Vector3D: - """The second derivative with respect to u and v.""" + """ + The second derivative with respect to u and v. + + Returns + ------- + Vector3D + The second derivative with respect to u and v. + """ return -np.sin(self.parameter.v) * self.sphere.radius.m * self.__cylinder_tangent() def vv_derivative(self) -> Vector3D: - """The second derivative with respect to v.""" + """ + The second derivative with respect to v. + + Returns + ------- + Vector3D + The second derivative with respect to v. + """ return self.sphere.radius.m * ( -np.sin(self.parameter.v) * self.sphere.dir_z - np.cos(self.parameter.v) * self.__cylinder_normal() ) def min_curvature(self) -> Real: - """The minimum curvature.""" + """ + The minimum curvature of the sphere. + + Returns + ------- + Real + The minimum curvature of the sphere. + """ return 1.0 / self.sphere.radius.m def min_curvature_direction(self) -> UnitVector3D: - """The minimum curvature direction.""" + """ + The minimum curvature direction. + + Returns + ------- + UnitVector3D + The minimum curvature direction. + """ return self.normal() % self.max_curvature_direction() def max_curvature(self) -> Real: - """The maximum curvature.""" + """ + The maximum curvature of the sphere. + + Returns + ------- + Real + The maximum curvature of the sphere. + """ return 1.0 / self.sphere.radius.m def max_curvature_direction(self) -> UnitVector3D: - """The maximum curvature direction.""" + """ + The maximum curvature direction. + + Returns + ------- + UnitVector3D + The maximum curvature direction. + """ return UnitVector3D(self.v_derivative()) diff --git a/src/ansys/geometry/core/primitives/surface_evaluation.py b/src/ansys/geometry/core/primitives/surface_evaluation.py index 9f49bc333a..a0e86e29fe 100644 --- a/src/ansys/geometry/core/primitives/surface_evaluation.py +++ b/src/ansys/geometry/core/primitives/surface_evaluation.py @@ -1,62 +1,8 @@ -from beartype import beartype as check_input_types - from ansys.geometry.core.math import Point3D, UnitVector3D, Vector3D +from ansys.geometry.core.primitives.parameterization import ParamUV from ansys.geometry.core.typing import Real -class ParamUV: - """ - Parameter class containing 2 parameters: (u, v). Likened to a 2D point in UV space - Used as an argument in parametric surface evaluations. This matches the service - implementation for the Geometry service. - - Parameters - ---------- - u : Real - u-parameter. - v : Real - v-parameter. - """ - - def __init__(self, u: Real, v: Real) -> None: - self._u = u - self._v = v - - @property - def u(self) -> Real: - """u-parameter.""" - return self._u - - @property - def v(self) -> Real: - """v-parameter.""" - return self._v - - @check_input_types - def __add__(self, other: "ParamUV") -> "ParamUV": - """Adds the u and v components of the other ParamUV to this ParamUV.""" - self._u += other._u - self._v += other._v - - @check_input_types - def __sub__(self, other: "ParamUV") -> "ParamUV": - """Subtracts the u and v components of the other ParamUV from this ParamUV.""" - self._u -= other._u - self._v -= other._v - - @check_input_types - def __mul__(self, other: "ParamUV") -> "ParamUV": - """Multiplies the u and v components of this ParamUV by the other ParamUV.""" - self._u *= other._u - self._v *= other._v - - @check_input_types - def __truediv__(self, other: "ParamUV") -> "ParamUV": - """Divides the u and v components of this ParamUV by the other ParamUV.""" - self._u /= other._u - self._v /= other._v - - class SurfaceEvaluation: """Provides result class when evaluating a surface.""" diff --git a/tests/test_parameterization.py b/tests/test_parameterization.py new file mode 100644 index 0000000000..408c8fc4e3 --- /dev/null +++ b/tests/test_parameterization.py @@ -0,0 +1,117 @@ +from beartype.roar import BeartypeCallHintParamViolation +import numpy as np +import pytest + +from ansys.geometry.core.misc.accuracy import Accuracy +from ansys.geometry.core.primitives.parameterization import ( + Interval, + Parameterization, + ParamForm, + ParamType, + ParamUV, +) + + +def test_param_uv(): + p = ParamUV(1, 1) + p2 = ParamUV(1, 2) + + assert p.u == 1 + assert p.v == 1 + + sum = p + p2 + assert sum.u == 2 + assert sum.v == 3 + + diff = p - p2 + assert diff.u == 0 + assert diff.v == -1 + + prod = p * p2 + assert prod.u == 1 + assert prod.v == 2 + + quot = p / p2 + assert Accuracy.length_is_equal(quot.u, 1) + assert Accuracy.length_is_equal(quot.v, 0.5) + + +def test_interval(): + open_interval = Interval(np.NINF, np.inf) + closed_interval = Interval(-1, 1) + open_start = Interval(np.NINF, 1) + open_end = Interval(-1, np.inf) + + assert np.isneginf(open_interval.start) + assert np.isinf(open_interval.end) + + assert open_interval.is_open() + assert not closed_interval.is_open() + assert not open_start.is_open() + assert not open_end.is_open() + + assert not open_interval.is_closed() + assert closed_interval.is_closed() + assert not open_start.is_closed() + assert not open_end.is_closed() + + with pytest.raises(ValueError): + open_interval.get_span() + with pytest.raises(ValueError): + open_start.get_span() + with pytest.raises(ValueError): + open_end.get_span() + + assert closed_interval.get_span() == 2 + + +def test_param_form(): + open = ParamForm.OPEN + closed = ParamForm.CLOSED + periodic = ParamForm.PERIODIC + other = ParamForm.OTHER + + assert open.name == "OPEN" + assert open.value == 1 + + assert closed.name == "CLOSED" + assert closed.value == 2 + + assert periodic.name == "PERIODIC" + assert periodic.value == 3 + + assert other.name == "OTHER" + assert other.value == 4 + + +def test_param_TYPE(): + linear = ParamType.LINEAR + circular = ParamType.CIRCULAR + other = ParamType.OTHER + + assert linear.name == "LINEAR" + assert linear.value == 1 + + assert circular.name == "CIRCULAR" + assert circular.value == 2 + + assert other.name == "OTHER" + assert other.value == 3 + + +def test_parameterization(): + p = Parameterization(ParamForm.PERIODIC, ParamType.CIRCULAR, Interval(0, 2 * np.pi)) + assert p.form == ParamForm.PERIODIC + assert p.type == ParamType.CIRCULAR + assert p.interval.start == 0 + assert Accuracy.length_is_equal(p.interval.end, 2 * np.pi) + assert p.interval.is_closed() + + with pytest.raises(BeartypeCallHintParamViolation): + Parameterization(ParamType.CIRCULAR, ParamType.OTHER, Interval(-1, 1)) + + with pytest.raises(BeartypeCallHintParamViolation): + Parameterization(ParamForm.CLOSED, ParamForm.CLOSED, Interval(-1, 1)) + + with pytest.raises(BeartypeCallHintParamViolation): + Parameterization(ParamForm.CLOSED, ParamType.OTHER, [-1, 1])