diff --git a/doc/changelog.d/1340.added.md b/doc/changelog.d/1340.added.md new file mode 100644 index 0000000000..7973c35e01 --- /dev/null +++ b/doc/changelog.d/1340.added.md @@ -0,0 +1 @@ +driving dimensions \ No newline at end of file diff --git a/doc/source/_static/thumbnails/block_with_parameters.png b/doc/source/_static/thumbnails/block_with_parameters.png new file mode 100644 index 0000000000..f9a40a79eb Binary files /dev/null and b/doc/source/_static/thumbnails/block_with_parameters.png differ diff --git a/doc/source/conf.py b/doc/source/conf.py index b775877604..59090e8b45 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -294,6 +294,7 @@ def intersphinx_pyansys_geometry(switcher_version: str): "examples/03_modeling/design_tree": "_static/thumbnails/design_tree.png", "examples/03_modeling/service_colors": "_static/thumbnails/service_colors.png", "examples/03_modeling/surface_bodies": "_static/thumbnails/quarter_sphere.png", + "examples/03_modeling/design_parameters": "_static/thumbnails/block_with_parameters.png", "examples/04_applied/01_naca_airfoils": "_static/thumbnails/naca_airfoils.png", "examples/04_applied/02_naca_fluent": "_static/thumbnails/naca_fluent.png", } diff --git a/doc/source/examples.rst b/doc/source/examples.rst index fc4c5abb1d..860bed6a18 100644 --- a/doc/source/examples.rst +++ b/doc/source/examples.rst @@ -49,6 +49,8 @@ These examples demonstrate service-based modeling operations. examples/03_modeling/design_tree.mystnb examples/03_modeling/service_colors.mystnb examples/03_modeling/surface_bodies.mystnb + examples/03_modeling/design_parameters.mystnb + Applied examples ---------------- diff --git a/doc/source/examples/03_modeling/design_parameters.mystnb b/doc/source/examples/03_modeling/design_parameters.mystnb new file mode 100644 index 0000000000..f83d22ca40 --- /dev/null +++ b/doc/source/examples/03_modeling/design_parameters.mystnb @@ -0,0 +1,145 @@ +--- +jupytext: + text_representation: + extension: .mystnb + format_name: myst + format_version: 0.13 + jupytext_version: 1.16.4 +kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + +# Modeling: Using design parameters + +You can read and update parameters that are part of the design. +In this example, we have a simple design with two associated parameters. + ++++ + +## Perform required imports + +```{code-cell} ipython3 +import os +import requests +from ansys.geometry.core import launch_modeler +from ansys.geometry.core.modeler import * +from ansys.geometry.core.parameters import * +``` + +The file for this example is in the integration tests folder, so let's download it. + ++++ + +## Download the example file + ++++ + +Download the file for this example from the integration tests folder in the PyAnsys Geometry repository. + +```{code-cell} ipython3 +import requests + +def download_file(url, filename): + """Download a file from a URL and save it to a local file.""" + response = requests.get(url) + response.raise_for_status() # Check if the request was successful + with open(filename, 'wb') as file: + file.write(response.content) + +# URL of the file to download +url = "https://github.com/ansys/pyansys-geometry/blob/main/tests/integration/files/blockswithparameters.dsco" + +# Local path where the file will be saved +file_name = "blockswithparameters.dsco" +current_path = os.getcwd() +file_path = os.path.join(current_path, file_name) +# Download the file +download_file(url, file_path) +print("File is downloaded to " + file_path) +``` + +## Import a design with parameters + ++++ + +Importing the model using ``open_file`` method of the modeler. + +```{code-cell} ipython3 +# Create a modeler object +modeler = launch_modeler() +design = modeler.open_file(file_path) +design.plot() +``` + +## Read existing parameters of the design + +You can get all the parameters of the design as a list of parameters. Because this example has two parameters, you see the two items in the list. + +```{code-cell} ipython3 +my_parameters = design.get_all_parameters() +print(len(my_parameters)) +``` + +A parameter object has a name, value, and unit. + +```{code-cell} ipython3 +print(my_parameters[0].name) +print(my_parameters[0].dimension_value) +print(my_parameters[0].dimension_type) + +print(my_parameters[1].name) +print(my_parameters[1].dimension_value) +print(my_parameters[1].dimension_type) +``` + +Parameter values are returned in the default unit for each dimension type. Since default length unit is meter and default area unit is meter square, the value is returned in metersquare. + ++++ + +## Edit a parameter value + +You can edit the parameters name or value by simply setting these fields. Let's +set the second parameter (p2 value to 350 mm. ) + +```{code-cell} ipython3 +parameter1 = my_parameters[1] +parameter1.dimension_value = 0.000440 +response = design.set_parameter(parameter1) +print(response) +print(my_parameters[0].dimension_value) +print(my_parameters[1].dimension_value) +``` + +After a successful parameter update the design in the backend might have been updated. Therefore you need to refresh the design on the client. + +```{code-cell} ipython3 +design = modeler.read_existing_design() +design.plot() +``` + +The ``set_parameter()`` method returns a ``Success`` status message if the parameter is updated or a "FAILURE" status message if the update fails. If the ``p2`` parameter depends on the ``p1`` parameter, updating the ``p1`` parameter might also change the ``p2`` parameter. In such cases, the method returns ``CONSTRAINED_PARAMETERS``, which indicates other parameters were also updated. + +```{code-cell} ipython3 +parameter1 = my_parameters[0] +parameter1.dimension_value = 0.000250 +response = design.set_parameter(parameter1) +print(response) +``` + +Therefore user can query the parameters once again to get updated list. + +```{code-cell} ipython3 +my_parameters = design.get_all_parameters() +print(my_parameters[0].dimension_value) +print(my_parameters[1].dimension_value) +``` + +## Close the modeler + +Close the modeler to free up resources and release the connection. + +```{code-cell} ipython3 +modeler.close() +``` diff --git a/src/ansys/geometry/core/designer/design.py b/src/ansys/geometry/core/designer/design.py index 449c4970ee..82069e27ce 100644 --- a/src/ansys/geometry/core/designer/design.py +++ b/src/ansys/geometry/core/designer/design.py @@ -33,6 +33,8 @@ from ansys.api.dbu.v0.dbumodels_pb2 import EntityIdentifier, PartExportFormat from ansys.api.dbu.v0.designs_pb2 import InsertRequest, NewRequest, SaveAsRequest from ansys.api.dbu.v0.designs_pb2_grpc import DesignsStub +from ansys.api.dbu.v0.drivingdimensions_pb2 import GetAllRequest, UpdateRequest +from ansys.api.dbu.v0.drivingdimensions_pb2_grpc import DrivingDimensionsStub from ansys.api.geometry.v0.commands_pb2 import ( AssignMidSurfaceOffsetTypeRequest, AssignMidSurfaceThicknessRequest, @@ -74,6 +76,7 @@ from ansys.geometry.core.misc.checks import ensure_design_is_active, min_backend_version from ansys.geometry.core.misc.measurements import DEFAULT_UNITS, Distance from ansys.geometry.core.modeler import Modeler +from ansys.geometry.core.parameters.parameter import Parameter, ParameterUpdateStatus from ansys.geometry.core.typing import RealSequence @@ -125,6 +128,7 @@ def __init__(self, name: str, modeler: Modeler, read_existing_design: bool = Fal self._materials_stub = MaterialsStub(self._grpc_client.channel) self._named_selections_stub = NamedSelectionsStub(self._grpc_client.channel) self._parts_stub = PartsStub(self._grpc_client.channel) + self._parameters_stub = DrivingDimensionsStub(self._grpc_client.channel) # Initialize needed instance variables self._materials = [] @@ -166,6 +170,11 @@ def beam_profiles(self) -> list[BeamProfile]: """List of beam profile available for the design.""" return list(self._beam_profiles.values()) + @property + def parameters(self) -> list[Parameter]: + """List of parameters available for the design.""" + return self.get_all_parameters() + @property def is_active(self) -> bool: """Whether the design is currently active.""" @@ -679,6 +688,45 @@ def add_beam_circular_profile( return self._beam_profiles[profile.name] + @protect_grpc + @min_backend_version(25, 1, 0) + def get_all_parameters(self) -> list[Parameter]: + """Get parameters for the design. + + Returns + ------- + list[Parameter] + List of parameters for the design. + """ + response = self._parameters_stub.GetAll(GetAllRequest()) + return [Parameter._from_proto(dimension) for dimension in response.driving_dimensions] + + @protect_grpc + @check_input_types + @min_backend_version(25, 1, 0) + def set_parameter(self, dimension: Parameter) -> ParameterUpdateStatus: + """Update a parameter of the design. + + Parameters + ---------- + dimension : Parameter + Parameter to set. + + Returns + ------- + ParameterUpdateStatus + Status of the update operation. + """ + request = UpdateRequest(driving_dimension=Parameter._to_proto(dimension)) + response = self._parameters_stub.UpdateParameter(request) + status = response.status + + # Update the design in place. This method is expensive, + # consider finding a more efficient approach. + self._update_design_inplace() + + return ParameterUpdateStatus._from_update_status(status) + @protect_grpc @check_input_types @ensure_design_is_active diff --git a/src/ansys/geometry/core/parameters/__init__.py b/src/ansys/geometry/core/parameters/__init__.py new file mode 100644 index 0000000000..1e6f921cb1 --- /dev/null +++ b/src/ansys/geometry/core/parameters/__init__.py @@ -0,0 +1,24 @@ +# Copyright (C) 2023 - 2024 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. +"""PyAnsys Geometry parameters subpackage.""" + +from ansys.geometry.core.parameters.parameter import Parameter, ParameterType diff --git a/src/ansys/geometry/core/parameters/parameter.py b/src/ansys/geometry/core/parameters/parameter.py new file mode 100644 index 0000000000..cbf0ad6efa --- /dev/null +++ b/src/ansys/geometry/core/parameters/parameter.py @@ -0,0 +1,127 @@ +# Copyright (C) 2023 - 2024 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 get and set methods for parameters.""" + +from enum import Enum, unique + +from ansys.api.dbu.v0.dbumodels_pb2 import DrivingDimension as ParameterProto +from ansys.api.dbu.v0.drivingdimensions_pb2 import UpdateStatus + + +@unique +class ParameterType(Enum): + """Provides values for parameter types supported.""" + + DIMENSIONTYPE_UNKNOWN = 0 + DIMENSIONTYPE_LINEAR = 1 + DIMENSIONTYPE_DIAMETRIC = 2 + DIMENSIONTYPE_RADIAL = 3 + DIMENSIONTYPE_ARC = 4 + DIMENSIONTYPE_AREA = 5 + DIMENSIONTYPE_VOLUME = 6 + DIMENSIONTYPE_MASS = 7 + DIMENSIONTYPE_ANGULAR = 8 + DIMENSIONTYPE_COUNT = 9 + DIMENSIONTYPE_UNITLESS = 10 + + +@unique +class ParameterUpdateStatus(Enum): + """Provides values for the status messages associated with parameter updates.""" + + SUCCESS = 0 + FAILURE = 1 + CONSTRAINED_PARAMETERS = 2 + UNKNOWN = 3 + + @staticmethod + def _from_update_status(status): + """Convert UpdateStatus to ParameterUpdateStatus.""" + status_mapping = { + UpdateStatus.SUCCESS: ParameterUpdateStatus.SUCCESS, + UpdateStatus.FAILURE: ParameterUpdateStatus.FAILURE, + UpdateStatus.CONSTRAINED_PARAMETERS: ParameterUpdateStatus.CONSTRAINED_PARAMETERS, + } + return status_mapping.get(status, ParameterUpdateStatus.UNKNOWN) + + +class Parameter: + """Represents a parameter.""" + + def __init__(self, id, name, dimension_type: ParameterType, dimension_value): + """ + Initialize an instance of the ``Parameter`` class. + + Parameters + ---------- + id : int + Unique ID for the parameter. + name : str + Name of the parameter. + dimension_type : ParameterType + Type of the parameter. + dimension_value : float + Value of the parameter. + """ + self.id = id + self._name = name + self.dimension_type = dimension_type + self._dimension_value = dimension_value + + @classmethod + def _from_proto(cls, proto): + """Create a ``Parameter`` instance from a proto object.""" + return cls( + id=proto.id, + name=proto.name, + dimension_type=ParameterType(proto.dimension_type), + dimension_value=proto.dimension_value, + ) + + @property + def name(self) -> str: + """Get the name of the parameter.""" + return self._name + + @name.setter + def name(self, value: str): + """Set the name of the parameter.""" + self._name = value + + @property + def dimension_value(self) -> float: + """Get the value of the parameter.""" + return self._dimension_value + + @dimension_value.setter + def dimension_value(self, value): + """Set the value of the parameter.""" + self._dimension_value = value + + def _to_proto(self): + """Convert a ``Parameter`` instance to a proto object.""" + return ParameterProto( + id=self.id, + name=self.name, + dimension_type=self.dimension_type.value, + dimension_value=self.dimension_value, + ) diff --git a/tests/integration/test_design.py b/tests/integration/test_design.py index cb99684d70..aa2250cfd1 100644 --- a/tests/integration/test_design.py +++ b/tests/integration/test_design.py @@ -56,6 +56,7 @@ Vector3D, ) from ansys.geometry.core.misc import DEFAULT_UNITS, UNITS, Accuracy, Angle, Distance +from ansys.geometry.core.parameters.parameter import ParameterType, ParameterUpdateStatus from ansys.geometry.core.shapes import ( Circle, Cone, @@ -2739,6 +2740,33 @@ def test_surface_body_creation(modeler: Modeler): assert body.faces[0].area.m == pytest.approx(39.4784176044 * 2) +def test_design_parameters(modeler: Modeler): + """Test the design parameter's functionality.""" + + design = modeler.open_file(FILES_DIR / "blockswithparameters.dsco") + test_parameters = design.get_all_parameters() + + # Verify the initial parameters + assert len(test_parameters) == 2 + assert test_parameters[0].name == "p1" + assert abs(test_parameters[0].dimension_value - 0.00010872999999999981) < 1e-8 + assert test_parameters[0].dimension_type == ParameterType.DIMENSIONTYPE_AREA + + assert test_parameters[1].name == "p2" + assert abs(test_parameters[1].dimension_value - 0.0002552758322160813) < 1e-8 + assert test_parameters[1].dimension_type == ParameterType.DIMENSIONTYPE_AREA + + # Update the second parameter and verify the status + test_parameters[1].dimension_value = 0.0006 + status = design.set_parameter(test_parameters[1]) + assert status == ParameterUpdateStatus.SUCCESS + + # Attempt to update the first parameter and expect a constrained status + test_parameters[0].dimension_value = 0.0006 + status = design.set_parameter(test_parameters[0]) + assert status == ParameterUpdateStatus.CONSTRAINED_PARAMETERS + + def test_cached_bodies(modeler: Modeler): """Test verifying that bodies are cached correctly.