From f5e93d64572b697374c3878f5a5f960f32d754a6 Mon Sep 17 00:00:00 2001 From: Mike Kaplan <133800723+mlkaplan36@users.noreply.github.com> Date: Sun, 26 Jan 2025 23:14:30 -0800 Subject: [PATCH 1/3] test: add unit tests for 3 repair tools (#1683) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Co-authored-by: rward --- doc/changelog.d/1683.test.md | 1 + src/ansys/geometry/core/tools/repair_tools.py | 30 +++++++------- tests/integration/test_repair_tools.py | 39 +++++++++++++++++-- 3 files changed, 52 insertions(+), 18 deletions(-) create mode 100644 doc/changelog.d/1683.test.md diff --git a/doc/changelog.d/1683.test.md b/doc/changelog.d/1683.test.md new file mode 100644 index 0000000000..83520edc2f --- /dev/null +++ b/doc/changelog.d/1683.test.md @@ -0,0 +1 @@ +add unit tests for 3 repair tools \ No newline at end of file diff --git a/src/ansys/geometry/core/tools/repair_tools.py b/src/ansys/geometry/core/tools/repair_tools.py index 811d4217e2..c019f47908 100644 --- a/src/ansys/geometry/core/tools/repair_tools.py +++ b/src/ansys/geometry/core/tools/repair_tools.py @@ -465,6 +465,8 @@ def find_and_fix_short_edges( RepairToolMessage Message containing created and/or modified bodies. """ + from ansys.geometry.core.designer.body import Body + check_type_all_elements_in_iterable(bodies, Body) check_type(length, Real) @@ -481,17 +483,15 @@ def find_and_fix_short_edges( parent_design = get_design_from_body(bodies[0]) parent_design._update_design_inplace() message = RepairToolMessage( - response.result.success, - response.result.created_bodies_monikers, - response.result.modified_bodies_monikers, + response.success, + response.created_bodies_monikers, + response.modified_bodies_monikers, ) return message @protect_grpc @min_backend_version(25, 2, 0) - def find_and_fix_extra_edges( - self, bodies: list["Body"], length: Real = 0.0 - ) -> RepairToolMessage: + def find_and_fix_extra_edges(self, bodies: list["Body"]) -> RepairToolMessage: """Find and fix the extra edge problem areas. This method finds the extra edges in the bodies and fixes them. @@ -508,8 +508,9 @@ def find_and_fix_extra_edges( RepairToolMessage Message containing created and/or modified bodies. """ + from ansys.geometry.core.designer.body import Body + check_type_all_elements_in_iterable(bodies, Body) - check_type(length, Real) if not bodies: return RepairToolMessage(False, [], []) @@ -517,16 +518,15 @@ def find_and_fix_extra_edges( response = self._repair_stub.FindAndFixExtraEdges( FindExtraEdgesRequest( selection=[body.id for body in bodies], - max_edge_length=DoubleValue(value=length), ) ) parent_design = get_design_from_body(bodies[0]) parent_design._update_design_inplace() message = RepairToolMessage( - response.result.success, - response.result.created_bodies_monikers, - response.result.modified_bodies_monikers, + response.success, + response.created_bodies_monikers, + response.modified_bodies_monikers, ) return message @@ -553,6 +553,8 @@ def find_and_fix_split_edges( RepairToolMessage Message containing created and/or modified bodies. """ + from ansys.geometry.core.designer.body import Body + check_type_all_elements_in_iterable(bodies, Body) check_type(length, Real) @@ -572,8 +574,8 @@ def find_and_fix_split_edges( parent_design = get_design_from_body(bodies[0]) parent_design._update_design_inplace() message = RepairToolMessage( - response.result.success, - response.result.created_bodies_monikers, - response.result.modified_bodies_monikers, + response.success, + response.created_bodies_monikers, + response.modified_bodies_monikers, ) return message diff --git a/tests/integration/test_repair_tools.py b/tests/integration/test_repair_tools.py index 8e030bf398..82fcbe2b20 100644 --- a/tests/integration/test_repair_tools.py +++ b/tests/integration/test_repair_tools.py @@ -293,7 +293,7 @@ def test_find_and_fix_duplicate_faces(modeler: Modeler): assert len(design.bodies) == 1 -def test_find_and_fix_extra_edges(modeler: Modeler): +def test_find_and_fix_extra_edges_problem_areas(modeler: Modeler): """Test to read geometry, find and fix extra edges and validate they are removed.""" design = modeler.open_file(FILES_DIR / "ExtraEdges_NoComponents.scdocx") assert len(design.bodies) == 3 @@ -365,7 +365,7 @@ def test_find_and_fix_missing_faces(modeler: Modeler): assert not comp.bodies[0].is_surface -def test_find_and_fix_short_edges(modeler: Modeler): +def test_find_and_fix_short_edges_problem_areas(modeler: Modeler): """Test to read geometry, find and fix short edges and validate they are fixed removed.""" design = modeler.open_file(FILES_DIR / "ShortEdges.scdocx") assert len(design.bodies[0].edges) == 685 @@ -376,11 +376,11 @@ def test_find_and_fix_short_edges(modeler: Modeler): assert len(design.bodies[0].edges) == 675 ##We get 673 edges if we repair all in one go -def test_find_and_fix_split_edges(modeler: Modeler): +def test_find_and_fix_split_edges_problem_areas(modeler: Modeler): """Test to read geometry, find and fix split edges and validate they are fixed removed.""" design = modeler.open_file(FILES_DIR / "bracket-with-split-edges.scdocx") assert len(design.bodies[0].edges) == 304 - split_edges = modeler.repair_tools.find_split_edges(design.bodies, 150, 0.0001) + split_edges = modeler.repair_tools.find_split_edges(design.bodies, 2.61799, 0.01) assert len(split_edges) == 166 for i in split_edges: try: # Try/Except is a workaround. Having .alive would be better @@ -419,3 +419,34 @@ def test_fix_simplify(modeler: Modeler): design = modeler.open_file(FILES_DIR / "SOBracket2.scdocx") problem_areas = modeler.repair_tools.find_simplify(design.bodies) assert problem_areas[0].fix().success is True + + +def test_find_and_fix_short_edges(modeler: Modeler): + """Test to read geometry, find and fix short edges and validate they are fixed removed.""" + design = modeler.open_file(FILES_DIR / "ShortEdges.scdocx") + assert len(design.bodies[0].edges) == 685 + modeler.repair_tools.find_and_fix_short_edges(design.bodies, 0.000127) + assert len(design.bodies[0].edges) == 673 ##We get 673 edges if we repair all in one go + + +def test_find_and_fix_split_edges(modeler: Modeler): + """Test to read geometry, find and fix split edges and validate they are fixed removed.""" + design = modeler.open_file(FILES_DIR / "bracket-with-split-edges.scdocx") + assert len(design.bodies[0].edges) == 304 + modeler.repair_tools.find_and_fix_split_edges(design.bodies, 2.61799, 0.01) + assert len(design.bodies[0].edges) == 138 + + +def test_find_and_fix_extra_edges(modeler: Modeler): + """Test to read geometry, find and fix extra edges and validate they are removed.""" + design = modeler.open_file(FILES_DIR / "ExtraEdges_NoComponents.scdocx") + assert len(design.bodies) == 3 + starting_edge_count = 0 + for body in design.bodies: + starting_edge_count += len(body.edges) + assert starting_edge_count == 69 + modeler.repair_tools.find_and_fix_extra_edges(design.bodies) + final_edge_count = 0 + for body in design.bodies: + final_edge_count += len(body.edges) + assert final_edge_count == 36 From f746d510a2a861ab40b84dbcabd3a8884e6d8bcb Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Sun, 26 Jan 2025 23:43:03 -0800 Subject: [PATCH 2/3] feat: implementation of NURBS curves (#1675) Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/1675.added.md | 1 + pyproject.toml | 3 + src/ansys/geometry/core/shapes/__init__.py | 1 + .../geometry/core/shapes/curves/__init__.py | 1 + .../geometry/core/shapes/curves/nurbs.py | 310 ++++++++++++++++++ tests/test_primitives.py | 111 ++++++- 6 files changed, 426 insertions(+), 1 deletion(-) create mode 100644 doc/changelog.d/1675.added.md create mode 100644 src/ansys/geometry/core/shapes/curves/nurbs.py diff --git a/doc/changelog.d/1675.added.md b/doc/changelog.d/1675.added.md new file mode 100644 index 0000000000..b54f415cba --- /dev/null +++ b/doc/changelog.d/1675.added.md @@ -0,0 +1 @@ +implementation of NURBS curves \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 3ff6bb03a1..098b6919c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", @@ -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", @@ -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", diff --git a/src/ansys/geometry/core/shapes/__init__.py b/src/ansys/geometry/core/shapes/__init__.py index ebf2466234..64ebb44e03 100644 --- a/src/ansys/geometry/core/shapes/__init__.py +++ b/src/ansys/geometry/core/shapes/__init__.py @@ -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, diff --git a/src/ansys/geometry/core/shapes/curves/__init__.py b/src/ansys/geometry/core/shapes/curves/__init__.py index 2658b3f654..b786b16208 100644 --- a/src/ansys/geometry/core/shapes/curves/__init__.py +++ b/src/ansys/geometry/core/shapes/curves/__init__.py @@ -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 diff --git a/src/ansys/geometry/core/shapes/curves/nurbs.py b/src/ansys/geometry/core/shapes/curves/nurbs.py new file mode 100644 index 0000000000..36005cc7a4 --- /dev/null +++ b/src/ansys/geometry/core/shapes/curves/nurbs.py @@ -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 `_, + 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 + ) diff --git a/tests/test_primitives.py b/tests/test_primitives.py index 72156290d5..f412dba58d 100644 --- a/tests/test_primitives.py +++ b/tests/test_primitives.py @@ -35,7 +35,17 @@ Vector3D, ) from ansys.geometry.core.misc import DEFAULT_UNITS, UNITS, Accuracy, Distance -from ansys.geometry.core.shapes import Circle, Cone, Cylinder, Ellipse, Line, ParamUV, Sphere, Torus +from ansys.geometry.core.shapes import ( + Circle, + Cone, + Cylinder, + Ellipse, + Line, + NURBSCurve, + ParamUV, + Sphere, + Torus, +) def test_cylinder(): @@ -917,3 +927,102 @@ def test_ellipse_evaluation(): ) assert Accuracy.length_is_equal(eval2.curvature, 0.31540327) + + +def test_nurbs_curve_from_control_points(): + """``NURBSCurve`` construction from control points.""" + control_points = [ + Point3D([0, 0, 0]), + Point3D([1, 1, 0]), + Point3D([2, 0, 0]), + ] + degree = 2 + knots = [0, 0, 0, 1, 1, 1] + nurbs_curve = NURBSCurve.from_control_points( + control_points=control_points, degree=degree, knots=knots + ) + assert nurbs_curve.degree == 2 + assert nurbs_curve.knots == [0, 0, 0, 1, 1, 1] + assert nurbs_curve.control_points == control_points + assert nurbs_curve.weights == [1, 1, 1] + + # Test with a different weight vector + weights = [1, 2, 1] + nurbs_curve_weights = NURBSCurve.from_control_points( + control_points=control_points, degree=degree, knots=knots, weights=weights + ) + + assert nurbs_curve_weights.degree == 2 + assert nurbs_curve_weights.knots == [0, 0, 0, 1, 1, 1] + assert nurbs_curve_weights.control_points == control_points + assert nurbs_curve_weights.weights == weights + + # Verify that the curves are different + assert nurbs_curve != nurbs_curve_weights + + +def test_nurbs_curve_evaluation(): + """``NURBSCurve`` evaluation.""" + control_points = [ + Point3D([0, 0, 0]), + Point3D([1, 1, 0]), + Point3D([2, 0, 0]), + ] + degree = 2 + knots = [0, 0, 0, 1, 1, 1] + nurbs_curve = NURBSCurve.from_control_points( + control_points=control_points, degree=degree, knots=knots + ) + + # Test evaluation at 0 + eval = nurbs_curve.evaluate(0) + assert eval is not None + assert eval.is_set() is True + assert eval.parameter == 0 + assert eval.position == Point3D([0, 0, 0]) + assert eval.first_derivative == Vector3D([2, 2, 0]) + assert eval.second_derivative == Vector3D([0, -4, 0]) + assert np.isclose(eval.curvature, 0.3535533905932737) + + # Test evaluation at 0.5 + eval = nurbs_curve.evaluate(0.5) + assert eval is not None + assert eval.is_set() is True + assert eval.parameter == 0.5 + assert eval.position == Point3D([1, 0.5, 0]) + assert eval.first_derivative == Vector3D([2, 0, 0]) + assert eval.second_derivative == Vector3D([0, -4, 0]) + assert np.isclose(eval.curvature, 1) + + # Test evaluation at 1 + eval = nurbs_curve.evaluate(1) + assert eval is not None + assert eval.is_set() is True + assert eval.parameter == 1 + assert eval.position == Point3D([2, 0, 0]) + assert eval.first_derivative == Vector3D([2, -2, 0]) + assert eval.second_derivative == Vector3D([0, -4, 0]) + assert np.isclose(eval.curvature, 0.3535533905932737) + + +def test_nurbs_curve_point_projection(): + # Define the NUTBS curve + control_points = [ + Point3D([0, 0, 0]), + Point3D([1, 1, 0]), + Point3D([2, 0, 0]), + ] + degree = 2 + knots = [0, 0, 0, 1, 1, 1] + nurbs_curve = NURBSCurve.from_control_points( + control_points=control_points, degree=degree, knots=knots + ) + + # Test projection of a point on the curve + point = Point3D([1, 3, 0]) + projection = nurbs_curve.project_point(point, initial_guess=0.1) + + assert projection is not None + assert projection.is_set() is True + assert np.allclose(projection.position, Point3D([1, 0.5, 0])) + assert np.isclose(projection.parameter, 0.5) From 91054496b409dab16569d24d8aa8b62a65a27fba Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Mon, 27 Jan 2025 02:34:43 -0800 Subject: [PATCH 3/3] fix: cleanup unsupported module (#1690) Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/1690.fixed.md | 1 + src/ansys/geometry/core/modeler.py | 8 ++--- src/ansys/geometry/core/tools/__init__.py | 1 + .../core/{misc => tools}/unsupported.py | 33 ++++++++++--------- tests/integration/test_design_import.py | 2 +- 5 files changed, 24 insertions(+), 21 deletions(-) create mode 100644 doc/changelog.d/1690.fixed.md rename src/ansys/geometry/core/{misc => tools}/unsupported.py (87%) diff --git a/doc/changelog.d/1690.fixed.md b/doc/changelog.d/1690.fixed.md new file mode 100644 index 0000000000..1e248a37f7 --- /dev/null +++ b/doc/changelog.d/1690.fixed.md @@ -0,0 +1 @@ +cleanup unsupported module \ No newline at end of file diff --git a/src/ansys/geometry/core/modeler.py b/src/ansys/geometry/core/modeler.py index dca6947d46..948d136ebd 100644 --- a/src/ansys/geometry/core/modeler.py +++ b/src/ansys/geometry/core/modeler.py @@ -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 @@ -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 @@ -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( diff --git a/src/ansys/geometry/core/tools/__init__.py b/src/ansys/geometry/core/tools/__init__.py index 8ce8cc3652..de96b4cc0c 100644 --- a/src/ansys/geometry/core/tools/__init__.py +++ b/src/ansys/geometry/core/tools/__init__.py @@ -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 diff --git a/src/ansys/geometry/core/misc/unsupported.py b/src/ansys/geometry/core/tools/unsupported.py similarity index 87% rename from src/ansys/geometry/core/misc/unsupported.py rename to src/ansys/geometry/core/tools/unsupported.py index 0afd9f884f..2e45e09fd8 100644 --- a/src/ansys/geometry/core/misc/unsupported.py +++ b/src/ansys/geometry/core/tools/unsupported.py @@ -29,7 +29,7 @@ from ansys.api.geometry.v0.unsupported_pb2_grpc import UnsupportedStub from ansys.geometry.core.connection import GrpcClient from ansys.geometry.core.errors import protect_grpc -from ansys.geometry.core.misc import auxiliary +from ansys.geometry.core.misc.auxiliary import get_all_bodies_from_design from ansys.geometry.core.misc.checks import ( min_backend_version, ) @@ -38,6 +38,7 @@ from ansys.geometry.core.designer.body import Body from ansys.geometry.core.designer.edge import Edge from ansys.geometry.core.designer.face import Face + from ansys.geometry.core.modeler import Modeler @unique @@ -58,17 +59,17 @@ class UnsupportedCommands: """ @protect_grpc - def __init__(self, grpc_client: GrpcClient, modeler): + def __init__(self, grpc_client: GrpcClient, modeler: "Modeler"): """Initialize an instance of the ``UnsupportedCommands`` class.""" self._grpc_client = grpc_client self._unsupported_stub = UnsupportedStub(self._grpc_client.channel) self.__id_map = {} self.__modeler = modeler - self.__current_design = None + self.__current_design = modeler.get_active_design() @protect_grpc @min_backend_version(25, 2, 0) - def __fill_imported_id_map(self, id_type: "PersistentIdType") -> None: + def __fill_imported_id_map(self, id_type: PersistentIdType) -> None: """Populate the persistent id map for caching. Parameters @@ -96,7 +97,7 @@ def __clear_cache(self) -> None: @protect_grpc @min_backend_version(25, 2, 0) - def __is_occurrence(self, master: "EntityIdentifier", occ: "str") -> bool: + def __is_occurrence(self, master: EntityIdentifier, occ: str) -> bool: """Determine if the master is the master of the occurrence. Parameters @@ -118,7 +119,7 @@ def __is_occurrence(self, master: "EntityIdentifier", occ: "str") -> bool: @protect_grpc @min_backend_version(25, 2, 0) def __get_moniker_from_import_id( - self, id_type: "PersistentIdType", import_id: "str" + self, id_type: PersistentIdType, import_id: str ) -> "EntityIdentifier | None": """Look up the moniker from the id map. @@ -148,7 +149,7 @@ def __get_moniker_from_import_id( @protect_grpc @min_backend_version(25, 2, 0) - def set_export_id(self, moniker: "str", id_type: "PersistentIdType", value: "str") -> None: + def set_export_id(self, moniker: str, id_type: PersistentIdType, value: str) -> None: """Set the persistent id for the moniker. Parameters @@ -175,7 +176,7 @@ def set_export_id(self, moniker: "str", id_type: "PersistentIdType", value: "str @protect_grpc @min_backend_version(25, 2, 0) def get_body_occurrences_from_import_id( - self, import_id: "str", id_type: "PersistentIdType" + self, import_id: str, id_type: PersistentIdType ) -> list["Body"]: """Get all body occurrences whose master has the given import id. @@ -194,19 +195,19 @@ def get_body_occurrences_from_import_id( moniker = self.__get_moniker_from_import_id(id_type, import_id) if moniker is None: - return list() + return [] design = self.__modeler.get_active_design() return [ body - for body in auxiliary.get_all_bodies_from_design(design) + for body in get_all_bodies_from_design(design) if self.__is_occurrence(moniker, body.id) ] @protect_grpc @min_backend_version(25, 2, 0) def get_face_occurrences_from_import_id( - self, import_id: "str", id_type: "PersistentIdType" + self, import_id: str, id_type: PersistentIdType ) -> list["Face"]: """Get all face occurrences whose master has the given import id. @@ -225,12 +226,12 @@ def get_face_occurrences_from_import_id( moniker = self.__get_moniker_from_import_id(id_type, import_id) if moniker is None: - return list() + return [] design = self.__modeler.get_active_design() return [ face - for body in auxiliary.get_all_bodies_from_design(design) + for body in get_all_bodies_from_design(design) for face in body.faces if self.__is_occurrence(moniker, face.id) ] @@ -238,7 +239,7 @@ def get_face_occurrences_from_import_id( @protect_grpc @min_backend_version(25, 2, 0) def get_edge_occurrences_from_import_id( - self, import_id: "str", id_type: "PersistentIdType" + self, import_id: str, id_type: PersistentIdType ) -> list["Edge"]: """Get all edge occurrences whose master has the given import id. @@ -257,12 +258,12 @@ def get_edge_occurrences_from_import_id( moniker = self.__get_moniker_from_import_id(id_type, import_id) if moniker is None: - return list() + return [] design = self.__modeler.get_active_design() return [ edge - for body in auxiliary.get_all_bodies_from_design(design) + for body in get_all_bodies_from_design(design) for edge in body.edges if self.__is_occurrence(moniker, edge.id) ] diff --git a/tests/integration/test_design_import.py b/tests/integration/test_design_import.py index 9136262b6a..822fa7a047 100644 --- a/tests/integration/test_design_import.py +++ b/tests/integration/test_design_import.py @@ -33,8 +33,8 @@ from ansys.geometry.core.designer.design import DesignFileFormat from ansys.geometry.core.math import Plane, Point2D, Point3D, UnitVector3D, Vector3D from ansys.geometry.core.misc import UNITS -from ansys.geometry.core.misc.unsupported import PersistentIdType from ansys.geometry.core.sketch import Sketch +from ansys.geometry.core.tools.unsupported import PersistentIdType from .conftest import FILES_DIR, IMPORT_FILES_DIR, skip_if_linux