Skip to content

Commit

Permalink
Merge branch 'blitz' into feat/matrix_rotation_translation_ops
Browse files Browse the repository at this point in the history
  • Loading branch information
RobPasMue authored Jan 28, 2025
2 parents 886807e + 9105449 commit fc775ec
Show file tree
Hide file tree
Showing 14 changed files with 502 additions and 40 deletions.
1 change: 1 addition & 0 deletions doc/changelog.d/1675.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
implementation of NURBS curves
1 change: 1 addition & 0 deletions doc/changelog.d/1683.test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
add unit tests for 3 repair tools
1 change: 1 addition & 0 deletions doc/changelog.d/1690.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
cleanup unsupported module
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ dependencies = [
"ansys-tools-visualization-interface>=0.2.6,<1",
"attrs!=24.3.0",
"beartype>=0.11.0,<0.20",
"geomdl>=5,<6",
"grpcio>=1.35.0,<1.68",
"grpcio-health-checking>=1.45.0,<1.68",
"numpy>=1.20.3,<3",
Expand All @@ -54,6 +55,7 @@ tests = [
"ansys-tools-visualization-interface==0.8.1",
"beartype==0.19.0",
"docker==7.1.0",
"geomdl==5.3.1",
"grpcio==1.67.1",
"grpcio-health-checking==1.67.1",
"numpy==2.2.1",
Expand Down Expand Up @@ -82,6 +84,7 @@ doc = [
"ansys-tools-visualization-interface==0.8.1",
"beartype==0.19.0",
"docker==7.1.0",
"geomdl==5.3.1",
"grpcio==1.67.1",
"grpcio-health-checking==1.67.1",
"ipyvtklink==0.2.3",
Expand Down
8 changes: 4 additions & 4 deletions src/ansys/geometry/core/modeler.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@
from ansys.geometry.core.logger import LOG
from ansys.geometry.core.misc.checks import check_type, min_backend_version
from ansys.geometry.core.misc.options import ImportOptions
from ansys.geometry.core.misc.unsupported import UnsupportedCommands
from ansys.geometry.core.tools.measurement_tools import MeasurementTools
from ansys.geometry.core.tools.prepare_tools import PrepareTools
from ansys.geometry.core.tools.repair_tools import RepairTools
from ansys.geometry.core.tools.unsupported import UnsupportedCommands
from ansys.geometry.core.typing import Real

if TYPE_CHECKING: # pragma: no cover
Expand Down Expand Up @@ -120,6 +120,9 @@ def __init__(
backend_type=backend_type,
)

# Maintaining references to all designs within the modeler workspace
self._designs: dict[str, "Design"] = {}

# Initialize the RepairTools - Not available on Linux
# TODO: delete "if" when Linux service is able to use repair tools
# https://github.com/ansys/pyansys-geometry/issues/1319
Expand All @@ -135,9 +138,6 @@ def __init__(
self._geometry_commands = GeometryCommands(self._grpc_client)
self._unsupported = UnsupportedCommands(self._grpc_client, self)

# Maintaining references to all designs within the modeler workspace
self._designs: dict[str, "Design"] = {}

# Check if the backend allows for multiple designs and throw warning if needed
if not self.client.multiple_designs_allowed:
LOG.warning(
Expand Down
1 change: 1 addition & 0 deletions src/ansys/geometry/core/shapes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from ansys.geometry.core.shapes.curves.curve import Curve
from ansys.geometry.core.shapes.curves.ellipse import Ellipse, EllipseEvaluation
from ansys.geometry.core.shapes.curves.line import Line, LineEvaluation
from ansys.geometry.core.shapes.curves.nurbs import NURBSCurve, NURBSCurveEvaluation
from ansys.geometry.core.shapes.parameterization import (
Interval,
Parameterization,
Expand Down
1 change: 1 addition & 0 deletions src/ansys/geometry/core/shapes/curves/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@
from ansys.geometry.core.shapes.curves.curve_evaluation import CurveEvaluation
from ansys.geometry.core.shapes.curves.ellipse import Ellipse, EllipseEvaluation
from ansys.geometry.core.shapes.curves.line import Line, LineEvaluation
from ansys.geometry.core.shapes.curves.nurbs import NURBSCurve, NURBSCurveEvaluation
310 changes: 310 additions & 0 deletions src/ansys/geometry/core/shapes/curves/nurbs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
# Copyright (C) 2023 - 2025 ANSYS, Inc. and/or its affiliates.
# SPDX-License-Identifier: MIT
#
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Provides for creating and managing a NURBS curve."""

from functools import cached_property
from typing import Optional

import geomdl.NURBS as geomdl_nurbs # noqa: N811

from ansys.geometry.core.math import Matrix44, Point3D
from ansys.geometry.core.math.vector import Vector3D
from ansys.geometry.core.shapes.curves.curve import Curve
from ansys.geometry.core.shapes.curves.curve_evaluation import CurveEvaluation
from ansys.geometry.core.shapes.parameterization import (
Interval,
Parameterization,
ParamForm,
ParamType,
)
from ansys.geometry.core.typing import Real


class NURBSCurve(Curve):
"""Represents a NURBS curve.
Notes
-----
This class is a wrapper around the NURBS curve class from the `geomdl` library.
By leveraging the `geomdl` library, this class provides a high-level interface
to create and manipulate NURBS curves. The `geomdl` library is a powerful
library for working with NURBS curves and surfaces. For more information, see
https://pypi.org/project/geomdl/.
"""

def __init__(
self,
):
"""Initialize ``NURBSCurve`` class."""
self._nurbs_curve = geomdl_nurbs.Curve()

@property
def geomdl_nurbs_curve(self) -> geomdl_nurbs.Curve:
"""Get the underlying NURBS curve.
Notes
-----
This property gives access to the full functionality of the NURBS curve
coming from the `geomdl` library. Use with caution.
"""
return self._nurbs_curve

@property
def control_points(self) -> list[Point3D]:
"""Get the control points of the curve."""
return [Point3D(point) for point in self._nurbs_curve.ctrlpts]

@property
def degree(self) -> int:
"""Get the degree of the curve."""
return self._nurbs_curve.degree

@property
def knots(self) -> list[Real]:
"""Get the knot vector of the curve."""
return self._nurbs_curve.knotvector

@property
def weights(self) -> list[Real]:
"""Get the weights of the control points."""
return self._nurbs_curve.weights

@classmethod
def from_control_points(
cls,
control_points: list[Point3D],
degree: int,
knots: list[Real],
weights: list[Real] = None,
) -> "NURBSCurve":
"""Create a NURBS curve from control points.
Parameters
----------
control_points : list[Point3D]
Control points of the curve.
degree : int
Degree of the curve.
knots : list[Real]
Knot vector of the curve.
weights : list[Real], optional
Weights of the control points.
Returns
-------
NURBSCurve
NURBS curve.
"""
curve = cls()
curve._nurbs_curve.degree = degree
curve._nurbs_curve.ctrlpts = control_points
curve._nurbs_curve.knotvector = knots
if weights:
curve._nurbs_curve.weights = weights

# Verify the curve is valid
try:
curve._nurbs_curve._check_variables()
except ValueError as e:
raise ValueError(f"Invalid NURBS curve: {e}")

return curve

def __eq__(self, other: "NURBSCurve") -> bool:
"""Determine if two curves are equal."""
if not isinstance(other, NURBSCurve):
return False
return (
self._nurbs_curve.degree == other._nurbs_curve.degree
and self._nurbs_curve.ctrlpts == other._nurbs_curve.ctrlpts
and self._nurbs_curve.knotvector == other._nurbs_curve.knotvector
and self._nurbs_curve.weights == other._nurbs_curve.weights
)

def parameterization(self) -> Parameterization:
"""Get the parametrization of the NURBS curve.
The parameter is defined in the interval [0, 1] by default. Information
is provided about the parameter type and form.
Returns
-------
Parameterization
Information about how the NURBS curve is parameterized.
"""
return Parameterization(
ParamType.OTHER,
ParamForm.OTHER,
Interval(start=self._nurbs_curve.domain[0], end=self._nurbs_curve.domain[1]),
)

def transformed_copy(self, matrix: Matrix44) -> "NURBSCurve":
"""Create a transformed copy of the curve.
Parameters
----------
matrix : Matrix44
Transformation matrix.
Returns
-------
NURBSCurve
Transformed copy of the curve.
"""
control_points = [matrix @ point for point in self._nurbs_curve.ctrlpts]
return NURBSCurve.from_control_points(
control_points,
self._nurbs_curve.degree,
self._nurbs_curve.knotvector,
self._nurbs_curve.weights,
)

def evaluate(self, parameter: Real) -> CurveEvaluation:
"""Evaluate the curve at the given parameter.
Parameters
----------
parameter : Real
Parameter to evaluate the curve at.
Returns
-------
CurveEvaluation
Evaluation of the curve at the given parameter.
"""
return NURBSCurveEvaluation(self, parameter)

def contains_param(self, param: Real) -> bool: # noqa: D102
raise NotImplementedError("contains_param() is not implemented.")

def contains_point(self, point: Point3D) -> bool: # noqa: D102
raise NotImplementedError("contains_point() is not implemented.")

def project_point(
self, point: Point3D, initial_guess: Optional[Real] = None
) -> CurveEvaluation:
"""Project a point to the NURBS curve.
This method returns the evaluation at the closest point.
Notes
-----
Based on `the NURBS book <https://link.springer.com/book/10.1007/978-3-642-59223-2>`_,
the projection of a point to a NURBS curve is the solution to the following optimization
problem: minimize the distance between the point and the curve. The distance is defined
as the Euclidean distance squared. For more information, please refer to
the implementation of the `distance_squared` function.
Parameters
----------
point : Point3D
Point to project to the curve.
initial_guess : Real, optional
Initial guess for the optimization algorithm. If not provided, the midpoint
of the domain is used.
Returns
-------
CurveEvaluation
Evaluation at the closest point on the curve.
"""
import numpy as np
from scipy.optimize import minimize

# Function to minimize (distance squared)
def distance_squared(
u: float, geomdl_nurbs_curbe: geomdl_nurbs.Curve, point: np.ndarray
) -> np.ndarray:
point_on_curve = np.array(geomdl_nurbs_curbe.evaluate_single(u))
return np.sum((point_on_curve - point) ** 2)

# Define the domain and initial guess (midpoint of the domain by default)
domain = self._nurbs_curve.domain
initial_guess = initial_guess if initial_guess else (domain[0] + domain[1]) / 2

# Minimize the distance squared
result = minimize(
distance_squared,
initial_guess,
bounds=[domain],
args=(self._nurbs_curve, np.array(point)),
)

# Closest point on the curve
u_min = result.x[0]

# Return the evaluation at the closest point
return self.evaluate(u_min)


class NURBSCurveEvaluation(CurveEvaluation):
"""Provides evaluation of a NURBS curve at a given parameter.
Parameters
----------
nurbs_curve: ~ansys.geometry.core.shapes.curves.nurbs.NURBSCurve
NURBS curve to evaluate.
parameter: Real
Parameter to evaluate the NURBS curve at.
"""

def __init__(self, nurbs_curve: NURBSCurve, parameter: Real) -> None:
"""Initialize the ``NURBSCurveEvaluation`` class."""
self._parameter = parameter
self._point_eval, self._first_deriv_eval, self._second_deriv_eval = (
nurbs_curve.geomdl_nurbs_curve.derivatives(parameter, 2)
)

@property
def parameter(self) -> Real:
"""Parameter that the evaluation is based upon."""
return self._parameter

@cached_property
def position(self) -> Point3D:
"""Position of the evaluation."""
return Point3D(self._point_eval)

@cached_property
def first_derivative(self) -> Vector3D:
"""First derivative of the evaluation."""
return Vector3D(self._first_deriv_eval)

@cached_property
def second_derivative(self) -> Vector3D:
"""Second derivative of the evaluation."""
return Vector3D(self._second_deriv_eval)

@cached_property
def curvature(self) -> Real:
"""Curvature of the evaluation."""
# For a curve, the curvature is the magnitude of the cross product
# of the first and second derivatives divided by the cube of the
# magnitude of the first derivative. For more information, please refer
# to https://en.wikipedia.org/wiki/Curvature#General_expressions.
return (
self.first_derivative.cross(self.second_derivative).magnitude
/ self.first_derivative.magnitude**3
)
1 change: 1 addition & 0 deletions src/ansys/geometry/core/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@
)
from ansys.geometry.core.tools.repair_tool_message import RepairToolMessage
from ansys.geometry.core.tools.repair_tools import RepairTools
from ansys.geometry.core.tools.unsupported import PersistentIdType, UnsupportedCommands
Loading

0 comments on commit fc775ec

Please sign in to comment.