From 2da9dff678acddd4b7bd4a6213b9e0644473a988 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Sep 2023 08:22:05 +0200 Subject: [PATCH 01/74] MAINT: Bump the docs-deps group with 2 updates (#763) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a1c151c6d8..6bbb81255f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,7 +70,7 @@ tests = [ "vtk==9.2.6", ] doc = [ - "ansys-sphinx-theme==0.11.2", + "ansys-sphinx-theme==0.12.0", "docker==6.1.3", "ipyvtklink==0.2.3", "jupyter_sphinx==0.4.0", @@ -79,7 +79,7 @@ doc = [ "nbconvert==7.8.0", "nbsphinx==0.9.3", "notebook==7.0.4", - "numpydoc==1.5.0", + "numpydoc==1.6.0", "panel==1.2.3", "pyvista[trame]==0.41.1", "requests==2.31.0", From a410c85c38c4f9c3b759535b6ae1a229932797e1 Mon Sep 17 00:00:00 2001 From: Revathy Venugopal <104772255+Revathyvenugopal162@users.noreply.github.com> Date: Wed, 27 Sep 2023 08:55:20 -0400 Subject: [PATCH 02/74] doc: enhance search engine using pymeilisearch (#765) --- .github/workflows/ci_cd.yml | 44 +++++++++++++++++++++++++++++++++++++ doc/source/conf.py | 6 +++++ 2 files changed, 50 insertions(+) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 51fa501e4e..16654883e7 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -20,6 +20,8 @@ env: RESET_IMAGE_CACHE: 6 IS_WORKFLOW_RUNNING: True ARTIFACTORY_VERSION: v241 + MEILISEARCH_API_KEY: ${{ secrets.MEILISEARCH_API_KEY }} + MEILISEARCH_PUBLIC_API_KEY: ${{ secrets.MEILISEARCH_PUBLIC_API_KEY }} concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -604,6 +606,20 @@ jobs: cname: ${{ env.DOCUMENTATION_CNAME }} token: ${{ secrets.GITHUB_TOKEN }} + doc-index-dev: + name: "Deploy dev index docs" + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + needs: upload_dev_docs + steps: + - name: "Deploy the latest documentation index" + uses: ansys/actions/doc-deploy-index@v4 + with: + cname: ${{ env.DOCUMENTATION_CNAME }}/version/dev + index-name: pyansys-geometry-vdev + host-url: ${{ env.MEILISEARCH_HOST_URL }} + api-key: ${{ env.MEILISEARCH_API_KEY }} + upload_docs_release: name: Upload release documentation if: github.event_name == 'push' && contains(github.ref, 'refs/tags') @@ -615,3 +631,31 @@ jobs: with: cname: ${{ env.DOCUMENTATION_CNAME }} token: ${{ secrets.GITHUB_TOKEN }} + + doc-index-stable: + name: "Deploy stable docs index" + runs-on: ubuntu-latest + needs: upload_docs_release + steps: + - name: "Install Git and clone project" + uses: actions/checkout@v4 + + - name: "Install the package requirements" + run: pip install -e . + + - name: "Get the version to PyMeilisearch" + run: | + VERSION=$(python -c "from ansys.geometry.core import __version__; print('.'.join(__version__.split('.')[:2]))") + VERSION_MEILI=$(python -c "from ansys.geometry.core import __version__; print('-'.join(__version__.split('.')[:2]))") + echo "Calculated VERSION: $VERSION" + echo "Calculated VERSION_MEILI: $VERSION_MEILI" + echo "VERSION=$VERSION" >> $GITHUB_ENV + echo "VERSION_MEILI=$VERSION_MEILI" >> $GITHUB_ENV + + - name: "Deploy the latest documentation index" + uses: ansys/actions/doc-deploy-index@v4 + with: + cname: ${{ env.DOCUMENTATION_CNAME }}/version/${{ env.VERSION }} + index-name: pyansys-geometry-v${{ env.VERSION_MEILI }} + host-url: ${{ vars.MEILISEARCH_HOST_URL }} + api-key: ${{ env.MEILISEARCH_API_KEY }} \ No newline at end of file diff --git a/doc/source/conf.py b/doc/source/conf.py index 2065178eb3..14e2e4c97f 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -61,6 +61,12 @@ "icon": "fa fa-comment fa-fw", }, ], + "use_meilisearch": { + "api_key": os.getenv("MEILISEARCH_PUBLIC_API_KEY", ""), + "index_uids": { + f"pyansys-geometry-v{get_version_match(__version__).replace('.', '-')}": "PyAnsys-Geometry", # noqa: E501 + }, + }, } # Sphinx extensions From e78c5bd41d86c87336ca9795bac62089586ddb67 Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Wed, 27 Sep 2023 16:36:48 +0200 Subject: [PATCH 03/74] feat: adding assets page to documentation (#764) --- .github/workflows/ci_cd.yml | 40 +++++++++++- README.rst | 2 +- doc/make.bat | 2 +- doc/source/_static/assets/download/.gitignore | 3 + doc/source/_static/assets/download/README.txt | 1 + doc/source/_static/assets/index_download.png | Bin 0 -> 8197 bytes doc/source/_static/assets/index_download.svg | 7 +++ doc/source/assets.rst | 59 ++++++++++++++++++ doc/source/conf.py | 7 ++- doc/source/index.rst | 19 +++++- 10 files changed, 134 insertions(+), 6 deletions(-) create mode 100644 doc/source/_static/assets/download/.gitignore create mode 100644 doc/source/_static/assets/download/README.txt create mode 100644 doc/source/_static/assets/index_download.png create mode 100644 doc/source/_static/assets/index_download.svg create mode 100644 doc/source/assets.rst diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 16654883e7..2fcd8c8a79 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -380,6 +380,42 @@ jobs: library-name: ${{ env.PACKAGE_NAME }} python-version: ${{ env.MAIN_PYTHON_VERSION }} + add-assets-to-docs: + name: Add downloadable assets to docs + needs: [testing-windows, testing-linux, docs] + runs-on: ubuntu-latest + steps: + - name: "Download all artifacts" + uses: actions/download-artifact@v3 + with: + path: /tmp/artifacts + + - name: "Compressing HTML docs to ZIP file" + run: | + zip -r /tmp/artifacts/documentation-html.zip /tmp/artifacts/documentation-html + + - name: "Download HTML documentation" + uses: actions/download-artifact@v3 + with: + name: documentation-html + path: /tmp/documentation-html + + - name: "Fill the HTML docs with assets" + run: | + # Move the docs in HTML format + mv /tmp/artifacts/documentation-html.zip /tmp/documentation-html/_static/assets/download/ + # Move the docs in PDF format + mv /tmp/artifacts/documentation-pdf/* /tmp/documentation-html/_static/assets/download/ + # Move the wheelhouses + mv /tmp/artifacts/**/*-wheelhouse-*.zip /tmp/documentation-html/_static/assets/download/ + + - name: "Upload HTML documentation" + uses: actions/upload-artifact@v3 + with: + name: documentation-html + path: /tmp/documentation-html + retention-days: 7 + # ================================================================================================= # vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv RUNNING ON SELF-HOSTED RUNNER vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv # ================================================================================================= @@ -578,7 +614,7 @@ jobs: release: name: Release project if: github.event_name == 'push' && contains(github.ref, 'refs/tags') - needs: [package, build-windows-container, build-linux-container] + needs: [package, add-assets-to-docs, build-windows-container, build-linux-container] runs-on: ubuntu-latest steps: - name: Release to the public PyPI repository @@ -598,7 +634,7 @@ jobs: name: Upload dev documentation if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest - needs: [package] + needs: [package, add-assets-to-docs] steps: - name: Deploy the latest documentation uses: ansys/actions/doc-deploy-dev@v4 diff --git a/README.rst b/README.rst index e421ff4420..6ee42ddde3 100644 --- a/README.rst +++ b/README.rst @@ -146,7 +146,7 @@ For example, on Linux with Python 3.8, unzip the wheelhouse archive and install .. code:: bash - unzip ansys-geometry-core-v0.4.dev0-wheelhouse-Linux-3.8.zip wheelhouse + unzip ansys-geometry-core-v0.4.dev0-wheelhouse-ubuntu-latest-3.8.zip wheelhouse pip install ansys-geometry-core -f wheelhouse --no-index --upgrade --ignore-installed If you're on Windows with Python 3.9, unzip to a wheelhouse directory and install using the preceding command. diff --git a/doc/make.bat b/doc/make.bat index 0136b3152c..349f0bd594 100644 --- a/doc/make.bat +++ b/doc/make.bat @@ -17,7 +17,7 @@ REM TODO: these lines of code should be removed once the feature branch is merge for /f %%i in ('pip freeze ^| findstr /c:"sphinx-autoapi @ git+https://github.com/ansys/sphinx-autoapi"') do set is_custom_sphinx_autoapi_installed=%%i if NOT "%is_custom_sphinx_autoapi_installed%" == "sphinx-autoapi" ( pip uninstall --yes sphinx-autoapi - pip install "sphinx-autoapi @ git+https://github.com/ansys/sphinx-autoapi@feat/single-page-option") + pip install "sphinx-autoapi @ git+https://github.com/ansys/sphinx-autoapi@feat/single-page-stable") REM TODO: these lines of code should be removed once the feature branch is merged if "%1" == "" goto help diff --git a/doc/source/_static/assets/download/.gitignore b/doc/source/_static/assets/download/.gitignore new file mode 100644 index 0000000000..6e36d76d59 --- /dev/null +++ b/doc/source/_static/assets/download/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!README.txt \ No newline at end of file diff --git a/doc/source/_static/assets/download/README.txt b/doc/source/_static/assets/download/README.txt new file mode 100644 index 0000000000..dc03057dfb --- /dev/null +++ b/doc/source/_static/assets/download/README.txt @@ -0,0 +1 @@ +Downloadable assets will be stored here \ No newline at end of file diff --git a/doc/source/_static/assets/index_download.png b/doc/source/_static/assets/index_download.png new file mode 100644 index 0000000000000000000000000000000000000000..4a1091944ea2f9e956d91396ddd2d137813b0b17 GIT binary patch literal 8197 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 Q`~iUOEdxZArepMf0RU^`zyJUM literal 0 HcmV?d00001 diff --git a/doc/source/_static/assets/index_download.svg b/doc/source/_static/assets/index_download.svg new file mode 100644 index 0000000000..7a3f2e3029 --- /dev/null +++ b/doc/source/_static/assets/index_download.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/doc/source/assets.rst b/doc/source/assets.rst new file mode 100644 index 0000000000..931cb10075 --- /dev/null +++ b/doc/source/assets.rst @@ -0,0 +1,59 @@ +Assets +###### + +In this section, users are able to download a set of assets related to PyAnsys Geometry. + +Documentation +------------- + +The following links will provide users with downloadable documentation in various formats + +* `Documentation in HTML format <_static/assets/download/documentation-html.zip>`_ +* `Documentation in PDF format <_static/assets/download/ansys-geometry-core.pdf>`_ + +Wheelhouse +---------- + +If you lack an internet connection on your installation machine, you should install PyAnsys Geometry +by downloading the wheelhouse archive. + +Each wheelhouse archive contains all the Python wheels necessary to install PyAnsys Geometry from scratch on Windows, +Linux, and MacOS from Python 3.8 to 3.11. You can install this on an isolated system with a fresh Python +installation or on a virtual environment. + +For example, on Linux with Python 3.8, unzip the wheelhouse archive and install it with: + +.. code:: bash + + unzip ansys-geometry-core-v0.4.dev0-wheelhouse-ubuntu-latest3.8.zip wheelhouse + pip install ansys-geometry-core -f wheelhouse --no-index --upgrade --ignore-installed + +If you are on Windows with Python 3.9, unzip to a wheelhouse directory and install using the preceding command. + +Consider installing using a `virtual environment `_. + +The following wheelhouse files are available for download: + +Linux +^^^^^ + +* `Linux wheelhouse for Python 3.8 <_static/assets/download/ansys-geometry-core-v0.4.dev0-wheelhouse-ubuntu-latest-3.8.zip>`_ +* `Linux wheelhouse for Python 3.9 <_static/assets/download/ansys-geometry-core-v0.4.dev0-wheelhouse-ubuntu-latest-3.9.zip>`_ +* `Linux wheelhouse for Python 3.10 <_static/assets/download/ansys-geometry-core-v0.4.dev0-wheelhouse-ubuntu-latest-3.10.zip>`_ +* `Linux wheelhouse for Python 3.11 <_static/assets/download/ansys-geometry-core-v0.4.dev0-wheelhouse-ubuntu-latest-3.11.zip>`_ + +Windows +^^^^^^^ + +* `Windows wheelhouse for Python 3.8 <_static/assets/download/ansys-geometry-core-v0.4.dev0-wheelhouse-windows-latest-3.8.zip>`_ +* `Windows wheelhouse for Python 3.9 <_static/assets/download/ansys-geometry-core-v0.4.dev0-wheelhouse-windows-latest-3.9.zip>`_ +* `Windows wheelhouse for Python 3.10 <_static/assets/download/ansys-geometry-core-v0.4.dev0-wheelhouse-windows-latest-3.10.zip>`_ +* `Windows wheelhouse for Python 3.11 <_static/assets/download/ansys-geometry-core-v0.4.dev0-wheelhouse-windows-latest-3.11.zip>`_ + +MacOS +^^^^^ + +* `MacOS wheelhouse for Python 3.8 <_static/assets/download/ansys-geometry-core-v0.4.dev0-wheelhouse-macos-latest-3.8.zip>`_ +* `MacOS wheelhouse for Python 3.9 <_static/assets/download/ansys-geometry-core-v0.4.dev0-wheelhouse-macos-latest-3.9.zip>`_ +* `MacOS wheelhouse for Python 3.10 <_static/assets/download/ansys-geometry-core-v0.4.dev0-wheelhouse-macos-latest-3.10.zip>`_ +* `MacOS wheelhouse for Python 3.11 <_static/assets/download/ansys-geometry-core-v0.4.dev0-wheelhouse-macos-latest-3.11.zip>`_ diff --git a/doc/source/conf.py b/doc/source/conf.py index 14e2e4c97f..81b25933f0 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -60,6 +60,11 @@ "url": "https://github.com/ansys/pyansys-geometry/discussions", "icon": "fa fa-comment fa-fw", }, + { + "name": "Download documentation in PDF", + "url": f"https://{cname}/version/{switcher_version}/_static/assets/download/ansys-geometry-core.pdf", # noqa: E501 + "icon": "fa fa-file-pdf fa-fw", + }, ], "use_meilisearch": { "api_key": os.getenv("MEILISEARCH_PUBLIC_API_KEY", ""), @@ -215,7 +220,7 @@ # variables are the title of pdf, watermark latex_elements = {"preamble": latex.generate_preamble(html_title)} -linkcheck_exclude_documents = ["index", "getting_started/local/index"] +linkcheck_exclude_documents = ["index", "getting_started/local/index", "assets"] # -- Declare the Jinja context ----------------------------------------------- exclude_patterns = [] diff --git a/doc/source/index.rst b/doc/source/index.rst index c1ffae219d..de11bb6461 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -88,7 +88,7 @@ PyAnsys Geometry is a Python client library for the Ansys Geometry service. Examples {% endif %} -.. grid:: +.. grid:: 2 .. grid-item-card:: :img-top: _static/assets/index_contribute.png @@ -107,6 +107,22 @@ PyAnsys Geometry is a Python client library for the Ansys Geometry service. Contribute + .. grid-item-card:: + :img-top: _static/assets/index_download.png + + Assets + ^^^^^^ + Download different assets related to PyAnsys Geometry, + such as documentation, package wheelhouse, and related files. + + +++ + .. button-link:: assets.html + :color: secondary + :expand: + :outline: + :click-parent: + + Assets .. jinja:: main_toctree @@ -123,3 +139,4 @@ PyAnsys Geometry is a Python client library for the Ansys Geometry service. examples {% endif %} contributing + assets From a29f9fa9bfbb1db31d74550ac6a43a545d432173 Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Wed, 27 Sep 2023 17:10:52 +0200 Subject: [PATCH 04/74] fix: vars from env variable (#766) --- .github/workflows/ci_cd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 2fcd8c8a79..d560e5b373 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -653,7 +653,7 @@ jobs: with: cname: ${{ env.DOCUMENTATION_CNAME }}/version/dev index-name: pyansys-geometry-vdev - host-url: ${{ env.MEILISEARCH_HOST_URL }} + host-url: ${{ vars.MEILISEARCH_HOST_URL }} api-key: ${{ env.MEILISEARCH_API_KEY }} upload_docs_release: @@ -694,4 +694,4 @@ jobs: cname: ${{ env.DOCUMENTATION_CNAME }}/version/${{ env.VERSION }} index-name: pyansys-geometry-v${{ env.VERSION_MEILI }} host-url: ${{ vars.MEILISEARCH_HOST_URL }} - api-key: ${{ env.MEILISEARCH_API_KEY }} \ No newline at end of file + api-key: ${{ env.MEILISEARCH_API_KEY }} From f073d963a1496ee3d11724d54be5b097ba2659cd Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Wed, 27 Sep 2023 18:12:08 +0200 Subject: [PATCH 05/74] feat: adding SECURITY policy (#767) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .github/labels.yml | 4 ++++ SECURITY.md | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 SECURITY.md diff --git a/.github/labels.yml b/.github/labels.yml index d1c3c77b8a..d8f55dc1b0 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -37,3 +37,7 @@ - name: testing description: Anything related to tests color: BFE4D6 + +- name: security + description: Anything related to security advisories + color: FF0000 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..edbeb92166 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,39 @@ + + +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| <= 0.2 | :x: | +| 0.3.x | :white_check_mark: | +| 0.4.x | :white_check_mark: | +| dev | :white_check_mark: | + +## Reporting a Vulnerability + +If you detect a vulnerability, please open an issue on GitHub and add the +``security`` label to your issue. The team will address it as soon as possible. From 9f4baa0c32043397cefea036eaf4dda4abd11579 Mon Sep 17 00:00:00 2001 From: Alex Fernandez <21alex295@gmail.com> Date: Wed, 27 Sep 2023 18:37:39 +0200 Subject: [PATCH 06/74] feat: Add DesignPoint plotting and button (#762) Co-authored-by: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> --- .../geometry/core/designer/designpoint.py | 11 +++ src/ansys/geometry/core/plotting/plotter.py | 18 ++++ .../geometry/core/plotting/plotter_helper.py | 14 ++- .../core/plotting/widgets/__init__.py | 1 + .../plotting/widgets/_images/designpoint.png | Bin 0 -> 569 bytes .../geometry/core/plotting/widgets/measure.py | 24 ++--- .../plotting/widgets/show_design_point.py | 82 ++++++++++++++++++ tests/integration/test_design.py | 5 ++ tests/integration/test_plotter.py | 28 +++++- 9 files changed, 169 insertions(+), 14 deletions(-) create mode 100644 src/ansys/geometry/core/plotting/widgets/_images/designpoint.png create mode 100644 src/ansys/geometry/core/plotting/widgets/show_design_point.py diff --git a/src/ansys/geometry/core/designer/designpoint.py b/src/ansys/geometry/core/designer/designpoint.py index f13c88bae4..a8d9e8d1b4 100644 --- a/src/ansys/geometry/core/designer/designpoint.py +++ b/src/ansys/geometry/core/designer/designpoint.py @@ -25,6 +25,7 @@ from ansys.geometry.core.math.point import Point3D from ansys.geometry.core.misc.checks import check_type +from ansys.geometry.core.misc.units import UNITS if TYPE_CHECKING: # pragma: no cover from ansys.geometry.core.designer.component import Component @@ -86,3 +87,13 @@ def __repr__(self) -> str: lines.append(f" Name : {self.name}") lines.append(f" Design Point : {self.value}") return "\n".join(lines) + + def _to_polydata(self) -> "pv.PolyData": + """Get polydata from DesignPoint object.""" + import pyvista as pv + + # get units to plot proportionally + # 0.3 is the size for the sphere representation + # determined empirically for proper representation + unit = 0.3 * self.value.unit + return pv.Sphere(center=self.value.flat, radius=unit.to(UNITS.m).magnitude) diff --git a/src/ansys/geometry/core/plotting/plotter.py b/src/ansys/geometry/core/plotting/plotter.py index 4ce0d00c39..6500bc77b8 100644 --- a/src/ansys/geometry/core/plotting/plotter.py +++ b/src/ansys/geometry/core/plotting/plotter.py @@ -31,6 +31,7 @@ from ansys.geometry.core.designer.body import Body, MasterBody from ansys.geometry.core.designer.component import Component from ansys.geometry.core.designer.design import Design +from ansys.geometry.core.designer.designpoint import DesignPoint from ansys.geometry.core.logger import LOG as logger from ansys.geometry.core.math.frame import Frame from ansys.geometry.core.math.plane import Plane @@ -346,6 +347,21 @@ def add_sketch_polydata(self, polydata_entries: List[pv.PolyData], **plotting_op for polydata in polydata_entries: self.scene.add_mesh(polydata, color=EDGE_COLOR, **plotting_options) + def add_design_point(self, design_point: DesignPoint, **plotting_options) -> None: + """ + Add a DesignPoint object to the plotter. + + Parameters + ---------- + design_point : DesignPoint + DesignPoint to add. + """ + # get the actor for the DesignPoint + actor = self.scene.add_mesh(design_point._to_polydata(), **plotting_options) + + # save the actor to the object/actor map + self._geom_object_actors_map[actor] = design_point + def add( self, object: Any, @@ -403,6 +419,8 @@ def add( self.add_body(object, merge_bodies, **plotting_options) elif isinstance(object, Design) or isinstance(object, Component): self.add_component(object, merge_components, merge_bodies, **plotting_options) + elif isinstance(object, DesignPoint): + self.add_design_point(object, **plotting_options) else: logger.warning(f"Object type {type(object)} can not be plotted.") return self._geom_object_actors_map diff --git a/src/ansys/geometry/core/plotting/plotter_helper.py b/src/ansys/geometry/core/plotting/plotter_helper.py index bb8c7ed747..4fb2280b8e 100644 --- a/src/ansys/geometry/core/plotting/plotter_helper.py +++ b/src/ansys/geometry/core/plotting/plotter_helper.py @@ -24,6 +24,8 @@ import numpy as np import pyvista as pv +from ansys.geometry.core.designer.body import Body, MasterBody +from ansys.geometry.core.designer.face import Face from ansys.geometry.core.logger import LOG as logger from ansys.geometry.core.plotting.plotter import ( DEFAULT_COLOR, @@ -40,6 +42,7 @@ MeasureWidget, PlotterWidget, Ruler, + ShowDesignPoints, ViewButton, ViewDirection, ) @@ -110,6 +113,7 @@ def enable_widgets(self): for dir in ViewDirection ] self._widgets.append(MeasureWidget(self)) + self._widgets.append(ShowDesignPoints((self))) def select_object(self, geom_object: Union[GeomObjectPlot, EdgePlot], pt: np.ndarray) -> None: """ @@ -224,8 +228,14 @@ def compute_edge_object_map(self) -> Dict[pv.Actor, EdgePlot]: Mapping between plotter actors and EdgePlot objects. """ for object in self._geom_object_actors_map.values(): - for edge in object.edges: - self._edge_actors_map[edge.actor] = edge + # get edges only from bodies + if ( + isinstance(object, Body) + or isinstance(object, MasterBody) + or isinstance(object, Face) + ): + for edge in object.edges: + self._edge_actors_map[edge.actor] = edge def enable_picking(self): """Enable picking capabilities in the plotter.""" diff --git a/src/ansys/geometry/core/plotting/widgets/__init__.py b/src/ansys/geometry/core/plotting/widgets/__init__.py index 0955dfcf98..d1694db3b6 100644 --- a/src/ansys/geometry/core/plotting/widgets/__init__.py +++ b/src/ansys/geometry/core/plotting/widgets/__init__.py @@ -26,5 +26,6 @@ ) from ansys.geometry.core.plotting.widgets.measure import MeasureWidget from ansys.geometry.core.plotting.widgets.ruler import Ruler +from ansys.geometry.core.plotting.widgets.show_design_point import ShowDesignPoints from ansys.geometry.core.plotting.widgets.view_button import ViewButton, ViewDirection from ansys.geometry.core.plotting.widgets.widget import PlotterWidget diff --git a/src/ansys/geometry/core/plotting/widgets/_images/designpoint.png b/src/ansys/geometry/core/plotting/widgets/_images/designpoint.png new file mode 100644 index 0000000000000000000000000000000000000000..e5c8af549f11195736ac237aa9c69d4cacdaa12d GIT binary patch literal 569 zcmV-90>=G`P)Px$^GQTOR7gu>WME+U&p;!<2sIo;Qycsz%J6v|4gZhde!=jYfq{WvkdI+vPZu$c z!EbnKa>jpBxFe4ev|M_#Z3UOoj&~!tjezGyeCVLv=Nw)Pn3%EMbo@d~s^V|Gj8If)O0(X;RRIVL?v_ z*!A<0GyWe!jaVeZlQaJBMT>QUhBFv4ESL+4#KoBU5r*%n&iKFN1!}MmG+Z}^VL^cn z*zlI*jQ@9Vxg4YbWEhuS0mHO-4X}n4xf%a=e8ZhXi4Jp^;piFS_QIO~tw^~QW)*s- z0Xa;{ql%#|R1TJcP?O^Ag@ylH4}Hg$+Zal-GXB5*$-p45YRb@9k&KeY=jCSnKlBYF ze_-a&{N#-PAF*d=tl_h#IrsmP2fvBS{ None: @@ -75,11 +75,13 @@ def callback(self, state: bool) -> None: def update(self) -> None: """Define the measurement widget button params.""" - show_ruler_vr = self._button.GetRepresentation() - show_ruler_icon_file = os.path.join(os.path.dirname(__file__), "_images", "measurement.png") - show_ruler_r = vtkPNGReader() - show_ruler_r.SetFileName(show_ruler_icon_file) - show_ruler_r.Update() - image = show_ruler_r.GetOutput() - show_ruler_vr.SetButtonTexture(0, image) - show_ruler_vr.SetButtonTexture(1, image) + show_measure_vr = self._button.GetRepresentation() + show_measure_icon_file = os.path.join( + os.path.dirname(__file__), "_images", "measurement.png" + ) + show_measure_r = vtkPNGReader() + show_measure_r.SetFileName(show_measure_icon_file) + show_measure_r.Update() + image = show_measure_r.GetOutput() + show_measure_vr.SetButtonTexture(0, image) + show_measure_vr.SetButtonTexture(1, image) diff --git a/src/ansys/geometry/core/plotting/widgets/show_design_point.py b/src/ansys/geometry/core/plotting/widgets/show_design_point.py new file mode 100644 index 0000000000..7980b7f091 --- /dev/null +++ b/src/ansys/geometry/core/plotting/widgets/show_design_point.py @@ -0,0 +1,82 @@ +# Copyright (C) 2023 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 the ruler widget for the PyAnsys Geometry plotter.""" + +import os + +from vtk import vtkButtonWidget, vtkPNGReader + +from ansys.geometry.core.designer.designpoint import DesignPoint +from ansys.geometry.core.plotting.widgets.widget import PlotterWidget + + +class ShowDesignPoints(PlotterWidget): + """ + Provides the a button to hide/show DesignPoint objects in the plotter. + + Parameters + ---------- + plotter_helper : PlotterHelper + Provides the plotter to add the button to. + """ + + def __init__(self, plotter_helper: "PlotterHelper") -> None: + """Initialize the ``ShowDesignPoints`` class.""" + # Call PlotterWidget ctor + super().__init__(plotter_helper._pl.scene) + self.plotter_helper = plotter_helper + + # Initialize variables + self._geom_object_actors_map = self.plotter_helper._pl._geom_object_actors_map + self._button: vtkButtonWidget = self.plotter_helper._pl.scene.add_checkbox_button_widget( + self.callback, position=(5, 438), size=30, border_size=3 + ) + + def callback(self, state: bool) -> None: + """ + Remove or add the DesignPoint actors upon click. + + Parameters + ---------- + state : bool + State of the button, which is inherited from PyVista. The value is ``True`` + if the button is active. + """ + if not state: + for actor, object in self._geom_object_actors_map.items(): + if isinstance(object, DesignPoint): + self.plotter_helper._pl.scene.add_actor(actor) + else: + for actor, object in self._geom_object_actors_map.items(): + if isinstance(object, DesignPoint): + self.plotter_helper._pl.scene.remove_actor(actor) + + def update(self) -> None: + """Define the configuration and representation of the button widget button.""" + show_point_vr = self._button.GetRepresentation() + show_point_icon_file = os.path.join(os.path.dirname(__file__), "_images", "designpoint.png") + show_point_r = vtkPNGReader() + show_point_r.SetFileName(show_point_icon_file) + show_point_r.Update() + image = show_point_r.GetOutput() + show_point_vr.SetButtonTexture(0, image) + show_point_vr.SetButtonTexture(1, image) diff --git a/tests/integration/test_design.py b/tests/integration/test_design.py index f965dae509..21dab9ddd3 100644 --- a/tests/integration/test_design.py +++ b/tests/integration/test_design.py @@ -5,6 +5,7 @@ import numpy as np from pint import Quantity import pytest +import pyvista as pv from ansys.geometry.core import Modeler from ansys.geometry.core.connection import BackendType @@ -1277,6 +1278,10 @@ def test_design_points(modeler: Modeler): assert " Name : SecondPointSet" in design_point_2_str assert " Design Point : [20. 20. 20.]" in design_point_2_str + # make sure it can create polydata + pd = design_points_1._to_polydata() + assert isinstance(pd, pv.PolyData) + def test_named_selections_beams(modeler: Modeler, skip_not_on_linux_service): """Test for verifying the correct creation of ``NamedSelection`` with beams.""" diff --git a/tests/integration/test_plotter.py b/tests/integration/test_plotter.py index 7ee3e7a2ac..43d2fc6db5 100644 --- a/tests/integration/test_plotter.py +++ b/tests/integration/test_plotter.py @@ -7,7 +7,7 @@ from pyvista.plotting import system_supports_plotting from ansys.geometry.core import Modeler -from ansys.geometry.core.math import UNITVECTOR3D_Y, UNITVECTOR3D_Z, Plane, Point2D +from ansys.geometry.core.math import UNITVECTOR3D_Y, UNITVECTOR3D_Z, Plane, Point2D, Point3D from ansys.geometry.core.misc import DEFAULT_UNITS, UNITS, Distance from ansys.geometry.core.plotting import Plotter, PlotterHelper from ansys.geometry.core.sketch import ( @@ -638,6 +638,7 @@ def test_visualization_polydata(): def test_name_filter(modeler: Modeler, verify_image_cache): + """Test the plotter name filter.""" # init modeler design = modeler.create_design("Multiplot") @@ -657,3 +658,28 @@ def test_name_filter(modeler: Modeler, verify_image_cache): filter="Cyl", screenshot=Path(IMAGE_RESULTS_DIR, "test_name_filter.png"), ) + + +def test_plot_design_point(modeler: Modeler, verify_image_cache): + """Test the plotting of DesignPoint objects.""" + design = modeler.create_design("Multiplot") + plot_list = [] + + # add body to check compatibility + body_sketch = Sketch() + body_sketch.box(Point2D([10, 10], UNITS.m), Quantity(10, UNITS.m), Quantity(10, UNITS.m)) + box_body = design.extrude_sketch("JustABox", body_sketch, Quantity(10, UNITS.m)) + plot_list.append(box_body) + + # add single point + point_set_2 = [Point3D([10, 10, 10], UNITS.m), Point3D([20, 20, 20], UNITS.m)] + + # add list of points + design_points_2 = design.add_design_points("SecondPointSet", point_set_2) + plot_list.extend(design_points_2) + + # plot + PlotterHelper().plot( + plot_list, + screenshot=Path(IMAGE_RESULTS_DIR, "test_plot_design_point.png"), + ) From ba47cf120615e1b94e38247e8d3878c0fbb9c490 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 Sep 2023 07:55:35 +0200 Subject: [PATCH 07/74] MAINT: Bump the docs-deps group with 1 update (#769) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6bbb81255f..2cc5c4b947 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,7 +84,7 @@ doc = [ "pyvista[trame]==0.41.1", "requests==2.31.0", "sphinx==7.2.5", - "sphinx-autoapi==2.1.1", # "sphinx-autoapi @ git+https://github.com/jorgepiloto/sphinx-autoapi@feat/single-page-option", ---> Installed directly in workflow + "sphinx-autoapi==3.0.0", # "sphinx-autoapi @ git+https://github.com/jorgepiloto/sphinx-autoapi@feat/single-page-option", ---> Installed directly in workflow "sphinx-autodoc-typehints==1.24.0", "sphinx-copybutton==0.5.2", "sphinx_design==0.5.0", From a75c8c05b7e9168eea7b8bc103defb0e9431c7f3 Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Thu, 28 Sep 2023 11:18:50 +0200 Subject: [PATCH 08/74] feat: add CodeQL analysis (#771) --- .github/workflows/codeql.yml | 82 ++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000000..9ee3099078 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,82 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main" ] + schedule: + - cron: '45 22 * * 4' + +jobs: + analyze: + name: Analyze + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners + # Consider using larger runners for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift' ] + # Use only 'java' to analyze code written in Java, Kotlin or both + # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" From d6dcf39f68a9c4c167bad96abfb5d8ce8dd86520 Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Thu, 28 Sep 2023 11:50:31 +0200 Subject: [PATCH 09/74] fix: avoid uploading docs assets(wheelhouse) and minor updates to workflow (#770) --- .github/workflows/ci_cd.yml | 51 ++++++++----------------------------- doc/source/assets.rst | 50 +++++++++++++++++++++++------------- doc/source/conf.py | 29 +++++++++++++++++++++ 3 files changed, 72 insertions(+), 58 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index d560e5b373..6549a3d609 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -21,6 +21,7 @@ env: IS_WORKFLOW_RUNNING: True ARTIFACTORY_VERSION: v241 MEILISEARCH_API_KEY: ${{ secrets.MEILISEARCH_API_KEY }} + MEILISEARCH_HOST_URL: ${{ vars.MEILISEARCH_HOST_URL }} MEILISEARCH_PUBLIC_API_KEY: ${{ secrets.MEILISEARCH_PUBLIC_API_KEY }} concurrency: @@ -256,6 +257,12 @@ jobs: cd doc .\make.bat pdf + - name: Add assets to HTML docs + run: | + zip -r documentation-html.zip .\doc\_build\html + mv documentation-html.zip .\doc\_build\html\_static\assets\download\ + cp doc/_build/latex/ansys-geometry-core.pdf .\doc\_build\html\_static\assets\download\ + - name: Upload HTML documentation uses: actions/upload-artifact@v3 with: @@ -380,42 +387,6 @@ jobs: library-name: ${{ env.PACKAGE_NAME }} python-version: ${{ env.MAIN_PYTHON_VERSION }} - add-assets-to-docs: - name: Add downloadable assets to docs - needs: [testing-windows, testing-linux, docs] - runs-on: ubuntu-latest - steps: - - name: "Download all artifacts" - uses: actions/download-artifact@v3 - with: - path: /tmp/artifacts - - - name: "Compressing HTML docs to ZIP file" - run: | - zip -r /tmp/artifacts/documentation-html.zip /tmp/artifacts/documentation-html - - - name: "Download HTML documentation" - uses: actions/download-artifact@v3 - with: - name: documentation-html - path: /tmp/documentation-html - - - name: "Fill the HTML docs with assets" - run: | - # Move the docs in HTML format - mv /tmp/artifacts/documentation-html.zip /tmp/documentation-html/_static/assets/download/ - # Move the docs in PDF format - mv /tmp/artifacts/documentation-pdf/* /tmp/documentation-html/_static/assets/download/ - # Move the wheelhouses - mv /tmp/artifacts/**/*-wheelhouse-*.zip /tmp/documentation-html/_static/assets/download/ - - - name: "Upload HTML documentation" - uses: actions/upload-artifact@v3 - with: - name: documentation-html - path: /tmp/documentation-html - retention-days: 7 - # ================================================================================================= # vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv RUNNING ON SELF-HOSTED RUNNER vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv # ================================================================================================= @@ -614,7 +585,7 @@ jobs: release: name: Release project if: github.event_name == 'push' && contains(github.ref, 'refs/tags') - needs: [package, add-assets-to-docs, build-windows-container, build-linux-container] + needs: [package, build-windows-container, build-linux-container] runs-on: ubuntu-latest steps: - name: Release to the public PyPI repository @@ -634,7 +605,7 @@ jobs: name: Upload dev documentation if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest - needs: [package, add-assets-to-docs] + needs: [package] steps: - name: Deploy the latest documentation uses: ansys/actions/doc-deploy-dev@v4 @@ -653,7 +624,7 @@ jobs: with: cname: ${{ env.DOCUMENTATION_CNAME }}/version/dev index-name: pyansys-geometry-vdev - host-url: ${{ vars.MEILISEARCH_HOST_URL }} + host-url: ${{ env.MEILISEARCH_HOST_URL }} api-key: ${{ env.MEILISEARCH_API_KEY }} upload_docs_release: @@ -693,5 +664,5 @@ jobs: with: cname: ${{ env.DOCUMENTATION_CNAME }}/version/${{ env.VERSION }} index-name: pyansys-geometry-v${{ env.VERSION_MEILI }} - host-url: ${{ vars.MEILISEARCH_HOST_URL }} + host-url: ${{ env.MEILISEARCH_HOST_URL }} api-key: ${{ env.MEILISEARCH_API_KEY }} diff --git a/doc/source/assets.rst b/doc/source/assets.rst index 931cb10075..7631a70854 100644 --- a/doc/source/assets.rst +++ b/doc/source/assets.rst @@ -34,26 +34,40 @@ Consider installing using a `virtual environment `_ -* `Linux wheelhouse for Python 3.9 <_static/assets/download/ansys-geometry-core-v0.4.dev0-wheelhouse-ubuntu-latest-3.9.zip>`_ -* `Linux wheelhouse for Python 3.10 <_static/assets/download/ansys-geometry-core-v0.4.dev0-wheelhouse-ubuntu-latest-3.10.zip>`_ -* `Linux wheelhouse for Python 3.11 <_static/assets/download/ansys-geometry-core-v0.4.dev0-wheelhouse-ubuntu-latest-3.11.zip>`_ + {%- for os_name, download_links in assets.items() %} -Windows -^^^^^^^ + {{ os_name }} + {{ "^" * os_name|length }} -* `Windows wheelhouse for Python 3.8 <_static/assets/download/ansys-geometry-core-v0.4.dev0-wheelhouse-windows-latest-3.8.zip>`_ -* `Windows wheelhouse for Python 3.9 <_static/assets/download/ansys-geometry-core-v0.4.dev0-wheelhouse-windows-latest-3.9.zip>`_ -* `Windows wheelhouse for Python 3.10 <_static/assets/download/ansys-geometry-core-v0.4.dev0-wheelhouse-windows-latest-3.10.zip>`_ -* `Windows wheelhouse for Python 3.11 <_static/assets/download/ansys-geometry-core-v0.4.dev0-wheelhouse-windows-latest-3.11.zip>`_ + {%- for link in download_links %} + * `{{ link.os }} wheelhouse for Python {{ link.python_versions }} <{{ link.prefix_url }}/ansys-geometry-core-{{ link.latest_released_version }}-wheelhouse-{{ link.runner }}-{{ link.python_versions }}.zip>`_ + {%- endfor %} -MacOS -^^^^^ + {%- endfor %} -* `MacOS wheelhouse for Python 3.8 <_static/assets/download/ansys-geometry-core-v0.4.dev0-wheelhouse-macos-latest-3.8.zip>`_ -* `MacOS wheelhouse for Python 3.9 <_static/assets/download/ansys-geometry-core-v0.4.dev0-wheelhouse-macos-latest-3.9.zip>`_ -* `MacOS wheelhouse for Python 3.10 <_static/assets/download/ansys-geometry-core-v0.4.dev0-wheelhouse-macos-latest-3.10.zip>`_ -* `MacOS wheelhouse for Python 3.11 <_static/assets/download/ansys-geometry-core-v0.4.dev0-wheelhouse-macos-latest-3.11.zip>`_ +Geometry service Docker container assets +---------------------------------------- + +Build the latest Geometry service Docker container using the following assets. Instructions +on how to build the containers are found at `Docker containers `_. + +Currently, the Geometry service backend is mainly delivered as a **Windows** Docker container. +However, these containers require a Windows machine to run them. + +A Linux version of the Geometry service is also available but with limited capabilities, +meaning that certain operations are not available or fail. + + +Windows container +^^^^^^^^^^^^^^^^^ + +* `Latest Geometry service binaries for Windows containers `_ +* `Latest Geometry service Dockerfile for Windows containers `_ + +Linux container +^^^^^^^^^^^^^^^ + +* `Latest Geometry service binaries for Linux containers `_ +* `Latest Geometry service Dockerfile for Linux containers `_ diff --git a/doc/source/conf.py b/doc/source/conf.py index 81b25933f0..df8f4f9bb7 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,5 +1,6 @@ """Sphinx documentation configuration file.""" from datetime import datetime +import json import os from pathlib import Path @@ -13,6 +14,7 @@ pyansys_logo_black, watermark, ) +import requests from sphinx.builders.latex import LaTeXBuilder from ansys.geometry.core import __version__ @@ -20,6 +22,32 @@ LaTeXBuilder.supported_image_types = ["image/png", "image/pdf", "image/svg+xml"] +def get_wheelhouse_assets_dictionary(): + """Auxiliary method to build the wheelhouse assets dictionary.""" + assets_context_os = ["Linux", "Windows", "MacOS"] + assets_context_runners = ["ubuntu-latest", "windows-latest", "macos-latest"] + assets_context_python_versions = ["3.8", "3.9", "3.10", "3.11"] + assets_context_version = json.loads( + requests.get("https://api.github.com/repos/ansys/pyansys-geometry/releases/latest").content + )["name"] + + assets = {} + for assets_os, assets_runner in zip(assets_context_os, assets_context_runners): + download_links = [] + for assets_py_ver in assets_context_python_versions: + temp_dict = { + "os": assets_os, + "runner": assets_runner, + "python_versions": assets_py_ver, + "latest_released_version": assets_context_version, + "prefix_url": f"https://github.com/ansys/pyansys-geometry/releases/download/{assets_context_version}", # noqa: E501 + } + download_links.append(temp_dict) + + assets[assets_os] = download_links + return assets + + # Project information project = "ansys-geometry-core" copyright = f"(c) {datetime.now().year} ANSYS, Inc. All rights reserved" @@ -244,6 +272,7 @@ "windows_containers": { "add_windows_warnings": True, }, + "wheelhouse-assets": {"assets": get_wheelhouse_assets_dictionary()}, } From 6f8d65942e9e9980810c4d85cf496903b3ae9fcf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Sep 2023 07:56:48 +0200 Subject: [PATCH 10/74] MAINT: Bump scipy from 1.11.2 to 1.11.3 (#776) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2cc5c4b947..77bf123480 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ tests = [ "pytest-xvfb==3.0.0", "pyvista[trame]==0.41.1", "requests==2.31.0", - "scipy==1.11.2", + "scipy==1.11.3", "six==1.16.0", "vtk==9.2.6", ] From d517a22d772a664de0ca06f176a33d2bc6066005 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Sep 2023 06:40:38 +0000 Subject: [PATCH 11/74] MAINT: Bump the actions group with 1 update (#775) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9ee3099078..f701743d8b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -46,7 +46,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL From 5dd2cb08ba84d33f84fdd1fe7afdabd0bfc997ad Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Fri, 29 Sep 2023 10:21:13 +0200 Subject: [PATCH 12/74] feat: add license discussion to Q&A (#772) Co-authored-by: Kathy Pippert <84872299+PipKat@users.noreply.github.com> --- doc/source/getting_started/faq.rst | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/doc/source/getting_started/faq.rst b/doc/source/getting_started/faq.rst index ccab5c5e7f..4a834c3ae9 100644 --- a/doc/source/getting_started/faq.rst +++ b/doc/source/getting_started/faq.rst @@ -12,6 +12,28 @@ Design Language (APDL), Ansys Fluent, and other Ansys products. You can use PyAnsys libraries within a Python environment of your choice in conjunction with external Python libraries. +What Ansys license do I need to run the Geometry service? +--------------------------------------------------------- + +.. note:: + + This question is answered in https://github.com/ansys/pyansys-geometry/discussions/754. + +The Ansys Geometry service is a headless service developed on top of the +modeling libraries for Discovery and SpaceClaim. + +Both in its standalone and Docker versions, the Ansys Geometry service +requires a **Discovery Modeling** license to run. + +To run PyAnsys Geometry against other backends, such as Discovery +or SpaceClaim, users must have an Ansys license that allows them to run these +Ansys products. + +The **Discovery Modeling** license is one of these licenses, but there are others, +such as the Ansys Mechanical Enterprise license, that also allow users to run +these Ansys products. However, the Geometry service is only compatible with +the **Discovery Modeling** license. + .. button-ref:: index :ref-type: doc :color: primary From 3ac8f8f5aa02c1b7e6a86f685efef7f2418f6f30 Mon Sep 17 00:00:00 2001 From: Matteo Bini <91963243+b-matteo@users.noreply.github.com> Date: Fri, 29 Sep 2023 10:58:59 +0200 Subject: [PATCH 13/74] Secure get_available_port method. (#777) --- src/ansys/geometry/core/connection/product_instance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ansys/geometry/core/connection/product_instance.py b/src/ansys/geometry/core/connection/product_instance.py index 3b392584f3..9219eb649a 100644 --- a/src/ansys/geometry/core/connection/product_instance.py +++ b/src/ansys/geometry/core/connection/product_instance.py @@ -246,7 +246,7 @@ def prepare_and_start_backend( def get_available_port(): """Return an available port to be used.""" sock = socket.socket() - sock.bind(("", 0)) + sock.bind((socket.gethostname(), 0)) port = sock.getsockname()[1] sock.close() return port From ab44a01eb3aee11ecbc387001fd28704e19abc05 Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Fri, 29 Sep 2023 13:03:22 +0200 Subject: [PATCH 14/74] cicd: cleanup CodeQL workflow (#778) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 50 +++++------------------------------- 1 file changed, 7 insertions(+), 43 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f701743d8b..61faabc731 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,21 +1,9 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# name: "CodeQL" on: push: branches: [ "main" ] pull_request: - # The branches below must be a subset of the branches above branches: [ "main" ] schedule: - cron: '45 22 * * 4' @@ -23,52 +11,28 @@ on: jobs: analyze: name: Analyze - # Runner size impacts CodeQL analysis time. To learn more, please see: - # - https://gh.io/recommended-hardware-resources-for-running-codeql - # - https://gh.io/supported-runners-and-hardware-resources - # - https://gh.io/using-larger-runners - # Consider using larger runners for possible analysis time improvements. - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} - timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + runs-on: 'ubuntu-latest' + timeout-minutes: 360 permissions: actions: read contents: read security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'python' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift' ] - # Use only 'java' to analyze code written in Java, Kotlin or both - # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - steps: - name: Checkout repository uses: actions/checkout@v4 - # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - + languages: 'python' + config: | + paths: + - src # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). - # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v2 - # ℹī¸ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - # If the Autobuild fails above, remove it and uncomment the following three lines. # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. @@ -79,4 +43,4 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 with: - category: "/language:${{matrix.language}}" + category: "/language:python" From 64b8f2bf61bf2a0d475b83276aa45319f30f959d Mon Sep 17 00:00:00 2001 From: Riccardo <107696364+rmanno91@users.noreply.github.com> Date: Fri, 29 Sep 2023 13:36:04 +0200 Subject: [PATCH 15/74] Remove staticmethod decorator (#779) Co-authored-by: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> --- CONTRIBUTORS.md | 1 + src/ansys/geometry/core/connection/product_instance.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 9d03534269..cc99b09c28 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -17,3 +17,4 @@ * [Alejandro FernÃĄndez](https://github.com/AlejandroFernandezLuces) * [Lance Lance](https://github.com/LanceX2214) * [Dastan Abdulla](https://github.com/dastan-ansys) +* [Riccardo Manno](https://github.com/rmanno91) diff --git a/src/ansys/geometry/core/connection/product_instance.py b/src/ansys/geometry/core/connection/product_instance.py index 9219eb649a..24fdadaf0a 100644 --- a/src/ansys/geometry/core/connection/product_instance.py +++ b/src/ansys/geometry/core/connection/product_instance.py @@ -242,7 +242,6 @@ def prepare_and_start_backend( ) -@staticmethod def get_available_port(): """Return an available port to be used.""" sock = socket.socket() From 5a10decb0f51bd0ad4797e9ccd55dccdd3a0865f Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Fri, 29 Sep 2023 15:13:24 +0200 Subject: [PATCH 16/74] fix: pymeilisearch use updated Python version (#780) --- .github/workflows/ci_cd.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 6549a3d609..2e3e96057a 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -626,6 +626,7 @@ jobs: index-name: pyansys-geometry-vdev host-url: ${{ env.MEILISEARCH_HOST_URL }} api-key: ${{ env.MEILISEARCH_API_KEY }} + python-version: '3.11' upload_docs_release: name: Upload release documentation @@ -666,3 +667,4 @@ jobs: index-name: pyansys-geometry-v${{ env.VERSION_MEILI }} host-url: ${{ env.MEILISEARCH_HOST_URL }} api-key: ${{ env.MEILISEARCH_API_KEY }} + python-version: '3.11' From c2fe86c5bf1646ffc6aaefe6ff7c29357b70397f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Sep 2023 13:44:59 +0000 Subject: [PATCH 17/74] MAINT: Bump pytest-pyvista from 0.1.8 to 0.1.9 (#782) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 77bf123480..79f11134e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ tests = [ "protobuf==3.20.3", "pytest==7.4.2", "pytest-cov==4.1.0", - "pytest-pyvista==0.1.8", + "pytest-pyvista==0.1.9", "pytest-xvfb==3.0.0", "pyvista[trame]==0.41.1", "requests==2.31.0", From 7bb60d40e3472c4389249c309d22782938aa5c42 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Sep 2023 14:20:09 +0000 Subject: [PATCH 18/74] MAINT: Bump the docs-deps group with 1 update (#781) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 79f11134e5..3cb520ac91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,7 +70,7 @@ tests = [ "vtk==9.2.6", ] doc = [ - "ansys-sphinx-theme==0.12.0", + "ansys-sphinx-theme==0.12.1", "docker==6.1.3", "ipyvtklink==0.2.3", "jupyter_sphinx==0.4.0", From cf5ec9047e1dcdb80ab51ea07f09b2055ef51ab5 Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Wed, 4 Oct 2023 09:19:50 +0200 Subject: [PATCH 19/74] skip Linux testing - temp fix (#785) --- .github/workflows/ci_cd.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 2e3e96057a..39549ba2a6 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -340,15 +340,15 @@ jobs: key: pyvista-image-cache-${{ runner.os }}-v-${{ env.RESET_IMAGE_CACHE }}-${{ hashFiles('pyproject.toml') }} restore-keys: pyvista-image-cache-${{ runner.os }}-v-${{ env.RESET_IMAGE_CACHE }} - - name: Run pytest - if: env.SKIP_UNSTABLE == 'false' - uses: ansys/actions/tests-pytest@v4 - env: - ALLOW_PLOTTING: true - with: - python-version: ${{ env.MAIN_PYTHON_VERSION }} - pytest-extra-args: "--service-os=linux" - checkout: false + # - name: Run pytest + # if: env.SKIP_UNSTABLE == 'false' + # uses: ansys/actions/tests-pytest@v4 + # env: + # ALLOW_PLOTTING: true + # with: + # python-version: ${{ env.MAIN_PYTHON_VERSION }} + # pytest-extra-args: "--service-os=linux" + # checkout: false - name: Upload integration test logs if: always() From feaeb72cd5d859accd24399daf41dce15736d2a3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 07:47:57 +0000 Subject: [PATCH 20/74] [pre-commit.ci] pre-commit autoupdate (#783) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- src/ansys/geometry/core/primitives/ellipse.py | 2 +- tests/test_logging.py | 2 +- tests/test_math.py | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d813f830c1..d19056eea8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: additional_dependencies: [tomli] - repo: https://github.com/codespell-project/codespell - rev: v2.2.5 + rev: v2.2.6 hooks: - id: codespell args: ["--ignore-words", "doc/styles/Vocab/ANSYS/accept.txt"] @@ -57,6 +57,6 @@ repos: # this validates our github workflow files - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.26.3 + rev: 0.27.0 hooks: - id: check-github-workflows diff --git a/src/ansys/geometry/core/primitives/ellipse.py b/src/ansys/geometry/core/primitives/ellipse.py index b0482689de..874af769f6 100644 --- a/src/ansys/geometry/core/primitives/ellipse.py +++ b/src/ansys/geometry/core/primitives/ellipse.py @@ -317,7 +317,7 @@ class EllipseEvaluation(CurveEvaluation): """ def __init__(self, ellipse: Ellipse, parameter: Real) -> None: - """``Intialize the ``EllipseEvaluation`` class.""" + """``Initialize the ``EllipseEvaluation`` class.""" self._ellipse = ellipse self._parameter = parameter diff --git a/tests/test_logging.py b/tests/test_logging.py index 57d292bd32..4cc1ef1de9 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -51,7 +51,7 @@ def test_only_logger(caplog: pytest.LogCaptureFixture): def test_global_logger_exist(): - """Test for checking the accurrate naming of the general Logger instance.""" + """Test for checking the accurate naming of the general Logger instance.""" assert isinstance(LOG.logger, deflogging.Logger) assert LOG.logger.name == "PyAnsys_Geometry_global" diff --git a/tests/test_math.py b/tests/test_math.py index 67879e6bd2..41e92b4254 100644 --- a/tests/test_math.py +++ b/tests/test_math.py @@ -622,7 +622,7 @@ def test_matrix(): m_1_copy = Matrix([[2, 5], [0, 8]]) m_2 = Matrix([[3, 2, 0], [1, 3, 0], [0, 6, 4]]) - # Intiate a test matrix using numpy.ndarray + # Initiate a test matrix using numpy.ndarray test_matrix = np.array([[2, 5], [0, 8]]) # Check inverse of matrix @@ -685,7 +685,7 @@ def test_matrix_33(): # Create a null matrix, which is 3x3 identity matrix m_null = Matrix33() - # Intiate a test matrix using numpy.ndarray + # Initiate a test matrix using numpy.ndarray test_matrix = np.array([[2, 0, 0], [0, 3, 0], [0, 0, 4]]) assert np.array_equal(test_matrix, m_1) @@ -718,7 +718,7 @@ def test_matrix_44(): # Create a null matrix, which is 4x4 identity matrix m_null = Matrix44() - # Intiate a test matrix using numpy.ndarray + # Initiate a test matrix using numpy.ndarray test_matrix = np.array([[2, 0, 0, 0], [0, 3, 0, 0], [0, 0, 4, 0], [0, 0, 0, 1]]) assert np.array_equal(test_matrix, m_1) From b2d154e09728ed222abbd7e1e3e4a6ba9cce8f1f Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Wed, 4 Oct 2023 10:46:27 +0200 Subject: [PATCH 21/74] feat: drop Python 3.8 and setting main version as 3.11 (#787) --- .github/workflows/ci_cd.yml | 15 ++++++++------- .github/workflows/docker_test_build.yml | 5 +++-- .github/workflows/nightly_docker_test.yml | 5 +++-- README.rst | 6 +++--- doc/source/assets.rst | 6 +++--- doc/source/conf.py | 2 +- doc/source/getting_started/installation.rst | 8 ++++---- pyproject.toml | 4 +--- src/ansys/geometry/core/__init__.py | 5 +---- src/ansys/geometry/core/logger.py | 6 +----- tox.ini | 3 +-- 11 files changed, 29 insertions(+), 36 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 39549ba2a6..ddebda327c 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -9,7 +9,8 @@ on: - main env: - MAIN_PYTHON_VERSION: '3.10' + MAIN_PYTHON_VERSION: '3.11' + MAIN_PYTHON_VERSION_WINDOWS_SELFHOSTED: '3.9' PACKAGE_NAME: 'ansys-geometry-core' DOCUMENTATION_CNAME: 'geometry.docs.pyansys.com' ANSRV_GEO_IMAGE: 'ghcr.io/ansys/geometry' @@ -46,7 +47,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ['3.8', '3.9', '3.10', '3.11'] + python-version: ['3.9', '3.10', '3.11'] should-release: - ${{ github.event_name == 'push' && contains(github.ref, 'refs/tags') }} exclude: @@ -88,7 +89,7 @@ jobs: if: env.SKIP_UNSTABLE == 'false' uses: actions/setup-python@v4 with: - python-version: '3.9' # use python3.9, self-hosted has an issue with 3.10 + python-version: ${{ env.MAIN_PYTHON_VERSION_WINDOWS_SELFHOSTED }} # self-hosted has an issue with 3.11 - name: Set up headless display if: env.SKIP_UNSTABLE == 'false' @@ -207,7 +208,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.9' # use python3.9, self-hosted has an issue with 3.10 + python-version: ${{ env.MAIN_PYTHON_VERSION_WINDOWS_SELFHOSTED }} # self-hosted has an issue with 3.11 - name: Set up headless display uses: pyvista/setup-headless-display-action@v2 @@ -430,7 +431,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.9' # use python3.9, self-hosted has an issue with 3.10 + python-version: ${{ env.MAIN_PYTHON_VERSION_WINDOWS_SELFHOSTED }} # self-hosted has an issue with 3.11 - name: Download Windows binaries uses: actions/download-artifact@v3 @@ -626,7 +627,7 @@ jobs: index-name: pyansys-geometry-vdev host-url: ${{ env.MEILISEARCH_HOST_URL }} api-key: ${{ env.MEILISEARCH_API_KEY }} - python-version: '3.11' + python-version: ${{ env.MAIN_PYTHON_VERSION }} upload_docs_release: name: Upload release documentation @@ -667,4 +668,4 @@ jobs: index-name: pyansys-geometry-v${{ env.VERSION_MEILI }} host-url: ${{ env.MEILISEARCH_HOST_URL }} api-key: ${{ env.MEILISEARCH_API_KEY }} - python-version: '3.11' + python-version: ${{ env.MAIN_PYTHON_VERSION }} diff --git a/.github/workflows/docker_test_build.yml b/.github/workflows/docker_test_build.yml index 8b401a9655..14d82db417 100644 --- a/.github/workflows/docker_test_build.yml +++ b/.github/workflows/docker_test_build.yml @@ -4,7 +4,8 @@ on: release: env: - MAIN_PYTHON_VERSION: '3.10' + MAIN_PYTHON_VERSION: '3.11' + MAIN_PYTHON_VERSION_WINDOWS_SELFHOSTED: '3.9' ANSRV_GEO_IMAGE_WINDOWS_TAG: ghcr.io/ansys/geometry:windows-latest-tmp ANSRV_GEO_IMAGE_LINUX_TAG: ghcr.io/ansys/geometry:linux-latest-tmp ANSRV_GEO_PORT: 700 @@ -31,7 +32,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.9' # use python3.9, self-hosted has an issue with 3.10 + python-version: ${{ env.MAIN_PYTHON_VERSION_WINDOWS_SELFHOSTED }} # self-hosted has an issue with 3.11 - name: Download Windows binaries uses: dsaltares/fetch-gh-release-asset@master diff --git a/.github/workflows/nightly_docker_test.yml b/.github/workflows/nightly_docker_test.yml index 4ae4d64066..d9199e755a 100644 --- a/.github/workflows/nightly_docker_test.yml +++ b/.github/workflows/nightly_docker_test.yml @@ -5,7 +5,8 @@ on: - cron: "0 3 * * *" env: - MAIN_PYTHON_VERSION: '3.10' + MAIN_PYTHON_VERSION: '3.11' + MAIN_PYTHON_VERSION_WINDOWS_SELFHOSTED: '3.9' ANSRV_GEO_IMAGE_WINDOWS_TAG: ghcr.io/ansys/geometry:windows-latest-unstable ANSRV_GEO_IMAGE_LINUX_TAG: ghcr.io/ansys/geometry:linux-latest-unstable ANSRV_GEO_PORT: 710 @@ -33,7 +34,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.9' # use python3.9, self-hosted has an issue with 3.10 + python-version: ${{ env.MAIN_PYTHON_VERSION_WINDOWS_SELFHOSTED }} # self-hosted has an issue with 3.11 cache: 'pip' cache-dependency-path: 'pyproject.toml' diff --git a/README.rst b/README.rst index 6ee42ddde3..ce87b1b3f5 100644 --- a/README.rst +++ b/README.rst @@ -139,14 +139,14 @@ by downloading the wheelhouse archive from the `Releases `_ statement, previous versions of Python are no longer supported. @@ -70,14 +70,14 @@ archive for your corresponding machine architecture from the repository's `Relea `_. Each wheelhouse archive contains all the Python wheels necessary to install PyAnsys Geometry from scratch on Windows, -Linux, and MacOS from Python 3.8 to 3.11. You can install this on an isolated system with a fresh Python +Linux, and MacOS from Python 3.9 to 3.11. You can install this on an isolated system with a fresh Python installation or on a virtual environment. -For example, on Linux with Python 3.8, unzip the wheelhouse archive and install it with these commands: +For example, on Linux with Python 3.9, unzip the wheelhouse archive and install it with these commands: .. code:: bash - unzip ansys-geometry-core-v0.4.dev0-wheelhouse-Linux-3.8.zip wheelhouse + unzip ansys-geometry-core-v0.4.dev0-wheelhouse-Linux-3.9.zip wheelhouse pip install ansys-geometry-core -f wheelhouse --no-index --upgrade --ignore-installed If you are on Windows with Python 3.9, unzip the wheelhouse archive to a wheelhouse directory diff --git a/pyproject.toml b/pyproject.toml index 3cb520ac91..3125a60d79 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "ansys-geometry-core" version = "0.4.dev0" description = "A python wrapper for Ansys Geometry service" readme = "README.rst" -requires-python = ">=3.8,<4" +requires-python = ">=3.9,<4" license = {file = "LICENSE"} authors = [{name = "ANSYS, Inc.", email = "pyansys.core@ansys.com"}] maintainers = [{name = "ANSYS, Inc.", email = "pyansys.core@ansys.com"}] @@ -17,7 +17,6 @@ classifiers = [ "Topic :: Scientific/Engineering :: Information Analysis", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -31,7 +30,6 @@ dependencies = [ "googleapis-common-protos>=1.52.0", "grpcio>=1.35.0", "grpcio-health-checking>=1.45.0", - "importlib-metadata>=4.0,<5; python_version<='3.8'", "numpy>=1.20.3", "Pint>=0.18", "protobuf~=3.20.2", diff --git a/src/ansys/geometry/core/__init__.py b/src/ansys/geometry/core/__init__.py index e3bd639289..15fbd6e2bf 100644 --- a/src/ansys/geometry/core/__init__.py +++ b/src/ansys/geometry/core/__init__.py @@ -24,10 +24,7 @@ # Version # ------------------------------------------------------------------------------ -try: - import importlib.metadata as importlib_metadata -except ModuleNotFoundError: # pragma: no cover - import importlib_metadata # type: ignore +import importlib.metadata as importlib_metadata __version__ = importlib_metadata.version(__name__.replace(".", "-")) """PyAnsys Geometry version.""" diff --git a/src/ansys/geometry/core/logger.py b/src/ansys/geometry/core/logger.py index 3c7fd0f117..e9886f7524 100644 --- a/src/ansys/geometry/core/logger.py +++ b/src/ansys/geometry/core/logger.py @@ -302,11 +302,7 @@ def __init__( defaults=None, ): """Initialize the ``PyGeometryFormatter`` class.""" - if sys.version_info[1] < 8: - super().__init__(fmt, datefmt, style) - else: - # 3.8: The validate parameter was added - super().__init__(fmt, datefmt, style, validate) + super().__init__(fmt, datefmt, style, validate) self._style = PyGeometryPercentStyle(fmt, defaults=defaults) # overwriting diff --git a/tox.ini b/tox.ini index cc1494737f..c2c0a1403c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] description = Default tox environments list envlist = - style,{tests38,tests39,tests310,tests311}{,-coverage},doc + style,{tests39,tests310,tests311}{,-coverage},doc skip_missing_interpreters = true isolated_build = true isolated_build_env = build @@ -9,7 +9,6 @@ isolated_build_env = build [testenv] description = Checks for project unit tests and coverage (if desired) basepython = - tests38: python3.8 tests39: python3.9 tests310: python3.10 tests311: python3.11 From 0b173ff8c2cc4e75a0fed42f7b4f8178fd60a846 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 09:35:00 +0000 Subject: [PATCH 22/74] MAINT: Bump the docs-deps group with 1 update (#790) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3125a60d79..1cbf828257 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,7 +74,7 @@ doc = [ "jupyter_sphinx==0.4.0", "jupytext==1.15.2", "myst-parser==2.0.0", - "nbconvert==7.8.0", + "nbconvert==7.9.0", "nbsphinx==0.9.3", "notebook==7.0.4", "numpydoc==1.6.0", From d567d42bff373a4223ca963cbd0e81a0eb9c2dbd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 10:08:14 +0000 Subject: [PATCH 23/74] MAINT: Bump ansys-api-geometry from 0.3.0 to 0.3.1 (#791) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1cbf828257..c2d66d09dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ ] dependencies = [ - "ansys-api-geometry==0.3.0", + "ansys-api-geometry==0.3.1", "ansys-tools-path>=0.3", "beartype>=0.11.0", "google-api-python-client>=1.7.11", From 226e3344f979b1094d8d6819e59f8b50a9289067 Mon Sep 17 00:00:00 2001 From: Revathy Venugopal <104772255+Revathyvenugopal162@users.noreply.github.com> Date: Wed, 4 Oct 2023 11:19:15 -0400 Subject: [PATCH 24/74] docs: update sphinx card design options for documentation (#784) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> --- doc/source/_static/custom.css | 25 +-- doc/source/getting_started/docker/index.rst | 3 +- doc/source/getting_started/index.rst | 5 +- doc/source/index.rst | 170 ++++++++++---------- 4 files changed, 88 insertions(+), 115 deletions(-) diff --git a/doc/source/_static/custom.css b/doc/source/_static/custom.css index ebfebc1939..2ac728db76 100644 --- a/doc/source/_static/custom.css +++ b/doc/source/_static/custom.css @@ -1,19 +1,5 @@ @import "../ansys-sphinx-theme.css"; -.col-md-3 { - flex: 0 0 30%; - max-width: 30%; -} -.col-xl-7 { - flex: 0 0 53.33333%; - max-width: 53.33333%; - } - -.bd-toc { - padding-top: 5em; -} - -/* Remove once implemented in ansys-sphinx-theme*/ .sd-card .sd-card-img-top { height: 100px; @@ -23,20 +9,11 @@ } .sd-card .sd-card-header { - border: none; - background-color:white; - color: #150458 !important; font-size: var(--pst-font-size-h5); font-weight: bold; - padding: .5rem 0rem 0.5rem 0rem; - text-align: center; + padding: 1rem 0rem 0.5rem 0rem; } .sd-card .sd-card-footer .sd-card-text { max-width: 220px; - margin-left: auto; - margin-right: auto; -} -.sd-card .sd-card-text { - text-align: center; } \ No newline at end of file diff --git a/doc/source/getting_started/docker/index.rst b/doc/source/getting_started/docker/index.rst index 97f0562e30..a91c601931 100644 --- a/doc/source/getting_started/docker/index.rst +++ b/doc/source/getting_started/docker/index.rst @@ -27,11 +27,11 @@ meaning that certain operations are not available or fail. Select the kind of Docker container you want to build: .. grid:: 2 + :gutter: 3 3 4 4 .. grid-item-card:: Windows Docker container :link: windows_container :link-type: doc - :margin: 2 2 0 0 Build a Windows Docker container for the Geometry service and use it from PyAnsys Geometry. Explore the full potential @@ -40,7 +40,6 @@ Select the kind of Docker container you want to build: .. grid-item-card:: Linux Docker container :link: linux_container :link-type: doc - :margin: 2 2 0 0 Test out the Linux Docker container for the Geometry service, which has limited functionalities. diff --git a/doc/source/getting_started/index.rst b/doc/source/getting_started/index.rst index 025eaa30f7..3c76aee5d9 100644 --- a/doc/source/getting_started/index.rst +++ b/doc/source/getting_started/index.rst @@ -13,11 +13,11 @@ running this backend, although the preferred and high-performance mode is using containers. Select the option that suits your needs best. .. grid:: 2 + :gutter: 3 3 4 4 .. grid-item-card:: Docker containers :link: docker/index :link-type: doc - :margin: 2 2 0 0 Launch the Geometry service as a Docker container and connect to it from PyAnsys Geometry. @@ -25,7 +25,6 @@ containers. Select the option that suits your needs best. .. grid-item-card:: Local service :link: local/index :link-type: doc - :margin: 2 2 0 0 Launch the Geometry service locally on your machine and connect to it from PyAnsys Geometry. @@ -33,7 +32,6 @@ containers. Select the option that suits your needs best. .. grid-item-card:: Remote service :link: remote/index :link-type: doc - :margin: 2 2 0 0 Launch the Geometry service on a remote machine and connect to it using PIM (Product Instance Manager). @@ -41,7 +39,6 @@ containers. Select the option that suits your needs best. .. grid-item-card:: Connect to an existing service :link: existing/index :link-type: doc - :margin: 2 2 0 0 Connect to an existing Geometry service locally or remotely. diff --git a/doc/source/index.rst b/doc/source/index.rst index de11bb6461..cec0e8af9f 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -3,126 +3,126 @@ PyAnsys Geometry documentation |version| PyAnsys Geometry is a Python client library for the Ansys Geometry service. -.. grid:: 2 +.. grid:: 1 2 2 2 + :gutter: 4 + :padding: 2 2 0 0 + :class-container: sd-text-center - .. grid-item-card:: - :img-top: _static/assets/index_getting_started.png + .. grid-item-card:: Getting started + :img-top: _static/assets/index_getting_started.png + :class-card: intro-card - Getting started - ^^^^^^^^^^^^^^^ + Learn how to run the Windows Docker container, install the + PyAnsys Geometry image, and launch and connect to the Geometry + service. - Learn how to run the Windows Docker container, install the - PyAnsys Geometry image, and launch and connect to the Geometry - service. + +++ + .. button-link:: getting_started/index.html + :color: secondary + :outline: + :expand: + :click-parent: - +++ + Getting started - .. button-link:: getting_started/index.html - :color: secondary - :expand: - :outline: - :click-parent: + .. grid-item-card:: User guide + :img-top: _static/assets/index_user_guide.png + :class-card: intro-card - Getting started + Understand key concepts and approaches for primitives, + sketches, and model designs. - .. grid-item-card:: - :img-top: _static/assets/index_user_guide.png + +++ + .. button-link:: user_guide/index.html + :color: secondary + :expand: + :outline: + :click-parent: - User guide - ^^^^^^^^^^ + User guide - Understand key concepts and approaches for primitives, - sketches, and model designs. - - +++ - .. button-link:: user_guide/index.html - :color: secondary - :expand: - :outline: - :click-parent: - - User guide .. jinja:: main_toctree - .. grid:: 2 + .. grid:: 1 2 2 2 + :gutter: 4 + :padding: 2 2 0 0 + :class-container: sd-text-center {% if build_api %} - .. grid-item-card:: - :img-top: _static/assets/index_api.png - - API reference - ^^^^^^^^^^^^^ + .. grid-item-card:: API reference + :img-top: _static/assets/index_api.png + :class-card: intro-card - Understand PyAnsys Geometry API endpoints, their capabilities, - and how to interact with them programmatically. + Understand PyAnsys Geometry API endpoints, their capabilities, + and how to interact with them programmatically. - +++ - .. button-link:: api/index.html - :color: secondary - :expand: - :outline: - :click-parent: + +++ + .. button-link:: api/index.html + :color: secondary + :expand: + :outline: + :click-parent: - API reference + API reference {% endif %} {% if build_examples %} - .. grid-item-card:: - :img-top: _static/assets/index_examples.png + .. grid-item-card:: Examples + :img-top: _static/assets/index_examples.png + :class-card: intro-card - Examples - ^^^^^^^^ + Explore examples that show how to use PyAnsys Geometry to + perform many different types of operations. - Explore examples that show how to use PyAnsys Geometry to - perform many different types of operations. + +++ + .. button-link:: examples.html + :color: secondary + :expand: + :outline: + :click-parent: - +++ - .. button-link:: examples.html - :color: secondary - :expand: - :outline: - :click-parent: + Examples - Examples {% endif %} -.. grid:: 2 +.. grid:: 1 2 2 2 + :gutter: 4 + :padding: 2 2 0 0 + :class-container: sd-text-center - .. grid-item-card:: - :img-top: _static/assets/index_contribute.png + .. grid-item-card:: Contribute + :img-top: _static/assets/index_contribute.png + :class-card: intro-card - Contribute - ^^^^^^^^^^ - Learn how to contribute to the PyAnsys Geometry codebase - or documentation. + Learn how to contribute to the PyAnsys Geometry codebase + or documentation. - +++ - .. button-link:: contributing.html - :color: secondary - :expand: - :outline: - :click-parent: + +++ + .. button-link:: contributing.html + :color: secondary + :expand: + :outline: + :click-parent: - Contribute + Contribute - .. grid-item-card:: - :img-top: _static/assets/index_download.png + .. grid-item-card:: Assets + :img-top: _static/assets/index_download.png + :class-card: intro-card - Assets - ^^^^^^ - Download different assets related to PyAnsys Geometry, - such as documentation, package wheelhouse, and related files. + Download different assets related to PyAnsys Geometry, + such as documentation, package wheelhouse, and related files. - +++ - .. button-link:: assets.html - :color: secondary - :expand: - :outline: - :click-parent: + +++ + .. button-link:: assets.html + :color: secondary + :expand: + :outline: + :click-parent: - Assets + Assets .. jinja:: main_toctree From e81f9d9e611b4e73a371cfabb117c74586c5a75f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 12:38:31 +0200 Subject: [PATCH 25/74] MAINT: Bump the docs-deps group with 2 updates (#794) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> --- doc/source/conf.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 76cfa2a023..f52e779db2 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -117,7 +117,7 @@ def get_wheelhouse_assets_dictionary(): # Intersphinx mapping intersphinx_mapping = { - "python": ("https://docs.python.org/3", None), + "python": ("https://docs.python.org/3.11", None), "numpy": ("https://numpy.org/doc/stable", None), "scipy": ("https://docs.scipy.org/doc/scipy/", None), "pyvista": ("https://docs.pyvista.org/version/stable", None), diff --git a/pyproject.toml b/pyproject.toml index c2d66d09dc..cdcd1d407f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,13 +68,13 @@ tests = [ "vtk==9.2.6", ] doc = [ - "ansys-sphinx-theme==0.12.1", + "ansys-sphinx-theme==0.12.2", "docker==6.1.3", "ipyvtklink==0.2.3", "jupyter_sphinx==0.4.0", "jupytext==1.15.2", "myst-parser==2.0.0", - "nbconvert==7.9.0", + "nbconvert==7.9.1", "nbsphinx==0.9.3", "notebook==7.0.4", "numpydoc==1.6.0", From 9f2ced7f56dbfe67bf24854af4d1d5d9ea970169 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 11:04:06 +0000 Subject: [PATCH 26/74] MAINT: Bump the grpc-deps group with 1 update (#793) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cdcd1d407f..5622e44f44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ tests = [ "ansys-tools-path==0.3.1", "beartype==0.16.2", "docker==6.1.3", - "google-api-python-client==2.101.0", + "google-api-python-client==2.102.0", "googleapis-common-protos==1.60.0", "grpcio==1.50.0", "grpcio-health-checking==1.48.2", From 0e4d1001ecc491dd9fb3237ea4d1abcdb485d669 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Oct 2023 08:13:40 +0200 Subject: [PATCH 27/74] MAINT: Bump the docs-deps group with 1 update (#795) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5622e44f44..b9c10ed1de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,7 +74,7 @@ doc = [ "jupyter_sphinx==0.4.0", "jupytext==1.15.2", "myst-parser==2.0.0", - "nbconvert==7.9.1", + "nbconvert==7.9.2", "nbsphinx==0.9.3", "notebook==7.0.4", "numpydoc==1.6.0", From 46ca18a72bb09f336a856bca9d3fc812a604de5d Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Fri, 6 Oct 2023 11:10:02 +0200 Subject: [PATCH 28/74] feat: proper service-based testing (#798) --- .github/workflows/ci_cd.yml | 3 +-- .github/workflows/docker_test_build.yml | 2 +- .github/workflows/nightly_docker_test.yml | 1 - tests/conftest.py | 13 ------------- tests/integration/conftest.py | 13 +++++++------ tests/integration/test_design.py | 11 +++++------ tests/integration/test_design_import.py | 5 +++-- 7 files changed, 17 insertions(+), 31 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index ddebda327c..11c84b64e9 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -348,7 +348,6 @@ jobs: # ALLOW_PLOTTING: true # with: # python-version: ${{ env.MAIN_PYTHON_VERSION }} - # pytest-extra-args: "--service-os=linux" # checkout: false - name: Upload integration test logs @@ -559,7 +558,7 @@ jobs: ALLOW_PLOTTING: true with: python-version: ${{ env.MAIN_PYTHON_VERSION }} - pytest-extra-args: "--service-os=linux --use-existing-service=yes" + pytest-extra-args: "--use-existing-service=yes" checkout: false requires-xvfb: true diff --git a/.github/workflows/docker_test_build.yml b/.github/workflows/docker_test_build.yml index 14d82db417..86d909d267 100644 --- a/.github/workflows/docker_test_build.yml +++ b/.github/workflows/docker_test_build.yml @@ -150,7 +150,7 @@ jobs: ALLOW_PLOTTING: true with: python-version: ${{ env.MAIN_PYTHON_VERSION }} - pytest-extra-args: "--service-os=linux --use-existing-service=yes" + pytest-extra-args: "--use-existing-service=yes" checkout: false requires-xvfb: true diff --git a/.github/workflows/nightly_docker_test.yml b/.github/workflows/nightly_docker_test.yml index d9199e755a..2217bf0638 100644 --- a/.github/workflows/nightly_docker_test.yml +++ b/.github/workflows/nightly_docker_test.yml @@ -128,7 +128,6 @@ jobs: ALLOW_PLOTTING: true with: python-version: ${{ env.MAIN_PYTHON_VERSION }} - pytest-extra-args: "--service-os=linux" requires-xvfb: true - name: Stop the Geometry service diff --git a/tests/conftest.py b/tests/conftest.py index c70696d633..10754b229a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,14 +22,6 @@ def pytest_addoption(parser): choices=("yes", "no"), ) - parser.addoption( - "--service-os", - action="store", - default="windows", - help="Geometry service OS running. Options: 'windows' or 'linux'. By default, 'windows'.", - choices=("windows", "linux"), - ) - @pytest.fixture(scope="session") def use_existing_service(request): @@ -37,11 +29,6 @@ def use_existing_service(request): return True if value.lower() == "yes" else False -@pytest.fixture(scope="session") -def service_os(request): - return request.config.getoption("--service-os") - - @pytest.fixture def fake_record(): def inner_fake_record( diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index b95b1e6d88..ed8700e663 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -15,18 +15,13 @@ import pyvista as pv from ansys.geometry.core import Modeler +from ansys.geometry.core.connection.backend import BackendType from ansys.geometry.core.connection.defaults import GEOMETRY_SERVICE_DOCKER_IMAGE from ansys.geometry.core.connection.local_instance import GeometryContainers, LocalDockerInstance pv.OFF_SCREEN = True -@pytest.fixture(scope="session") -def skip_not_on_linux_service(service_os: str): - if service_os == "linux": - return pytest.skip("Implementation not available on Linux service.") # skip! - - @pytest.fixture(scope="session") def docker_instance(use_existing_service): # This will only have a value in case that: @@ -121,3 +116,9 @@ def clean_plot_result_images(): files = os.listdir(results_dir) for file in files: os.remove(Path(results_dir, file)) + + +@pytest.fixture(scope="session") +def skip_not_on_linux_service(modeler: Modeler): + if modeler.client.backend_type == BackendType.LINUX_SERVICE: + return pytest.skip("Implementation not available on Linux service.") # skip! diff --git a/tests/integration/test_design.py b/tests/integration/test_design.py index 21dab9ddd3..81b048bb25 100644 --- a/tests/integration/test_design.py +++ b/tests/integration/test_design.py @@ -774,7 +774,7 @@ def test_bodies_translation(modeler: Modeler): ) -def test_download_file(modeler: Modeler, tmp_path_factory: pytest.TempPathFactory, service_os: str): +def test_download_file(modeler: Modeler, tmp_path_factory: pytest.TempPathFactory): """Test for downloading a design in multiple modes and verifying the correct download.""" @@ -803,8 +803,8 @@ def test_download_file(modeler: Modeler, tmp_path_factory: pytest.TempPathFactor design.save(file_location=file_save) - # Check for other exports - if service_os == "windows": + # Check for other exports - Windows backend... + if modeler.client.backend_type != BackendType.LINUX_SERVICE: binary_parasolid_file = tmp_path_factory.mktemp("scdoc_files_download") / "cylinder.x_b" text_parasolid_file = tmp_path_factory.mktemp("scdoc_files_download") / "cylinder.x_t" @@ -825,11 +825,10 @@ def test_download_file(modeler: Modeler, tmp_path_factory: pytest.TempPathFactor # design.download(pmdb_file, DesignFileFormat.PMDB) # assert pmdb_file.exists() - elif service_os == "linux": + # Linux backend... + else: binary_parasolid_file = tmp_path_factory.mktemp("scdoc_files_download") / "cylinder.xmt_bin" text_parasolid_file = tmp_path_factory.mktemp("scdoc_files_download") / "cylinder.xmt_txt" - else: - raise Exception("Unable to determine the service operating system.") fmd_file = tmp_path_factory.mktemp("scdoc_files_download") / "cylinder.fmd" diff --git a/tests/integration/test_design_import.py b/tests/integration/test_design_import.py index 039d4c8c55..021be1a17e 100644 --- a/tests/integration/test_design_import.py +++ b/tests/integration/test_design_import.py @@ -5,6 +5,7 @@ import pytest from ansys.geometry.core import Modeler +from ansys.geometry.core.connection.backend import BackendType from ansys.geometry.core.designer import Component, Design from ansys.geometry.core.designer.design import DesignFileFormat from ansys.geometry.core.math import Plane, Point2D, Point3D, UnitVector3D, Vector3D @@ -101,7 +102,7 @@ def test_design_import_simple_case(modeler: Modeler): _checker_method(read_design, design) -def test_open_file(modeler: Modeler, tmp_path_factory: pytest.TempPathFactory, service_os: str): +def test_open_file(modeler: Modeler, tmp_path_factory: pytest.TempPathFactory): """Test creation of a component, saving it to a file, and loading it again to a second component and make sure they have the same properties.""" @@ -152,7 +153,7 @@ def test_open_file(modeler: Modeler, tmp_path_factory: pytest.TempPathFactory, s _checker_method(design, design2, True) # Test HOOPS formats (Windows only) - if service_os == "windows": + if modeler.client.backend_type != BackendType.LINUX_SERVICE: # STEP file = tmp_path_factory.mktemp("test_design_import") / "two_cars.step" design.download(file, DesignFileFormat.STEP) From 0c13c7d7a4a787c4c30db4f378e81f0af4a6bc4c Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Fri, 6 Oct 2023 15:15:16 +0200 Subject: [PATCH 29/74] cicd: reacitvate Linux testing (#796) --- .github/workflows/ci_cd.yml | 16 ++++++++-------- tests/integration/test_design_import.py | 9 ++++++--- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 11c84b64e9..6d3ce2a4cc 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -341,14 +341,14 @@ jobs: key: pyvista-image-cache-${{ runner.os }}-v-${{ env.RESET_IMAGE_CACHE }}-${{ hashFiles('pyproject.toml') }} restore-keys: pyvista-image-cache-${{ runner.os }}-v-${{ env.RESET_IMAGE_CACHE }} - # - name: Run pytest - # if: env.SKIP_UNSTABLE == 'false' - # uses: ansys/actions/tests-pytest@v4 - # env: - # ALLOW_PLOTTING: true - # with: - # python-version: ${{ env.MAIN_PYTHON_VERSION }} - # checkout: false + - name: Run pytest + if: env.SKIP_UNSTABLE == 'false' + uses: ansys/actions/tests-pytest@v4 + env: + ALLOW_PLOTTING: true + with: + python-version: ${{ env.MAIN_PYTHON_VERSION }} + checkout: false - name: Upload integration test logs if: always() diff --git a/tests/integration/test_design_import.py b/tests/integration/test_design_import.py index 021be1a17e..03e5a82bfb 100644 --- a/tests/integration/test_design_import.py +++ b/tests/integration/test_design_import.py @@ -147,10 +147,13 @@ def test_open_file(modeler: Modeler, tmp_path_factory: pytest.TempPathFactory): file = tmp_path_factory.mktemp("test_design_import") / "two_cars.scdocx" design.download(file) - design2 = modeler.open_file(file) - # assert the two cars are the same - _checker_method(design, design2, True) + # TODO: to be reactivated by https://github.com/ansys/pyansys-geometry/issues/799 + if modeler.client.backend_type != BackendType.LINUX_SERVICE: + design2 = modeler.open_file(file) + + # assert the two cars are the same + _checker_method(design, design2, True) # Test HOOPS formats (Windows only) if modeler.client.backend_type != BackendType.LINUX_SERVICE: From 72302a81c370a1cd6343d22c1d63613d52ff801f Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Sat, 7 Oct 2023 08:16:34 +0200 Subject: [PATCH 30/74] feat: figure out binaries upload (#800) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Kathy Pippert <84872299+PipKat@users.noreply.github.com> --- .github/workflows/ci_cd.yml | 26 ++- .github/workflows/docker_test_build.yml | 162 ------------------ doc/source/assets.rst | 12 +- doc/source/conf.py | 1 + .../docker/linux_container.rst | 6 +- .../docker/windows_container.rst | 6 +- doc/styles/Vocab/ANSYS/accept.txt | 1 + 7 files changed, 45 insertions(+), 169 deletions(-) delete mode 100644 .github/workflows/docker_test_build.yml diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 6d3ce2a4cc..f56be00ca5 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -400,6 +400,12 @@ jobs: labels: [self-hosted, Windows, signtool] steps: + - name: Check out repository pyansys-geometry-binaries + uses: actions/checkout@v4 + with: + repository: 'ansys/pyansys-geometry-binaries' + token: ${{ env.BINARIES_TOKEN }} + - name: Download binaries run: | curl.exe -X GET -H "X-JFrog-Art-Api: ${{ secrets.ARTIFACTORY_KEY }}" ${{ secrets.ARTIFACTORY_URL }}/${{ env.ARTIFACTORY_VERSION }}/DockerWindows.zip --output windows-binaries.zip @@ -410,14 +416,28 @@ jobs: with: name: windows-binaries.zip path: windows-binaries.zip - retention-days: 7 + retention-days: 1 - name: Upload Linux binaries as workflow artifacts uses: actions/upload-artifact@v3 with: name: linux-binaries.zip path: linux-binaries.zip - retention-days: 7 + retention-days: 1 + + - name: Publish the binaries to private repo + env: + VERSION_WITH_PREFIX: ${{ github.ref_name }} + run: | + $env:VERSION=$env:VERSION_WITH_PREFIX.substring(1) + mkdir $env:VERSION + mv windows-binaries.zip .\$env:VERSION\ + mv linux-binaries.zip .\$env:VERSION\ + git config user.email ${{ env.BINARIES_EMAIL }} + git config user.name ${{ env.BINARIES_USERNAME }} + git add * + git commit -m "adding binaries for ${{ github.ref_name }}" + git push origin main build-windows-container: name: Building Geometry Service - Windows @@ -599,7 +619,7 @@ jobs: uses: ansys/actions/release-github@v4 with: library-name: ${{ env.PACKAGE_NAME }} - additional-artifacts: windows-binaries.zip windows-dockerfile.zip linux-binaries.zip linux-dockerfile.zip + additional-artifacts: windows-dockerfile.zip linux-dockerfile.zip upload_dev_docs: name: Upload dev documentation diff --git a/.github/workflows/docker_test_build.yml b/.github/workflows/docker_test_build.yml deleted file mode 100644 index 86d909d267..0000000000 --- a/.github/workflows/docker_test_build.yml +++ /dev/null @@ -1,162 +0,0 @@ -name: Docker images - Test build -on: - workflow_dispatch: - release: - -env: - MAIN_PYTHON_VERSION: '3.11' - MAIN_PYTHON_VERSION_WINDOWS_SELFHOSTED: '3.9' - ANSRV_GEO_IMAGE_WINDOWS_TAG: ghcr.io/ansys/geometry:windows-latest-tmp - ANSRV_GEO_IMAGE_LINUX_TAG: ghcr.io/ansys/geometry:linux-latest-tmp - ANSRV_GEO_PORT: 700 - ANSRV_GEO_LICENSE_SERVER: ${{ secrets.LICENSE_SERVER }} - GEO_CONT_NAME: ans_geo - RESET_IMAGE_CACHE: 1 - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: -# ================================================================================================= -# vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv RUNNING ON SELF-HOSTED RUNNER vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv -# ================================================================================================= - - build-windows: - name: Building Geometry Service - Windows - runs-on: [self-hosted, Windows, pygeometry] - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: ${{ env.MAIN_PYTHON_VERSION_WINDOWS_SELFHOSTED }} # self-hosted has an issue with 3.11 - - - name: Download Windows binaries - uses: dsaltares/fetch-gh-release-asset@master - with: - version: 'latest' - file: 'windows-binaries.zip' - token: ${{ secrets.GITHUB_TOKEN }} - target: 'docker/windows-binaries.zip' - - - name: Build Docker image - working-directory: docker - run: | - docker build -f Dockerfile.windows -t ${{ env.ANSRV_GEO_IMAGE_WINDOWS_TAG }} . - - - name: Check location of self-hosted runner and define license server accordingly - if: runner.name == 'pygeometry-ci-1' || runner.name == 'pygeometry-ci-2' - run: - echo "ANSRV_GEO_LICENSE_SERVER=${{ secrets.INTERNAL_LICENSE_SERVER }}" | Out-File -FilePath $env:GITHUB_ENV -Append - - - name: Launch Geometry service - run: | - docker run --detach --name ${{ env.GEO_CONT_NAME }} -e LICENSE_SERVER=${{ env.ANSRV_GEO_LICENSE_SERVER }} -p ${{ env.ANSRV_GEO_PORT }}:50051 ${{ env.ANSRV_GEO_IMAGE_WINDOWS_TAG }} - - - name: Validate connection using PyAnsys Geometry - run: | - python -m venv .venv - .\.venv\Scripts\Activate.ps1 - python -m pip install --upgrade pip - pip install -e .[tests] - python -c "from ansys.geometry.core.connection.validate import validate; validate()" - - - name: Restore images cache - uses: actions/cache@v3 - with: - path: .\tests\integration\image_cache - key: pyvista-image-cache-${{ runner.os }}-v-${{ env.RESET_IMAGE_CACHE }}-${{ hashFiles('pyproject.toml') }} - restore-keys: pyvista-image-cache-${{ runner.os }}-v-${{ env.RESET_IMAGE_CACHE }} - - - name: Testing - run: | - .\.venv\Scripts\Activate.ps1 - pytest -v --use-existing-service=yes - - - name: Stop the Geometry service - if: always() - run: | - docker stop ${{ env.GEO_CONT_NAME }} - docker logs ${{ env.GEO_CONT_NAME }} - docker rm ${{ env.GEO_CONT_NAME }} - - - name: Stop any remaining containers - if: always() - run: | - $dockerContainers = docker ps -a -q - if (-not [string]::IsNullOrEmpty($dockerContainers)) { - docker stop $dockerContainers - docker rm $dockerContainers - } - - - name: Delete the Docker images (and untagged ones) - if: always() - run: | - docker image rm ${{ env.ANSRV_GEO_IMAGE_WINDOWS_TAG }} - docker system prune -f - -# ================================================================================================= -# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUNNING ON SELF-HOSTED RUNNER ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -# ================================================================================================= - - build-linux: - name: Building Geometry Service - Linux - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: ${{ env.MAIN_PYTHON_VERSION }} - - - name: Download Linux binaries - uses: dsaltares/fetch-gh-release-asset@master - with: - version: 'latest' - file: 'linux-binaries.zip' - token: ${{ secrets.GITHUB_TOKEN }} - target: 'docker/linux-binaries.zip' - - - name: Build Docker image - working-directory: docker - run: | - docker build -f Dockerfile.linux -t ${{ env.ANSRV_GEO_IMAGE_LINUX_TAG }} . - - - name: Launch Geometry service - run: | - docker run --detach --name ${{ env.GEO_CONT_NAME }} -e LICENSE_SERVER=${{ env.ANSRV_GEO_LICENSE_SERVER }} -p ${{ env.ANSRV_GEO_PORT }}:50051 ${{ env.ANSRV_GEO_IMAGE_LINUX_TAG }} - - - name: Validate connection using PyAnsys Geometry - run: | - python -m pip install --upgrade pip - pip install -e .[tests] - python -c "from ansys.geometry.core.connection.validate import validate; validate()" - - - name: Restore images cache - uses: actions/cache@v3 - with: - path: .\tests\integration\image_cache - key: pyvista-image-cache-${{ runner.os }}-v-${{ env.RESET_IMAGE_CACHE }}-${{ hashFiles('pyproject.toml') }} - restore-keys: pyvista-image-cache-${{ runner.os }}-v-${{ env.RESET_IMAGE_CACHE }} - - - name: Run pytest - uses: ansys/actions/tests-pytest@v4 - env: - ALLOW_PLOTTING: true - with: - python-version: ${{ env.MAIN_PYTHON_VERSION }} - pytest-extra-args: "--use-existing-service=yes" - checkout: false - requires-xvfb: true - - - name: Stop the Geometry service - if: always() - run: | - docker stop ${{ env.GEO_CONT_NAME }} - docker logs ${{ env.GEO_CONT_NAME }} - docker rm ${{ env.GEO_CONT_NAME }} diff --git a/doc/source/assets.rst b/doc/source/assets.rst index 9cb69b2084..74324d8d0e 100644 --- a/doc/source/assets.rst +++ b/doc/source/assets.rst @@ -63,11 +63,19 @@ meaning that certain operations are not available or fail. Windows container ^^^^^^^^^^^^^^^^^ -* `Latest Geometry service binaries for Windows containers `_ +.. note:: + + Only users with access to https://github.com/ansys/pyansys-geometry-binaries can download these binaries. + +* `Latest Geometry service binaries for Windows containers `_ * `Latest Geometry service Dockerfile for Windows containers `_ Linux container ^^^^^^^^^^^^^^^ -* `Latest Geometry service binaries for Linux containers `_ +.. note:: + + Only users with access to https://github.com/ansys/pyansys-geometry-binaries can download these binaries. + +* `Latest Geometry service binaries for Linux containers `_ * `Latest Geometry service Dockerfile for Linux containers `_ diff --git a/doc/source/conf.py b/doc/source/conf.py index f52e779db2..80b1c8e914 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -249,6 +249,7 @@ def get_wheelhouse_assets_dictionary(): latex_elements = {"preamble": latex.generate_preamble(html_title)} linkcheck_exclude_documents = ["index", "getting_started/local/index", "assets"] +linkcheck_ignore = [r"https://github.com/ansys/pyansys-geometry-binaries/.*"] # -- Declare the Jinja context ----------------------------------------------- exclude_patterns = [] diff --git a/doc/source/getting_started/docker/linux_container.rst b/doc/source/getting_started/docker/linux_container.rst index 9fe2e81773..ed1bec9d1a 100644 --- a/doc/source/getting_started/docker/linux_container.rst +++ b/doc/source/getting_started/docker/linux_container.rst @@ -128,7 +128,11 @@ Prerequisites * Download the `latest Linux Dockerfile `_. * Download the `latest release artifacts for the Linux - Docker container (ZIP file) `_. + Docker container (ZIP file) according to your version `_. + +.. note:: + + Only users with access to https://github.com/ansys/pyansys-geometry-binaries can download these binaries. * Move this ZIP file to the location of the Linux Dockerfile previously downloaded. diff --git a/doc/source/getting_started/docker/windows_container.rst b/doc/source/getting_started/docker/windows_container.rst index 51d316141a..3bd5cb8f1a 100644 --- a/doc/source/getting_started/docker/windows_container.rst +++ b/doc/source/getting_started/docker/windows_container.rst @@ -115,7 +115,11 @@ Prerequisites * Download the `latest Windows Dockerfile `_. * Download the `latest release artifacts for the Windows - Docker container (ZIP file) `_. + Docker container (ZIP file) for your version `_. + +.. note:: + + Only users with access to https://github.com/ansys/pyansys-geometry-binaries can download these binaries. * Move this ZIP file to the location of the Windows Dockerfile previously downloaded. diff --git a/doc/styles/Vocab/ANSYS/accept.txt b/doc/styles/Vocab/ANSYS/accept.txt index cc1e7a9892..9b24bc51bb 100644 --- a/doc/styles/Vocab/ANSYS/accept.txt +++ b/doc/styles/Vocab/ANSYS/accept.txt @@ -11,6 +11,7 @@ Geometry geometry Geometry service Geometry models +github GitHub GitHub Container Registry namespace From 114d4e82163161553b71a678a95eecf80be782bc Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Sat, 7 Oct 2023 18:49:02 +0200 Subject: [PATCH 31/74] temp fix: comment out IGES test - Issue 801 --- tests/integration/test_design_import.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/integration/test_design_import.py b/tests/integration/test_design_import.py index 03e5a82bfb..56cb0b7c52 100644 --- a/tests/integration/test_design_import.py +++ b/tests/integration/test_design_import.py @@ -164,10 +164,14 @@ def test_open_file(modeler: Modeler, tmp_path_factory: pytest.TempPathFactory): _checker_method(design, design2, False) # IGES - file = tmp_path_factory.mktemp("test_design_import") / "two_cars.igs" - design.download(file, DesignFileFormat.IGES) - design2 = modeler.open_file(file) - _checker_method(design, design2, False) + # + # TODO: Something has gone wrong with IGES + # TODO: Issue https://github.com/ansys/pyansys-geometry/issues/801 + # + # file = tmp_path_factory.mktemp("test_design_import") / "two_cars.igs" + # design.download(file, DesignFileFormat.IGES) + # design2 = modeler.open_file(file) + # _checker_method(design, design2, False) # Catia design2 = modeler.open_file("./tests/integration/files/import/catia_car/car.CATProduct") From 9892e2b79bcb32293eb191a8dffb446a1172119b Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Sat, 7 Oct 2023 19:14:38 +0200 Subject: [PATCH 32/74] fix: workflow --- .github/workflows/ci_cd.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index f56be00ca5..38e38882a7 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -404,7 +404,7 @@ jobs: uses: actions/checkout@v4 with: repository: 'ansys/pyansys-geometry-binaries' - token: ${{ env.BINARIES_TOKEN }} + token: ${{ secrets.BINARIES_TOKEN }} - name: Download binaries run: | @@ -433,8 +433,8 @@ jobs: mkdir $env:VERSION mv windows-binaries.zip .\$env:VERSION\ mv linux-binaries.zip .\$env:VERSION\ - git config user.email ${{ env.BINARIES_EMAIL }} - git config user.name ${{ env.BINARIES_USERNAME }} + git config user.email ${{ secrets.BINARIES_EMAIL }} + git config user.name ${{ secrets.BINARIES_USERNAME }} git add * git commit -m "adding binaries for ${{ github.ref_name }}" git push origin main From 9c6a3993ed386b0b661c465a0d3f099d233216ed Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 15 Oct 2023 11:31:43 +0200 Subject: [PATCH 33/74] [pre-commit.ci] pre-commit autoupdate (#802) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d19056eea8..c598bf128a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: exclude: "tests/" - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-merge-conflict - id: debug-statements From 9b3ec3cc0ccc4864c031b1696fdd04ca319ef70d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 06:32:56 +0000 Subject: [PATCH 34/74] MAINT: Bump the docs-deps group with 2 updates (#808) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b9c10ed1de..5862299afd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,7 +68,7 @@ tests = [ "vtk==9.2.6", ] doc = [ - "ansys-sphinx-theme==0.12.2", + "ansys-sphinx-theme==0.12.3", "docker==6.1.3", "ipyvtklink==0.2.3", "jupyter_sphinx==0.4.0", @@ -76,7 +76,7 @@ doc = [ "myst-parser==2.0.0", "nbconvert==7.9.2", "nbsphinx==0.9.3", - "notebook==7.0.4", + "notebook==7.0.5", "numpydoc==1.6.0", "panel==1.2.3", "pyvista[trame]==0.41.1", From 030bcb34a4f460b2ffec43ccabdd884298bfb1e5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 11:35:16 +0200 Subject: [PATCH 35/74] MAINT: Bump the grpc-deps group with 2 updates (#807) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5862299afd..3d3fecaadb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,8 +50,8 @@ tests = [ "ansys-tools-path==0.3.1", "beartype==0.16.2", "docker==6.1.3", - "google-api-python-client==2.102.0", - "googleapis-common-protos==1.60.0", + "google-api-python-client==2.103.0", + "googleapis-common-protos==1.61.0", "grpcio==1.50.0", "grpcio-health-checking==1.48.2", "numpy==1.26.0", From 36dad018958b9dd6a85105effdf5b4813a88148a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 08:33:14 +0200 Subject: [PATCH 36/74] MAINT: Bump numpy from 1.26.0 to 1.26.1 (#811) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3d3fecaadb..1aab52918e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,7 @@ tests = [ "googleapis-common-protos==1.61.0", "grpcio==1.50.0", "grpcio-health-checking==1.48.2", - "numpy==1.26.0", + "numpy==1.26.1", "Pint==0.22", "protobuf==3.20.3", "pytest==7.4.2", From 91d95883cdd687577640fcfed455226066e2b920 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 08:33:53 +0200 Subject: [PATCH 37/74] MAINT: Bump beartype from 0.16.2 to 0.16.3 (#812) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1aab52918e..332616617a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ all = [ tests = [ "ansys-platform-instancemanagement==1.1.2", "ansys-tools-path==0.3.1", - "beartype==0.16.2", + "beartype==0.16.3", "docker==6.1.3", "google-api-python-client==2.103.0", "googleapis-common-protos==1.61.0", From 7eb252d69d351b97702cd011939aee712201c0e3 Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Thu, 19 Oct 2023 09:42:27 +0200 Subject: [PATCH 38/74] deactivate codestyle checks temporarily --- .github/workflows/ci_cd.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 38e38882a7..45c44d7c04 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -35,10 +35,13 @@ jobs: name: Documentation Style Check runs-on: ubuntu-latest steps: - - name: PyAnsys documentation style checks - uses: ansys/actions/doc-style@v4 - with: - token: ${{ secrets.GITHUB_TOKEN }} + # TODO - Fix codestyle issues + # - name: PyAnsys documentation style checks + # uses: ansys/actions/doc-style@v4 + # with: + # token: ${{ secrets.GITHUB_TOKEN }} + - name : TODO - Reactivate code style + run : sleep 1 smoke-tests: name: Build and Smoke tests From 9ddc6a32a821a58b1a1385b6400d2501d0581ca8 Mon Sep 17 00:00:00 2001 From: Matteo Bini <91963243+b-matteo@users.noreply.github.com> Date: Thu, 19 Oct 2023 09:54:20 +0200 Subject: [PATCH 39/74] Feat/change design id compression (#810) Co-authored-by: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- src/ansys/geometry/core/designer/design.py | 16 +++++++++++++--- tests/integration/test_design.py | 2 ++ tests/integration/test_design_import.py | 2 +- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 332616617a..dd850c3ace 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ ] dependencies = [ - "ansys-api-geometry==0.3.1", + "ansys-api-geometry==0.3.2", "ansys-tools-path>=0.3", "beartype>=0.11.0", "google-api-python-client>=1.7.11", diff --git a/src/ansys/geometry/core/designer/design.py b/src/ansys/geometry/core/designer/design.py index 223bea50d4..63b1a120c3 100644 --- a/src/ansys/geometry/core/designer/design.py +++ b/src/ansys/geometry/core/designer/design.py @@ -127,6 +127,7 @@ def __init__(self, name: str, grpc_client: GrpcClient, read_existing_design: boo self._materials = [] self._named_selections = {} self._beam_profiles = {} + self._design_id = "" # Check whether we want to process an existing design or create a new one. if read_existing_design: @@ -134,9 +135,15 @@ def __init__(self, name: str, grpc_client: GrpcClient, read_existing_design: boo self.__read_existing_design() else: new_design = self._design_stub.New(NewRequest(name=name)) - self._id = new_design.id + self._design_id = new_design.id + self._id = new_design.main_part.id self._grpc_client.log.debug("Design object instantiated successfully.") + @property + def design_id(self) -> str: + """The design's object unique id.""" + return self._design_id + @property def materials(self) -> List[Material]: """List of materials available for the design.""" @@ -587,6 +594,7 @@ def __read_existing_design(self) -> None: if not design: raise RuntimeError("No existing design available at service level.") else: + self._design_id = design.id self._id = design.main_part.id # Here we may take the design's name instead of the main part's name. # Since they're the same in the backend. @@ -597,11 +605,13 @@ def __read_existing_design(self) -> None: # Store created objects created_parts = {p.id: Part(p.id, p.name, [], []) for p in response.parts} created_tps = {} - created_components = {self.id: self} + created_components = {design.main_part.id: self} created_bodies = {} # Make dummy master for design since server doesn't have one - self._master_component = MasterComponent("1", "master_design", created_parts[self.id]) + self._master_component = MasterComponent( + "1", "master_design", created_parts[design.main_part.id] + ) # Create MasterComponents for master in response.transformed_parts: diff --git a/tests/integration/test_design.py b/tests/integration/test_design.py index 81b048bb25..35cd406bb5 100644 --- a/tests/integration/test_design.py +++ b/tests/integration/test_design.py @@ -47,6 +47,7 @@ def test_design_extrusion_and_material_assignment(modeler: Modeler): design_name = "ExtrudeProfile" design = modeler.create_design(design_name) assert design.name == design_name + assert design.design_id is not None assert design.id is not None assert design.parent_component is None assert len(design.components) == 0 @@ -175,6 +176,7 @@ def test_component_body(modeler: Modeler): design_name = "ComponentBody_Test" design = modeler.create_design(design_name) assert design.name == design_name + assert design.design_id is not None assert design.id is not None assert design.parent_component is None assert len(design.components) == 0 diff --git a/tests/integration/test_design_import.py b/tests/integration/test_design_import.py index 56cb0b7c52..a1f3b2baba 100644 --- a/tests/integration/test_design_import.py +++ b/tests/integration/test_design_import.py @@ -152,7 +152,7 @@ def test_open_file(modeler: Modeler, tmp_path_factory: pytest.TempPathFactory): if modeler.client.backend_type != BackendType.LINUX_SERVICE: design2 = modeler.open_file(file) - # assert the two cars are the same + # assert the two cars are the same, excepted for the ID, which should be different _checker_method(design, design2, True) # Test HOOPS formats (Windows only) From 272c0adbcbd77a2c1c9992becfdba828ca6056ad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 08:23:24 +0000 Subject: [PATCH 40/74] MAINT: Bump the grpc-deps group with 1 update (#813) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dd850c3ace..b71f5f39f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ tests = [ "ansys-tools-path==0.3.1", "beartype==0.16.3", "docker==6.1.3", - "google-api-python-client==2.103.0", + "google-api-python-client==2.104.0", "googleapis-common-protos==1.61.0", "grpcio==1.50.0", "grpcio-health-checking==1.48.2", From 077f7872c98ea3030dca7c115a10b6909e3a5b0b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 08:49:49 +0000 Subject: [PATCH 41/74] MAINT: Bump the docs-deps group with 1 update (#814) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b71f5f39f8..df0e8f21c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ doc = [ "myst-parser==2.0.0", "nbconvert==7.9.2", "nbsphinx==0.9.3", - "notebook==7.0.5", + "notebook==7.0.6", "numpydoc==1.6.0", "panel==1.2.3", "pyvista[trame]==0.41.1", From 1b1bb8fac0f5f0653de8c7dbac1c4cf69356a299 Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Fri, 20 Oct 2023 12:00:30 +0200 Subject: [PATCH 42/74] fix: download on nested directories not working (#818) --- src/ansys/geometry/core/designer/design.py | 5 +++++ tests/integration/test_design.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/ansys/geometry/core/designer/design.py b/src/ansys/geometry/core/designer/design.py index 63b1a120c3..21dbf02702 100644 --- a/src/ansys/geometry/core/designer/design.py +++ b/src/ansys/geometry/core/designer/design.py @@ -231,6 +231,11 @@ def download( if isinstance(file_location, Path): file_location = str(file_location) + # Check if the folder for the file location exists + if not Path(file_location).parent.exists(): + # Create the parent directory + Path(file_location).parent.mkdir(parents=True, exist_ok=True) + # Process response self._grpc_client.log.debug(f"Requesting design download in {format.value[0]} format.") received_bytes = bytes() diff --git a/tests/integration/test_design.py b/tests/integration/test_design.py index 35cd406bb5..d6ff644961 100644 --- a/tests/integration/test_design.py +++ b/tests/integration/test_design.py @@ -791,7 +791,7 @@ def test_download_file(modeler: Modeler, tmp_path_factory: pytest.TempPathFactor design.extrude_sketch(name="MyCylinder", sketch=sketch, distance=Quantity(50, UNITS.mm)) # Download the design - file = tmp_path_factory.mktemp("scdoc_files_download") / "cylinder.scdocx" + file = tmp_path_factory.mktemp("scdoc_files_download") / "dummy_folder" / "cylinder.scdocx" design.download(file) # Check that the file exists From 39dbc797360afd12fc6b17f6e6c144c05335e822 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 10:32:38 +0200 Subject: [PATCH 43/74] MAINT: Bump ansys-tools-path from 0.3.1 to 0.3.2 (#820) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index df0e8f21c4..043dfedcb3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ all = [ ] tests = [ "ansys-platform-instancemanagement==1.1.2", - "ansys-tools-path==0.3.1", + "ansys-tools-path==0.3.2", "beartype==0.16.3", "docker==6.1.3", "google-api-python-client==2.104.0", From a2efc93de181e36995806c72614d5d30854bfe95 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 09:36:05 +0200 Subject: [PATCH 44/74] MAINT: Bump beartype from 0.16.3 to 0.16.4 (#822) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 043dfedcb3..b17e85d538 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ all = [ tests = [ "ansys-platform-instancemanagement==1.1.2", "ansys-tools-path==0.3.2", - "beartype==0.16.3", + "beartype==0.16.4", "docker==6.1.3", "google-api-python-client==2.104.0", "googleapis-common-protos==1.61.0", From cf404624c31acf253510a413436098ce3b3bb9b5 Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Tue, 24 Oct 2023 12:21:25 +0200 Subject: [PATCH 45/74] feat: reactivate PMDB export (#821) --- tests/integration/test_design.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/integration/test_design.py b/tests/integration/test_design.py index d6ff644961..0f9f8716dd 100644 --- a/tests/integration/test_design.py +++ b/tests/integration/test_design.py @@ -811,7 +811,6 @@ def test_download_file(modeler: Modeler, tmp_path_factory: pytest.TempPathFactor text_parasolid_file = tmp_path_factory.mktemp("scdoc_files_download") / "cylinder.x_t" # Windows-only HOOPS exports for now - step_file = tmp_path_factory.mktemp("scdoc_files_download") / "cylinder.stp" design.download(step_file, format=DesignFileFormat.STEP) assert step_file.exists() @@ -821,11 +820,9 @@ def test_download_file(modeler: Modeler, tmp_path_factory: pytest.TempPathFactor assert iges_file.exists() # PMDB addin is Windows-only - # TODO: Requires resolution of https://github.com/ansys/pyansys-geometry/issues/710 - # - # pmdb_file = tmp_path_factory.mktemp("scdoc_files_download") / "cylinder.pmdb" - # design.download(pmdb_file, DesignFileFormat.PMDB) - # assert pmdb_file.exists() + pmdb_file = tmp_path_factory.mktemp("scdoc_files_download") / "cylinder.pmdb" + design.download(pmdb_file, DesignFileFormat.PMDB) + assert pmdb_file.exists() # Linux backend... else: From 470b927596d970debedaaa4d80f2981d6ce1b680 Mon Sep 17 00:00:00 2001 From: Umut Soysal <97195410+umutsoysalansys@users.noreply.github.com> Date: Tue, 24 Oct 2023 11:43:25 -0500 Subject: [PATCH 46/74] feat: edge repair tools (#648) Co-authored-by: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- CONTRIBUTORS.md | 17 +- src/ansys/geometry/core/modeler.py | 14 + src/ansys/geometry/core/tools/__init__.py | 30 ++ .../geometry/core/tools/problem_areas.py | 362 ++++++++++++++++++ .../core/tools/repair_tool_message.py | 61 +++ src/ansys/geometry/core/tools/repair_tools.py | 275 +++++++++++++ .../files/DuplicateFacesDesignBefore.scdocx | Bin 0 -> 25312 bytes .../files/ExtraEdgesDesignBefore.scdocx | Bin 0 -> 26850 bytes .../files/InExactEdgesBefore.scdocx | Bin 0 -> 59337 bytes .../files/MissingFacesDesignBefore.scdocx | Bin 0 -> 24017 bytes .../integration/files/SmallFacesBefore.scdocx | Bin 0 -> 21414 bytes .../files/SplitEdgeDesignTest.scdocx | Bin 0 -> 24475 bytes tests/integration/files/stitch_before.scdocx | Bin 0 -> 30047 bytes tests/integration/test_repair_tools.py | 254 ++++++++++++ 15 files changed, 1006 insertions(+), 9 deletions(-) create mode 100644 src/ansys/geometry/core/tools/__init__.py create mode 100644 src/ansys/geometry/core/tools/problem_areas.py create mode 100644 src/ansys/geometry/core/tools/repair_tool_message.py create mode 100644 src/ansys/geometry/core/tools/repair_tools.py create mode 100644 tests/integration/files/DuplicateFacesDesignBefore.scdocx create mode 100644 tests/integration/files/ExtraEdgesDesignBefore.scdocx create mode 100644 tests/integration/files/InExactEdgesBefore.scdocx create mode 100644 tests/integration/files/MissingFacesDesignBefore.scdocx create mode 100644 tests/integration/files/SmallFacesBefore.scdocx create mode 100644 tests/integration/files/SplitEdgeDesignTest.scdocx create mode 100644 tests/integration/files/stitch_before.scdocx create mode 100644 tests/integration/test_repair_tools.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c598bf128a..b52cf98607 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,7 +32,7 @@ repos: rev: v2.2.6 hooks: - id: codespell - args: ["--ignore-words", "doc/styles/Vocab/ANSYS/accept.txt"] + args: ["--ignore-words", "doc/styles/Vocab/ANSYS/accept.txt", "-w"] - repo: https://github.com/pycqa/pydocstyle rev: 6.3.0 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index cc99b09c28..f3cc733294 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -6,15 +6,16 @@ ## Individual Contributors -* [Jonah Boling](https://github.com/jonahrb) -* [Matteo Bini](https://github.com/b-matteo) -* [Chris Hawkins](https://github.com/chris-hawkins-usa) -* [Chad Queen](https://github.com/chadqueen) -* [Revathy Venugopal](https://github.com/Revathyvenugopal162) -* [Maxime Rey](https://github.com/MaxJPRey) +* [Alejandro FernÃĄndez](https://github.com/AlejandroFernandezLuces) * [Alexander Kaszynski](https://github.com/akaszynski) +* [Chad Queen](https://github.com/chadqueen) +* [Chris Hawkins](https://github.com/chris-hawkins-usa) +* [Dastan Abdulla](https://github.com/dastan-ansys) +* [Jonah Boling](https://github.com/jonahrb) * [Jorge Martínez](https://github.com/jorgepiloto) -* [Alejandro FernÃĄndez](https://github.com/AlejandroFernandezLuces) * [Lance Lance](https://github.com/LanceX2214) -* [Dastan Abdulla](https://github.com/dastan-ansys) +* [Matteo Bini](https://github.com/b-matteo) +* [Maxime Rey](https://github.com/MaxJPRey) +* [Revathy Venugopal](https://github.com/Revathyvenugopal162) * [Riccardo Manno](https://github.com/rmanno91) +* [Umut Soysal](https://github.com/umutsoysal) diff --git a/src/ansys/geometry/core/modeler.py b/src/ansys/geometry/core/modeler.py index 349f3140d1..1bbdbc49a8 100644 --- a/src/ansys/geometry/core/modeler.py +++ b/src/ansys/geometry/core/modeler.py @@ -40,6 +40,7 @@ from ansys.geometry.core.logger import LOG as logger from ansys.geometry.core.misc.checks import check_type from ansys.geometry.core.misc.options import ImportOptions +from ansys.geometry.core.tools.repair_tools import RepairTools from ansys.geometry.core.typing import Real if TYPE_CHECKING: # pragma: no cover @@ -115,6 +116,14 @@ def __init__( backend_type=backend_type, ) + # Initialize the RepairTools - Not available on Linux + # TODO: delete "if" when Linux service is able to use repair tools + if self.client.backend_type == BackendType.LINUX_SERVICE: + self._repair_tools = None + logger.warning("Linux backend does not support repair tools.") + else: + self._repair_tools = RepairTools(self._client) + # Design[] maintaining references to all designs within the modeler workspace self._designs = [] @@ -341,3 +350,8 @@ def run_discovery_script_file( return (response.values, self.read_existing_design()) else: return response.values + + @property + def repair_tools(self) -> RepairTools: + """Access to repair tools.""" + return self._repair_tools diff --git a/src/ansys/geometry/core/tools/__init__.py b/src/ansys/geometry/core/tools/__init__.py new file mode 100644 index 0000000000..86f427d171 --- /dev/null +++ b/src/ansys/geometry/core/tools/__init__.py @@ -0,0 +1,30 @@ +# Copyright (C) 2023 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 tools subpackage.""" + +from ansys.geometry.core.tools.problem_areas import ( + DuplicateFaceProblemAreas, + ExtraEdgeProblemAreas, + InexactEdgeProblemAreas, +) +from ansys.geometry.core.tools.repair_tool_message import RepairToolMessage +from ansys.geometry.core.tools.repair_tools import RepairTools diff --git a/src/ansys/geometry/core/tools/problem_areas.py b/src/ansys/geometry/core/tools/problem_areas.py new file mode 100644 index 0000000000..b3dff7fc55 --- /dev/null +++ b/src/ansys/geometry/core/tools/problem_areas.py @@ -0,0 +1,362 @@ +# Copyright (C) 2023 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. +"""The problem area definition.""" +from abc import abstractmethod + +from ansys.api.geometry.v0.repairtools_pb2 import ( + FixDuplicateFacesRequest, + FixInexactEdgesRequest, + FixMissingFacesRequest, + FixSmallFacesRequest, + FixSplitEdgesRequest, + FixStitchFacesRequest, +) +from ansys.api.geometry.v0.repairtools_pb2_grpc import RepairToolsStub +from beartype.typing import List +from google.protobuf.wrappers_pb2 import Int32Value + +from ansys.geometry.core.connection import GrpcClient +from ansys.geometry.core.tools.repair_tool_message import RepairToolMessage + + +class ProblemArea: + """ + Represents problem areas. + + Parameters + ---------- + id : str + Server-defined ID for the problem area. + grpc_client : GrpcClient + Active supporting geometry service instance for design modeling. + """ + + def __init__(self, id: str, grpc_client: GrpcClient): + """Initialize a new instance of a problem area class.""" + self._id = id + self._id_grpc = Int32Value(value=int(id)) + self._repair_stub = RepairToolsStub(grpc_client.channel) + + @property + def id(self) -> str: + """The id of the problem area.""" + return self._id + + @abstractmethod + def fix(self): + """Fix problem area.""" + raise NotImplementedError("Fix method is not implemented in the base class.") + + +class DuplicateFaceProblemAreas(ProblemArea): + """ + Provides duplicate face problem area definition. + + Represents a duplicate face problem area with unique identifier and associated faces. + + Parameters + ---------- + id : str + Server-defined ID for the body. + grpc_client : GrpcClient + Active supporting geometry service instance for design modeling. + faces : List[str] + List of faces associated with the design. + """ + + def __init__(self, id: str, faces: List[str], grpc_client: GrpcClient): + """Initialize a new instance of the duplicate face problem area class.""" + super().__init__(id, grpc_client) + self._faces = faces + + @property + def faces(self) -> List[str]: + """ + The list of the problem area ids. + + This method returns the list of problem area ids with duplicate faces. + """ + return self._faces + + def fix(self) -> RepairToolMessage: + """ + Fix the problem area. + + Returns + ------- + message: RepairToolMessage + Message containing created and/or modified bodies. + """ + response = self._repair_stub.FixDuplicateFaces( + FixDuplicateFacesRequest(duplicate_face_problem_area_id=self._id_grpc) + ) + message = RepairToolMessage( + response.result.success, + response.result.created_bodies_monikers, + response.result.modified_bodies_monikers, + ) + return message + + +class MissingFaceProblemAreas(ProblemArea): + """ + Provides missing face problem area definition. + + Parameters + ---------- + id : str + Server-defined ID for the body. + grpc_client : GrpcClient + Active supporting geometry service instance for design modeling. + edges : List[str] + List of edges associated with the design. + """ + + def __init__(self, id: str, edges: List[str], grpc_client: GrpcClient): + """Initialize a new instance of the missing face problem area class.""" + super().__init__(id, grpc_client) + self._edges = edges + + @property + def edges(self) -> List[str]: + """The list of the ids of the edges connected to this problem area.""" + return self._edges + + def fix(self) -> RepairToolMessage: + """ + Fix the problem area. + + Returns + ------- + message: RepairToolMessage + Message containing created and/or modified bodies. + """ + response = self._repair_stub.FixMissingFaces( + FixMissingFacesRequest(missing_face_problem_area_id=self._id_grpc) + ) + message = RepairToolMessage( + response.result.success, + response.result.created_bodies_monikers, + response.result.modified_bodies_monikers, + ) + return message + + +class InexactEdgeProblemAreas(ProblemArea): + """ + Represents an inexact edge problem area with unique identifier and associated edges. + + Parameters + ---------- + id : str + Server-defined ID for the body. + grpc_client : GrpcClient + Active supporting geometry service instance for design modeling. + edges : List[str] + List of edges associated with the design. + """ + + def __init__(self, id: str, edges: List[str], grpc_client: GrpcClient): + """Initialize a new instance of the inexact edge problem area class.""" + super().__init__(id, grpc_client) + self._edges = edges + + @property + def edges(self) -> List[str]: + """The list of the ids of the edges connected to this problem area.""" + return self._edges + + def fix(self) -> RepairToolMessage: + """ + Fix the problem area. + + Returns + ------- + message: RepairToolMessage + Message containing created and/or modified bodies. + """ + response = self._repair_stub.FixInexactEdges( + FixInexactEdgesRequest(inexact_edge_problem_area_id=self._id_grpc) + ) + message = RepairToolMessage( + response.result.success, + response.result.created_bodies_monikers, + response.result.modified_bodies_monikers, + ) + return message + + +class ExtraEdgeProblemAreas(ProblemArea): + """ + Represents a extra edge problem area with unique identifier and associated edges. + + Parameters + ---------- + id : str + Server-defined ID for the body. + grpc_client : GrpcClient + Active supporting geometry service instance for design modeling. + edges : List[str] + List of edges associated with the design. + """ + + def __init__(self, id: str, edges: List[str], grpc_client: GrpcClient): + """Initialize a new instance of the extra edge problem area class.""" + super().__init__(id, grpc_client) + self._edges = edges + + @property + def edges(self) -> List[str]: + """The list of the ids of the edges connected to this problem area.""" + return self._edges + + +class SmallFaceProblemAreas(ProblemArea): + """ + Represents a small face problem area with unique identifier and associated faces. + + Parameters + ---------- + id : str + Server-defined ID for the body. + grpc_client : GrpcClient + Active supporting geometry service instance for design modeling. + faces : List[str] + List of edges associated with the design. + """ + + def __init__(self, id: str, faces: List[str], grpc_client: GrpcClient): + """Initialize a new instance of the small face problem area class.""" + super().__init__(id, grpc_client) + self._faces = faces + + @property + def faces(self) -> List[str]: + """The list of the ids of the edges connected to this problem area.""" + return self._faces + + def fix(self) -> RepairToolMessage: + """ + Fix the problem area. + + Returns + ------- + message: RepairToolMessage + Message containing created and/or modified bodies. + """ + response = self._repair_stub.FixSmallFaces( + FixSmallFacesRequest(small_face_problem_area_id=self._id_grpc) + ) + message = RepairToolMessage( + response.result.success, + response.result.created_bodies_monikers, + response.result.modified_bodies_monikers, + ) + return message + + +class SplitEdgeProblemAreas(ProblemArea): + """ + Represents a split edge problem area with unique identifier and associated edges. + + Parameters + ---------- + id : str + Server-defined ID for the body. + grpc_client : GrpcClient + Active supporting geometry service instance for design modeling. + edges : List[str] + List of edges associated with the design. + """ + + def __init__(self, id: str, edges: List[str], grpc_client: GrpcClient): + """Initialize a new instance of the split edge problem area class.""" + super().__init__(id, grpc_client) + self._edges = edges + + @property + def edges(self) -> List[str]: + """The list of the ids of the edges connected to this problem area.""" + return self._edges + + def fix(self) -> RepairToolMessage: + """ + Fix the problem area. + + Returns + ------- + message: RepairToolMessage + Message containing created and/or modified bodies. + """ + response = self._repair_stub.FixSplitEdges( + FixSplitEdgesRequest(split_edge_problem_area_id=self._id_grpc) + ) + message = RepairToolMessage( + response.result.success, + response.result.created_bodies_monikers, + response.result.modified_bodies_monikers, + ) + return message + + +class StitchFaceProblemAreas(ProblemArea): + """ + Represents a stitch face problem area with unique identifier and associated faces. + + Parameters + ---------- + id : str + Server-defined ID for the body. + grpc_client : GrpcClient + Active supporting geometry service instance for design modeling. + faces : List[str] + List of faces associated with the design. + """ + + def __init__(self, id: str, faces: List[str], grpc_client: GrpcClient): + """Initialize a new instance of the stitch face problem area class.""" + super().__init__(id, grpc_client) + self._faces = faces + + @property + def faces(self) -> List[str]: + """The list of the ids of the faces connected to this problem area.""" + return self._faces + + def fix(self) -> RepairToolMessage: + """ + Fix the problem area. + + Returns + ------- + message: RepairToolMessage + Message containing created and/or modified bodies. + """ + response = self._repair_stub.FixStitchFaces( + FixStitchFacesRequest(stitch_face_problem_area_id=self._id_grpc) + ) + message = RepairToolMessage( + response.result.success, + response.result.created_bodies_monikers, + response.result.modified_bodies_monikers, + ) + return message diff --git a/src/ansys/geometry/core/tools/repair_tool_message.py b/src/ansys/geometry/core/tools/repair_tool_message.py new file mode 100644 index 0000000000..ce86600ac4 --- /dev/null +++ b/src/ansys/geometry/core/tools/repair_tool_message.py @@ -0,0 +1,61 @@ +# Copyright (C) 2023 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. +"""Module for repair tool message.""" + + +from beartype.typing import List + + +class RepairToolMessage: + """Provides return message for the repair tool methods.""" + + def __init__(self, success: bool, created_bodies: List[str], modified_bodies: List[str]): + """ + Initialize a new instance of the extra edge problem area class. + + Parameters + ---------- + success: bool + True if the repair operation was effective, false if it is not. + created_bodies: List[str] + List of bodies created after the repair operation. + modified_bodies: List[str] + List of bodies modified after the repair operation. + """ + self._success = success + self._created_bodies = created_bodies + self._modified_bodies = modified_bodies + + @property + def success(self) -> bool: + """The success of the repair operation.""" + return self._success + + @property + def created_bodies(self) -> List[str]: + """The list of the created bodies after the repair operation.""" + return self._created_bodies + + @property + def modified_bodies(self) -> List[str]: + """The list of the modified bodies after the repair operation.""" + return self._modified_bodies diff --git a/src/ansys/geometry/core/tools/repair_tools.py b/src/ansys/geometry/core/tools/repair_tools.py new file mode 100644 index 0000000000..472ad87b61 --- /dev/null +++ b/src/ansys/geometry/core/tools/repair_tools.py @@ -0,0 +1,275 @@ +# Copyright (C) 2023 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 tools for repairing bodies.""" + +from ansys.api.geometry.v0.repairtools_pb2 import ( + FindDuplicateFacesRequest, + FindExtraEdgesRequest, + FindInexactEdgesRequest, + FindMissingFacesRequest, + FindSmallFacesRequest, + FindSplitEdgesRequest, + FindStitchFacesRequest, +) +from ansys.api.geometry.v0.repairtools_pb2_grpc import RepairToolsStub +from beartype.typing import List +from google.protobuf.wrappers_pb2 import DoubleValue + +from ansys.geometry.core.connection import GrpcClient +from ansys.geometry.core.tools.problem_areas import ( + DuplicateFaceProblemAreas, + ExtraEdgeProblemAreas, + InexactEdgeProblemAreas, + MissingFaceProblemAreas, + SmallFaceProblemAreas, + SplitEdgeProblemAreas, + StitchFaceProblemAreas, +) +from ansys.geometry.core.typing import Real + + +class RepairTools: + """Repair tools for PyAnsys Geometry.""" + + def __init__(self, grpc_client: GrpcClient): + """Initialize Repair Tools class.""" + self._grpc_client = grpc_client + self._repair_stub = RepairToolsStub(self._grpc_client.channel) + + def find_split_edges( + self, ids: List[str], angle: Real = 0.0, length: Real = 0.0 + ) -> List[SplitEdgeProblemAreas]: + """ + Find split edges in the given list of bodies. + + This method finds the split edge problem areas and returns a list of split edge + problem areas objects. + + Parameters + ---------- + ids : List[str] + Server-defined ID for the bodies. + angle : Real + The maximum angle between edges. + length : Real + The maximum length of the edges. + + Returns + ---------- + List[SplitEdgeProblemAreas] + List of objects representing split edge problem areas. + """ + angle_value = DoubleValue(value=float(angle)) + length_value = DoubleValue(value=float(length)) + problem_areas_response = self._repair_stub.FindSplitEdges( + FindSplitEdgesRequest(bodies_or_faces=ids, angle=angle_value, distance=length_value) + ) + + problem_areas = [] + for res in problem_areas_response.result: + connected_edges = [] + for edge_moniker in res.edge_monikers: + connected_edges.append(edge_moniker) + problem_area = SplitEdgeProblemAreas(res.id, connected_edges, self._grpc_client) + problem_areas.append(problem_area) + + return problem_areas + + def find_extra_edges(self, ids: List[str]) -> List[ExtraEdgeProblemAreas]: + """ + Find the extra edges in the given list of bodies. + + This method find the extra edge problem areas and returns a list of extra edge + problem areas objects. + + Parameters + ---------- + ids : List[str] + Server-defined ID for the bodies. + + Returns + ---------- + List[ExtraEdgeProblemArea] + List of objects representing extra edge problem areas. + """ + problem_areas_response = self._repair_stub.FindExtraEdges( + FindExtraEdgesRequest(selection=ids) + ) + problem_areas = [] + for res in problem_areas_response.result: + connected_edges = [] + for edge_moniker in res.edge_monikers: + connected_edges.append(edge_moniker) + problem_area = ExtraEdgeProblemAreas(res.id, connected_edges, self._grpc_client) + problem_areas.append(problem_area) + + return problem_areas + + def find_inexact_edges(self, ids) -> List[InexactEdgeProblemAreas]: + """ + Find inexact edges in the given list of bodies. + + This method find the inexact edge problem areas and returns a list of inexact + edge problem areas objects. + + Parameters + ---------- + ids : List[str] + Server-defined ID for the bodies. + + Returns + ------- + List[InExactEdgeProblemArea] + List of objects representing inexact edge problem areas. + """ + problem_areas_response = self._repair_stub.FindInexactEdges( + FindInexactEdgesRequest(selection=ids) + ) + problem_areas = [] + for res in problem_areas_response.result: + connected_edges = [] + for edge_moniker in res.edge_monikers: + connected_edges.append(edge_moniker) + problem_area = InexactEdgeProblemAreas(res.id, connected_edges, self._grpc_client) + problem_areas.append(problem_area) + + return problem_areas + + def find_duplicate_faces(self, ids) -> List[DuplicateFaceProblemAreas]: + """ + Find the duplicate face problem areas. + + This method finds the duplicate face problem areas and returns a list of + duplicate face problem areas objects. + + Parameters + ---------- + ids : List[str] + Server-defined ID for the bodies. + + Returns + ------- + List[DuplicateFaceProblemAreas] + List of objects representing duplicate face problem areas. + """ + problem_areas_response = self._repair_stub.FindDuplicateFaces( + FindDuplicateFacesRequest(faces=ids) + ) + problem_areas = [] + for res in problem_areas_response.result: + connected_edges = [] + for face_moniker in res.face_monikers: + connected_edges.append(face_moniker) + problem_area = DuplicateFaceProblemAreas(res.id, connected_edges, self._grpc_client) + problem_areas.append(problem_area) + + return problem_areas + + def find_missing_faces(self, ids) -> List[MissingFaceProblemAreas]: + """ + Find the missing faces. + + This method find the missing face problem areas and returns a list of missing + face problem areas objects. + + Parameters + ---------- + ids : List[str] + Server-defined ID for the bodies. + + Returns + ------- + List[MissingFaceProblemAreas] + List of objects representing missing face problem areas. + """ + problem_areas_response = self._repair_stub.FindMissingFaces( + FindMissingFacesRequest(faces=ids) + ) + problem_areas = [] + for res in problem_areas_response.result: + connected_edges = [] + for edge_moniker in res.edge_monikers: + connected_edges.append(edge_moniker) + problem_area = MissingFaceProblemAreas(res.id, connected_edges, self._grpc_client) + problem_areas.append(problem_area) + + return problem_areas + + def find_small_faces(self, ids) -> List[SmallFaceProblemAreas]: + """ + Find the small face problem areas. + + This method finds and returns a list of ids of small face problem areas + objects. + + Parameters + ---------- + ids : List[str] + Server-defined ID for the bodies. + + Returns + ------- + List[SmallFaceProblemAreas] + List of objects representing small face problem areas. + """ + problem_areas_response = self._repair_stub.FindSmallFaces( + FindSmallFacesRequest(selection=ids) + ) + problem_areas = [] + for res in problem_areas_response.result: + connected_edges = [] + for face_moniker in res.face_monikers: + connected_edges.append(face_moniker) + problem_area = SmallFaceProblemAreas(res.id, connected_edges, self._grpc_client) + problem_areas.append(problem_area) + + return problem_areas + + def find_stitch_faces(self, ids) -> List[StitchFaceProblemAreas]: + """ + Return the list of stitch face problem areas. + + This method find the stitch face problem areas and returns a list of ids of stitch face + problem areas objects. + + Parameters + ---------- + ids : List[str] + Server-defined ID for the bodies. + + Returns + ------- + List[StitchFaceProblemAreas] + List of objects representing stitch face problem areas. + """ + problem_areas_response = self._repair_stub.FindStitchFaces( + FindStitchFacesRequest(faces=ids) + ) + problem_areas = [] + for res in problem_areas_response.result: + connected_edges = [] + for face_moniker in res.body_monikers: + connected_edges.append(face_moniker) + problem_area = StitchFaceProblemAreas(res.id, connected_edges, self._grpc_client) + problem_areas.append(problem_area) + + return problem_areas diff --git a/tests/integration/files/DuplicateFacesDesignBefore.scdocx b/tests/integration/files/DuplicateFacesDesignBefore.scdocx new file mode 100644 index 0000000000000000000000000000000000000000..6687718aefa8c120f982eca3d4653a5b4cdee858 GIT binary patch literal 25312 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 literal 0 HcmV?d00001 diff --git a/tests/integration/files/ExtraEdgesDesignBefore.scdocx b/tests/integration/files/ExtraEdgesDesignBefore.scdocx new file mode 100644 index 0000000000000000000000000000000000000000..df67ae1a08fce408f6373f616278de485131b1c1 GIT binary patch literal 26850 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

+C!hStPjXLpWm*wL1OQtf*rxn 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 F`Y)~`WXS*k literal 0 HcmV?d00001 diff --git a/tests/integration/files/MissingFacesDesignBefore.scdocx b/tests/integration/files/MissingFacesDesignBefore.scdocx new file mode 100644 index 0000000000000000000000000000000000000000..3de7779ac7933d2cc7d0dbcc72c3e6584ee20ad2 GIT binary patch literal 24017 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 B=mh`( literal 0 HcmV?d00001 diff --git a/tests/integration/files/SmallFacesBefore.scdocx b/tests/integration/files/SmallFacesBefore.scdocx new file mode 100644 index 0000000000000000000000000000000000000000..6c79328bf7926c136652d31e817ab622a5a0a610 GIT binary patch literal 21414 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 literal 0 HcmV?d00001 diff --git a/tests/integration/files/SplitEdgeDesignTest.scdocx b/tests/integration/files/SplitEdgeDesignTest.scdocx new file mode 100644 index 0000000000000000000000000000000000000000..72b5f4738b502b41e072ed7c680c603016c3d168 GIT binary patch literal 24475 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�i;>0HNjixCuVDTEX`a1@7e>hQ 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

  • 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 Zg%whj7~svy22#ckggL;a@^=eBJOBe>+;so| literal 0 HcmV?d00001 diff --git a/tests/integration/test_repair_tools.py b/tests/integration/test_repair_tools.py new file mode 100644 index 0000000000..0d81bc28a2 --- /dev/null +++ b/tests/integration/test_repair_tools.py @@ -0,0 +1,254 @@ +""""Testing of repair tools.""" + +import pytest + +from ansys.geometry.core.connection.backend import BackendType +from ansys.geometry.core.modeler import Modeler + + +# TODO: re-enable when Linux service is able to use repair tools +def skip_if_linux(modeler: Modeler): + """Skip test if running on Linux.""" + if modeler.client.backend_type == BackendType.LINUX_SERVICE: + pytest.skip("Repair tools not available on Linux service.") + + +def test_find_split_edges(modeler: Modeler): + """Test if split edge problem areas are detectable.""" + skip_if_linux(modeler) # Skip test on Linux + modeler.open_file("./tests/integration/files/SplitEdgeDesignTest.scdocx") + problem_areas = modeler.repair_tools.find_split_edges(["0:39"], 25, 150) + assert len(problem_areas) == 3 + + +def test_find_split_edge_id(modeler: Modeler): + """Test whether problem area has the id.""" + skip_if_linux(modeler) # Skip test on Linux + modeler.open_file("./tests/integration/files/SplitEdgeDesignTest.scdocx") + problem_areas = modeler.repair_tools.find_split_edges(["0:39"], 25, 150) + assert problem_areas[0].id > 0 + + +def test_find_split_edge_edges(modeler: Modeler): + """Test to find split edge problem areas with the connected edges.""" + skip_if_linux(modeler) # Skip test on Linux + modeler.open_file("./tests/integration/files/SplitEdgeDesignTest.scdocx") + problem_areas = modeler.repair_tools.find_split_edges(["0:39"], 25, 150) + assert len(problem_areas[0].edges) > 0 + + +def test_fix_split_edge(modeler: Modeler): + """Test to find and fix split edge problem areas.""" + skip_if_linux(modeler) # Skip test on Linux + modeler.open_file("./tests/integration/files/SplitEdgeDesignTest.scdocx") + problem_areas = modeler.repair_tools.find_split_edges(["0:39"], 25, 150) + assert problem_areas[0].fix().success + + +def test_find_extra_edges(modeler: Modeler): + """Test to read geometry and find it's extra edge problem areas.""" + skip_if_linux(modeler) # Skip test on Linux + modeler.open_file("./tests/integration/files/ExtraEdgesDesignBefore.scdocx") + problem_areas = modeler.repair_tools.find_extra_edges(["0:22"]) + assert len(problem_areas) == 1 + + +def test_find_extra_edge_id(modeler: Modeler): + """Test whether problem area has the id.""" + skip_if_linux(modeler) # Skip test on Linux + modeler.open_file("./tests/integration/files/ExtraEdgesDesignBefore.scdocx") + problem_areas = modeler.repair_tools.find_extra_edges(["0:22"]) + assert problem_areas[0].id > 0 + + +def test_find_extra_edge_edges(modeler: Modeler): + """Test to read geometry and find it's extra edge problem area with connected + edges.""" + skip_if_linux(modeler) # Skip test on Linux + modeler.open_file("./tests/integration/files/ExtraEdgesDesignBefore.scdocx") + problem_areas = modeler.repair_tools.find_extra_edges(["0:22"]) + assert len(problem_areas[0].edges) > 0 + + +def test_find_inexact_edges(modeler: Modeler): + """Test to read geometry and find it's inexact edge problem areas.""" + skip_if_linux(modeler) # Skip test on Linux + modeler.open_file("./tests/integration/files/InExactEdgesBefore.scdocx") + problem_areas = modeler.repair_tools.find_inexact_edges(["0:38"]) + assert len(problem_areas) == 12 + + +def test_find_inexact_edge_id(modeler: Modeler): + """Test whether problem area has the id.""" + skip_if_linux(modeler) # Skip test on Linux + modeler.open_file("./tests/integration/files/InExactEdgesBefore.scdocx") + problem_areas = modeler.repair_tools.find_inexact_edges(["0:38"]) + assert problem_areas[0].id > 0 + + +def test_find_inexact_edge_edges(modeler: Modeler): + """Test to read geometry and find it's inexact edge problem areas with connected + edges.""" + skip_if_linux(modeler) # Skip test on Linux + modeler.open_file("./tests/integration/files/InExactEdgesBefore.scdocx") + problem_areas = modeler.repair_tools.find_inexact_edges(["0:38"]) + assert len(problem_areas[0].edges) > 0 + + +def test_fix_inexact_edge(modeler: Modeler): + """Test to read geometry and find and fix it's inexact edge problem areas.""" + skip_if_linux(modeler) # Skip test on Linux + modeler.open_file("./tests/integration/files/InExactEdgesBefore.scdocx") + problem_areas = modeler.repair_tools.find_inexact_edges(["0:38"]) + assert problem_areas[0].fix().success + + +def test_find_missing_faces(modeler: Modeler): + """Test to read geometry and find it's missing face problem areas.""" + skip_if_linux(modeler) # Skip test on Linux + modeler.open_file("./tests/integration/files/MissingFacesDesignBefore.scdocx") + problem_areas = modeler.repair_tools.find_missing_faces(["1:40"]) + assert len(problem_areas) == 1 + + +def test_find_missing_face_id(modeler: Modeler): + """Test whether problem area has the id.""" + skip_if_linux(modeler) # Skip test on Linux + modeler.open_file("./tests/integration/files/MissingFacesDesignBefore.scdocx") + problem_areas = modeler.repair_tools.find_missing_faces(["1:40"]) + assert problem_areas[0].id > 0 + + +def test_find_missing_face_faces(modeler: Modeler): + """Test to read geometry and find it's missing face problem area with connected + edges.""" + skip_if_linux(modeler) # Skip test on Linux + modeler.open_file("./tests/integration/files/MissingFacesDesignBefore.scdocx") + problem_areas = modeler.repair_tools.find_missing_faces(["1:40"]) + assert len(problem_areas[0].edges) > 0 + + +def test_fix_missing_face(modeler: Modeler): + """Test to read geometry and find and fix it's missing face problem areas.""" + skip_if_linux(modeler) # Skip test on Linux + modeler.open_file("./tests/integration/files/MissingFacesDesignBefore.scdocx") + problem_areas = modeler.repair_tools.find_missing_faces(["1:40"]) + assert problem_areas[0].fix().success + + +def test_find_duplicate_faces(modeler: Modeler): + """Test to read geometry and find it's duplicate face problem areas.""" + skip_if_linux(modeler) # Skip test on Linux + modeler.open_file("./tests/integration/files/DuplicateFacesDesignBefore.scdocx") + problem_areas = modeler.repair_tools.find_duplicate_faces(["0:22", "0:85"]) + assert len(problem_areas) == 1 + + +def test_duplicate_face_id(modeler: Modeler): + """Test whether duplicate face problem area has the id.""" + skip_if_linux(modeler) # Skip test on Linux + modeler.open_file("./tests/integration/files/DuplicateFacesDesignBefore.scdocx") + problem_areas = modeler.repair_tools.find_duplicate_faces(["0:22", "0:85"]) + assert problem_areas[0].id > 0 + + +def test_duplicate_face_faces(modeler: Modeler): + """Test to read geometry and find it's duplicate face problem area and its connected + faces.""" + skip_if_linux(modeler) # Skip test on Linux + modeler.open_file("./tests/integration/files/DuplicateFacesDesignBefore.scdocx") + problem_areas = modeler.repair_tools.find_duplicate_faces(["0:22", "0:85"]) + assert len(problem_areas[0].faces) > 0 + + +def test_fix_duplicate_face(modeler: Modeler): + """Test to read geometry and find and fix it's duplicate face problem areas.""" + skip_if_linux(modeler) # Skip test on Linux + modeler.open_file("./tests/integration/files/DuplicateFacesDesignBefore.scdocx") + problem_areas = modeler.repair_tools.find_duplicate_faces(["0:22", "0:85"]) + assert problem_areas[0].fix().success + + +def test_find_small_faces(modeler: Modeler): + """Test to read geometry and find it's small face problem areas.""" + skip_if_linux(modeler) # Skip test on Linux + modeler.open_file("./tests/integration/files/SmallFacesBefore.scdocx") + problem_areas = modeler.repair_tools.find_small_faces(["0:38"]) + assert len(problem_areas) == 4 + + +def test_find_small_face_id(modeler: Modeler): + """Test whether problem area has the id.""" + skip_if_linux(modeler) # Skip test on Linux + modeler.open_file("./tests/integration/files/SmallFacesBefore.scdocx") + problem_areas = modeler.repair_tools.find_small_faces(["0:38"]) + assert problem_areas[0].id > 0 + + +def test_find_small_face_faces(modeler: Modeler): + """Test to read geometry, find it's small face problem area and return connected + faces.""" + skip_if_linux(modeler) # Skip test on Linux + modeler.open_file("./tests/integration/files/SmallFacesBefore.scdocx") + problem_areas = modeler.repair_tools.find_small_faces(["0:38"]) + assert len(problem_areas[0].faces) > 0 + + +def test_fix_small_face(modeler: Modeler): + """Test to read geometry and find and fix it's small face problem areas.""" + skip_if_linux(modeler) # Skip test on Linux + modeler.open_file("./tests/integration/files/SmallFacesBefore.scdocx") + problem_areas = modeler.repair_tools.find_small_faces(["0:38"]) + assert problem_areas[0].fix().success > 0 + + +def test_find_stitch_faces(modeler: Modeler): + """Test to read geometry and find it's stitch face problem areas.""" + skip_if_linux(modeler) # Skip test on Linux + modeler.open_file("./tests/integration/files/stitch_before.scdocx") + design = modeler.read_existing_design() + face_ids = [] + for body in design.bodies: + face_ids.append(body.id) + problem_areas = modeler.repair_tools.find_stitch_faces(face_ids) + assert len(problem_areas) == 1 + + +def test_find_stitch_face_id(modeler: Modeler): + """Test whether problem area has the id.""" + skip_if_linux(modeler) # Skip test on Linux + modeler.open_file("./tests/integration/files/stitch_before.scdocx") + design = modeler.read_existing_design() + face_ids = [] + for body in design.bodies: + face_ids.append(body.id) + problem_areas = modeler.repair_tools.find_stitch_faces(face_ids) + assert problem_areas[0].id > 0 + + +def test_find_stitch_face_faces(modeler: Modeler): + """Test to read geometry and find it's stitch face problem area and return the + connected faces.""" + skip_if_linux(modeler) # Skip test on Linux + modeler.open_file("./tests/integration/files/stitch_before.scdocx") + design = modeler.read_existing_design() + face_ids = [] + for body in design.bodies: + face_ids.append(body.id) + problem_areas = modeler.repair_tools.find_stitch_faces(face_ids) + assert len(problem_areas[0].faces) > 0 + + +def test_fix_stitch_face(modeler: Modeler): + """Test to read geometry, find the split edge problem areas and to fix them.""" + skip_if_linux(modeler) # Skip test on Linux + modeler.open_file("./tests/integration/files/stitch_before.scdocx") + design = modeler.read_existing_design() + face_ids = [] + for body in design.bodies: + face_ids.append(body.id) + problem_areas = modeler.repair_tools.find_stitch_faces(face_ids) + message = problem_areas[0].fix() + assert message.success == True + assert len(message.created_bodies) == 0 + assert len(message.modified_bodies) > 0 From a2dcf28dcadf3b4552990239312441ed694f8f1b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Oct 2023 09:26:46 +0200 Subject: [PATCH 47/74] MAINT: Bump the grpc-deps group with 1 update (#824) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b17e85d538..985e873821 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ tests = [ "ansys-tools-path==0.3.2", "beartype==0.16.4", "docker==6.1.3", - "google-api-python-client==2.104.0", + "google-api-python-client==2.105.0", "googleapis-common-protos==1.61.0", "grpcio==1.50.0", "grpcio-health-checking==1.48.2", From d5ddc6cd091c8370448e9c5f4c31db555cee4d04 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Oct 2023 13:04:49 +0000 Subject: [PATCH 48/74] MAINT: Bump the docs-deps group with 2 updates (#828) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 985e873821..3a6a250eb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,7 +68,7 @@ tests = [ "vtk==9.2.6", ] doc = [ - "ansys-sphinx-theme==0.12.3", + "ansys-sphinx-theme==0.12.4", "docker==6.1.3", "ipyvtklink==0.2.3", "jupyter_sphinx==0.4.0", @@ -78,7 +78,7 @@ doc = [ "nbsphinx==0.9.3", "notebook==7.0.6", "numpydoc==1.6.0", - "panel==1.2.3", + "panel==1.3.0", "pyvista[trame]==0.41.1", "requests==2.31.0", "sphinx==7.2.5", From f5bc63e37d4bd4cfa8c3e60382fe157d8d4b6aba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Oct 2023 09:49:51 +0200 Subject: [PATCH 49/74] MAINT: Bump pytest from 7.4.2 to 7.4.3 (#830) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3a6a250eb7..bd2e39f99c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ tests = [ "numpy==1.26.1", "Pint==0.22", "protobuf==3.20.3", - "pytest==7.4.2", + "pytest==7.4.3", "pytest-cov==4.1.0", "pytest-pyvista==0.1.9", "pytest-xvfb==3.0.0", From 77595f206138819a9b08340fc68c387d0a4a394e Mon Sep 17 00:00:00 2001 From: Matteo Bini <91963243+b-matteo@users.noreply.github.com> Date: Fri, 27 Oct 2023 09:52:19 +0200 Subject: [PATCH 50/74] Add logs folder as variable to the launcher (#831) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../geometry/core/connection/launcher.py | 12 ++++++++++ .../core/connection/product_instance.py | 24 +++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/ansys/geometry/core/connection/launcher.py b/src/ansys/geometry/core/connection/launcher.py index f682e151f6..4e1c8c332c 100644 --- a/src/ansys/geometry/core/connection/launcher.py +++ b/src/ansys/geometry/core/connection/launcher.py @@ -282,6 +282,7 @@ def launch_modeler_with_geometry_service( enable_trace: bool = False, log_level: int = 2, timeout: int = 60, + logs_folder: str = None, ) -> "Modeler": """ Start the Geometry service locally using the ``ProductInstance`` class. @@ -313,6 +314,8 @@ def launch_modeler_with_geometry_service( The default is ``2`` (Warning). timeout : int, optional Timeout for starting the backend startup process. The default is 60. + logs_folder : sets the backend's logs folder path. If nothing is defined, + the backend will use its default path. Raises ------ @@ -354,6 +357,7 @@ def launch_modeler_with_geometry_service( log_level=log_level, api_version=ApiVersions.LATEST, timeout=timeout, + logs_folder=logs_folder, ) @@ -364,6 +368,7 @@ def launch_modeler_with_discovery( log_level: int = 2, api_version: ApiVersions = ApiVersions.LATEST, timeout: int = 150, + logs_folder: str = None, ): """ Start Ansys Discovery locally using the ``ProductInstance`` class. @@ -408,6 +413,8 @@ def launch_modeler_with_discovery( the latest. Default is ``ApiVersions.LATEST``. timeout : int, optional Timeout for starting the backend startup process. The default is 150. + logs_folder : sets the backend's logs folder path. If nothing is defined, + the backend will use its default path. Raises ------ @@ -451,6 +458,7 @@ def launch_modeler_with_discovery( log_level=log_level, api_version=api_version, timeout=timeout, + logs_folder=logs_folder, ) @@ -461,6 +469,7 @@ def launch_modeler_with_spaceclaim( log_level: int = 2, api_version: ApiVersions = ApiVersions.LATEST, timeout: int = 150, + logs_folder: str = None, ): """ Start Ansys SpaceClaim locally using the ``ProductInstance`` class. @@ -502,6 +511,8 @@ def launch_modeler_with_spaceclaim( the latest. Default is ``ApiVersions.LATEST``. timeout : int, optional Timeout for starting the backend startup process. The default is 150. + logs_folder : sets the backend's logs folder path. If nothing is defined, + the backend will use its default path. Raises ------ @@ -545,6 +556,7 @@ def launch_modeler_with_spaceclaim( log_level=log_level, api_version=api_version, timeout=timeout, + logs_folder=logs_folder, ) diff --git a/src/ansys/geometry/core/connection/product_instance.py b/src/ansys/geometry/core/connection/product_instance.py index 24fdadaf0a..5aa16d947e 100644 --- a/src/ansys/geometry/core/connection/product_instance.py +++ b/src/ansys/geometry/core/connection/product_instance.py @@ -78,6 +78,9 @@ BACKEND_PORT_VARIABLE = "API_PORT" """The backend's port number environment variable for local start.""" +BACKEND_LOGS_FOLDER_VARIABLE = "ANS_DSCO_REMOTE_LOGS_FOLDER" +"""The backend's logs folder path to be used.""" + BACKEND_API_VERSION_VARIABLE = "API_VERSION" """ The backend's api version environment variable for local start. @@ -138,6 +141,7 @@ def prepare_and_start_backend( log_level: int = 2, api_version: ApiVersions = ApiVersions.LATEST, timeout: int = 150, + logs_folder: str = None, ) -> "Modeler": """ Start the requested service locally using the ``ProductInstance`` class. @@ -176,6 +180,8 @@ def prepare_and_start_backend( the latest. Default is ``ApiVersions.LATEST``. timeout : int, optional Timeout for starting the backend startup process. The default is 150. + logs_folder : sets the backend's logs folder path. If nothing is defined, + the backend will use its default path. Raises ------ @@ -202,7 +208,13 @@ def prepare_and_start_backend( _check_minimal_versions(product_version) args = [] - env_copy = _get_common_env(host=host, port=port, enable_trace=enable_trace, log_level=log_level) + env_copy = _get_common_env( + host=host, + port=port, + enable_trace=enable_trace, + log_level=log_level, + logs_folder=logs_folder, + ) if backend_type == BackendType.DISCOVERY: args.append(os.path.join(installations[product_version], DISCOVERY_FOLDER, DISCOVERY_EXE)) @@ -343,7 +355,13 @@ def _check_port_or_get_one(port: int) -> int: return get_available_port() -def _get_common_env(host: str, port: int, enable_trace: bool, log_level: int) -> Dict[str, str]: +def _get_common_env( + host: str, + port: int, + enable_trace: bool, + log_level: int, + logs_folder: str = None, +) -> Dict[str, str]: """ Make a copy of the actual system's environment. @@ -355,5 +373,7 @@ def _get_common_env(host: str, port: int, enable_trace: bool, log_level: int) -> env_copy[BACKEND_PORT_VARIABLE] = f"{port}" env_copy[BACKEND_TRACE_VARIABLE] = f"{enable_trace:d}" env_copy[BACKEND_LOG_LEVEL_VARIABLE] = f"{log_level}" + if logs_folder is not None: + env_copy[BACKEND_LOGS_FOLDER_VARIABLE] = f"{logs_folder}" return env_copy From e59bfd239215714e982d066a003fd12551eb680e Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Fri, 27 Oct 2023 13:01:32 +0200 Subject: [PATCH 51/74] feat: handling multiple designs (#819) Co-authored-by: Alex Fernandez <21alex295@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- src/ansys/geometry/core/__init__.py | 10 +++- src/ansys/geometry/core/connection/client.py | 15 +++++ src/ansys/geometry/core/designer/body.py | 55 ++++++++++++------ src/ansys/geometry/core/designer/component.py | 22 ++++++- src/ansys/geometry/core/designer/design.py | 40 ++++++++++++- .../geometry/core/designer/designpoint.py | 2 + src/ansys/geometry/core/designer/edge.py | 5 ++ src/ansys/geometry/core/designer/face.py | 6 ++ src/ansys/geometry/core/misc/checks.py | 57 +++++++++++++++++- src/ansys/geometry/core/modeler.py | 28 +++++---- .../geometry/core/plotting/plotter_helper.py | 4 +- tests/integration/test_design.py | 58 +++++++++++++++++++ 12 files changed, 266 insertions(+), 36 deletions(-) diff --git a/src/ansys/geometry/core/__init__.py b/src/ansys/geometry/core/__init__.py index 15fbd6e2bf..05d0b70151 100644 --- a/src/ansys/geometry/core/__init__.py +++ b/src/ansys/geometry/core/__init__.py @@ -49,6 +49,14 @@ # Global config constants # ------------------------------------------------------------------------------ -USE_TRAME = False +USE_TRAME: bool = False """Global constant for checking whether to use `trame `_ for visualization.""" + +DISABLE_MULTIPLE_DESIGN_CHECK: bool = False +""" +Global constant for disabling the ``ensure_design_is_active`` check. + +Only set this to false if you are sure you want to disable this check and you will ONLY +be working with one design. +""" diff --git a/src/ansys/geometry/core/connection/client.py b/src/ansys/geometry/core/connection/client.py index c6311cf99c..77df12b981 100644 --- a/src/ansys/geometry/core/connection/client.py +++ b/src/ansys/geometry/core/connection/client.py @@ -183,6 +183,9 @@ def __init__( # Store the backend type self._backend_type = backend_type + self._multiple_designs_allowed = ( + False if backend_type in (BackendType.DISCOVERY, BackendType.LINUX_SERVICE) else True + ) @property def backend_type(self) -> BackendType: @@ -199,6 +202,18 @@ def backend_type(self) -> BackendType: """ return self._backend_type + @property + def multiple_designs_allowed(self) -> bool: + """ + Flag indicating whether multiple designs are allowed. + + Notes + ----- + This method will return ``False`` if the backend type is ``Discovery`` or + ``Linux Service``. Otherwise, it will return ``True``. + """ + return self._multiple_designs_allowed + @property def channel(self) -> grpc.Channel: """Client gRPC channel.""" diff --git a/src/ansys/geometry/core/designer/body.py b/src/ansys/geometry/core/designer/body.py index 0ce5c3bb19..d38376a78f 100644 --- a/src/ansys/geometry/core/designer/body.py +++ b/src/ansys/geometry/core/designer/body.py @@ -56,7 +56,7 @@ from ansys.geometry.core.math.constants import IDENTITY_MATRIX44 from ansys.geometry.core.math.matrix import Matrix44 from ansys.geometry.core.math.vector import UnitVector3D -from ansys.geometry.core.misc.checks import check_type +from ansys.geometry.core.misc.checks import check_type, ensure_design_is_active from ansys.geometry.core.misc.measurements import DEFAULT_UNITS, Distance from ansys.geometry.core.sketch.sketch import Sketch from ansys.geometry.core.typing import Real @@ -799,11 +799,9 @@ def plot( use_trame: Optional[bool] = None, **plotting_options: Optional[dict], ) -> None: # noqa: D102 - # lazy import here to improve initial module load time - - from ansys.geometry.core.plotting import PlotterHelper - - PlotterHelper(use_trame=use_trame).plot(self, merge_bodies=merge) + raise NotImplementedError( + "MasterBody does not implement plot methods. Call this method on a body instead." + ) def intersect(self, other: "Body") -> None: # noqa: D102 raise NotImplementedError( @@ -846,17 +844,17 @@ class Body(IBody): Server-defined ID for the body. name : str User-defined label for the body. - parent : Component + parent_component : Component Parent component to place the new component under within the design assembly. template : MasterBody Master body that this body is an occurrence of. """ - def __init__(self, id, name, parent: "Component", template: MasterBody) -> None: + def __init__(self, id, name, parent_component: "Component", template: MasterBody) -> None: """Initialize the ``Body`` class.""" self._id = id self._name = name - self._parent = parent + self._parent_component = parent_component self._template = template def reset_tessellation_cache(func): @@ -890,11 +888,12 @@ def name(self) -> str: # noqa: D102 return self._template.name @property - def parent(self) -> "Component": # noqa: D102 - return self._parent + def parent_component(self) -> "Component": # noqa: D102 + return self._parent_component @property @protect_grpc + @ensure_design_is_active def faces(self) -> List[Face]: # noqa: D102 self._template._grpc_client.log.debug(f"Retrieving faces for body {self.id} from server.") grpc_faces = self._template._bodies_stub.GetFaces(EntityIdentifier(id=self.id)) @@ -910,6 +909,7 @@ def faces(self) -> List[Face]: # noqa: D102 @property @protect_grpc + @ensure_design_is_active def edges(self) -> List[Edge]: # noqa: D102 self._template._grpc_client.log.debug(f"Retrieving edges for body {self.id} from server.") grpc_edges = self._template._bodies_stub.GetEdges(EntityIdentifier(id=self.id)) @@ -959,18 +959,24 @@ def surface_offset(self) -> Union["MidSurfaceOffsetType", None]: # noqa: D102 return self._surface_offset @property + @ensure_design_is_active def volume(self) -> Quantity: # noqa: D102 return self._template.volume + @ensure_design_is_active def assign_material(self, material: Material) -> None: # noqa: D102 self._template.assign_material(material) + @ensure_design_is_active def add_midsurface_thickness(self, thickness: Quantity) -> None: # noqa: D102 self._template.add_midsurface_thickness(thickness) + @ensure_design_is_active def add_midsurface_offset(self, offset: "MidSurfaceOffsetType") -> None: # noqa: D102 self._template.add_midsurface_offset(offset) + @protect_grpc + @ensure_design_is_active def imprint_curves( self, faces: List[Face], sketch: Sketch ) -> Tuple[List[Edge], List[Face]]: # noqa: D102 @@ -1010,6 +1016,8 @@ def imprint_curves( return (new_edges, new_faces) + @protect_grpc + @ensure_design_is_active def project_curves( self, direction: UnitVector3D, @@ -1040,6 +1048,7 @@ def project_curves( @check_input_types @protect_grpc + @ensure_design_is_active def imprint_projected_curves( self, direction: UnitVector3D, @@ -1068,18 +1077,21 @@ def imprint_projected_curves( return imprinted_faces + @ensure_design_is_active def translate( self, direction: UnitVector3D, distance: Union[Quantity, Distance, Real] ) -> None: # noqa: D102 return self._template.translate(direction, distance) + @ensure_design_is_active def copy(self, parent: "Component", name: str = None) -> "Body": # noqa: D102 return self._template.copy(parent, name) + @ensure_design_is_active def tessellate( self, merge: Optional[bool] = False ) -> Union["PolyData", "MultiBlock"]: # noqa: D102 - return self._template.tessellate(merge, self.parent.get_world_transform()) + return self._template.tessellate(merge, self.parent_component.get_world_transform()) def plot( self, @@ -1088,10 +1100,17 @@ def plot( use_trame: Optional[bool] = None, **plotting_options: Optional[dict], ) -> None: # noqa: D102 - return self._template.plot(merge, screenshot, use_trame, **plotting_options) + # lazy import here to improve initial module load time + + from ansys.geometry.core.plotting import PlotterHelper + + PlotterHelper(use_trame=use_trame).plot( + self, merge_bodies=merge, screenshot=screenshot, **plotting_options + ) @protect_grpc @reset_tessellation_cache + @ensure_design_is_active def intersect(self, other: "Body") -> None: # noqa: D102 response = self._template._bodies_stub.Boolean( BooleanRequest(body1=self.id, body2=other.id, method="intersect") @@ -1100,10 +1119,11 @@ def intersect(self, other: "Body") -> None: # noqa: D102 if response == 1: raise ValueError("Bodies do not intersect.") - other.parent.delete_body(other) + other.parent_component.delete_body(other) @protect_grpc @reset_tessellation_cache + @ensure_design_is_active def subtract(self, other: "Body") -> None: # noqa: D102 response = self._template._bodies_stub.Boolean( BooleanRequest(body1=self.id, body2=other.id, method="subtract") @@ -1112,22 +1132,23 @@ def subtract(self, other: "Body") -> None: # noqa: D102 if response == 1: raise ValueError("Subtraction of bodies results in an empty (complete) subtraction.") - other.parent.delete_body(other) + other.parent_component.delete_body(other) @protect_grpc @reset_tessellation_cache + @ensure_design_is_active def unite(self, other: "Body") -> None: # noqa: D102 self._template._bodies_stub.Boolean( BooleanRequest(body1=self.id, body2=other.id, method="unite") ) - other.parent.delete_body(other) + other.parent_component.delete_body(other) def __repr__(self) -> str: """Represent the ``Body`` as a string.""" lines = [f"ansys.geometry.core.designer.Body {hex(id(self))}"] lines.append(f" Name : {self.name}") lines.append(f" Exists : {self.is_alive}") - lines.append(f" Parent component : {self._parent.name}") + lines.append(f" Parent component : {self._parent_component.name}") lines.append(f" MasterBody : {self._template.id}") lines.append(f" Surface body : {self.is_surface}") if self.is_surface: diff --git a/src/ansys/geometry/core/designer/component.py b/src/ansys/geometry/core/designer/component.py index 9652f1f99f..16980b9adc 100644 --- a/src/ansys/geometry/core/designer/component.py +++ b/src/ansys/geometry/core/designer/component.py @@ -66,7 +66,7 @@ from ansys.geometry.core.math.matrix import Matrix44 from ansys.geometry.core.math.point import Point3D from ansys.geometry.core.math.vector import UnitVector3D, Vector3D -from ansys.geometry.core.misc.checks import check_pint_unit_compatibility +from ansys.geometry.core.misc.checks import check_pint_unit_compatibility, ensure_design_is_active from ansys.geometry.core.misc.measurements import DEFAULT_UNITS, Angle, Distance from ansys.geometry.core.sketch.sketch import Sketch from ansys.geometry.core.typing import Real @@ -280,6 +280,7 @@ def get_world_transform(self) -> Matrix44: return self.parent_component.get_world_transform() * self._master_component.transform @protect_grpc + @ensure_design_is_active def modify_placement( self, translation: Optional[Vector3D] = None, @@ -339,6 +340,7 @@ def reset_placement(self): self.modify_placement() @check_input_types + @ensure_design_is_active def add_component(self, name: str, template: Optional["Component"] = None) -> "Component": """ Add a new component under this component within the design assembly. @@ -379,6 +381,7 @@ def add_component(self, name: str, template: Optional["Component"] = None) -> "C @protect_grpc @check_input_types + @ensure_design_is_active def set_shared_topology(self, share_type: SharedTopologyType) -> None: """ Set the shared topology to apply to the component. @@ -401,6 +404,7 @@ def set_shared_topology(self, share_type: SharedTopologyType) -> None: @protect_grpc @check_input_types + @ensure_design_is_active def extrude_sketch( self, name: str, sketch: Sketch, distance: Union[Quantity, Distance, Real] ) -> Body: @@ -445,6 +449,7 @@ def extrude_sketch( @protect_grpc @check_input_types + @ensure_design_is_active def extrude_face(self, name: str, face: Face, distance: Union[Quantity, Distance]) -> Body: """ Extrude the face profile by a given distance to create a solid body. @@ -492,6 +497,7 @@ def extrude_face(self, name: str, face: Face, distance: Union[Quantity, Distance @protect_grpc @check_input_types + @ensure_design_is_active def create_surface(self, name: str, sketch: Sketch) -> Body: """ Create a surface body with a sketch profile. @@ -529,6 +535,7 @@ def create_surface(self, name: str, sketch: Sketch) -> Body: @protect_grpc @check_input_types + @ensure_design_is_active def create_surface_from_face(self, name: str, face: Face) -> Body: """ Create a surface body based on a face. @@ -568,6 +575,7 @@ def create_surface_from_face(self, name: str, face: Face) -> Body: return Body(response.id, response.name, self, tb) @check_input_types + @ensure_design_is_active def create_coordinate_system(self, name: str, frame: Frame) -> CoordinateSystem: """ Create a coordinate system. @@ -591,6 +599,7 @@ def create_coordinate_system(self, name: str, frame: Frame) -> CoordinateSystem: @protect_grpc @check_input_types + @ensure_design_is_active def translate_bodies( self, bodies: List[Body], direction: UnitVector3D, distance: Union[Quantity, Distance, Real] ) -> None: @@ -643,6 +652,7 @@ def translate_bodies( @protect_grpc @check_input_types + @ensure_design_is_active def create_beams( self, segments: List[Tuple[Point3D, Point3D]], profile: BeamProfile ) -> List[Beam]: @@ -705,6 +715,7 @@ def create_beam(self, start: Point3D, end: Point3D, profile: BeamProfile) -> Bea @protect_grpc @check_input_types + @ensure_design_is_active def delete_component(self, component: Union["Component", str]) -> None: """ Delete a component (itself or its children). @@ -740,6 +751,7 @@ def delete_component(self, component: Union["Component", str]) -> None: @protect_grpc @check_input_types + @ensure_design_is_active def delete_body(self, body: Union[Body, str]) -> None: """ Delete a body belonging to this component (or its children). @@ -792,6 +804,7 @@ def add_design_point( @protect_grpc @check_input_types + @ensure_design_is_active def add_design_points( self, name: str, @@ -828,6 +841,7 @@ def add_design_points( @protect_grpc @check_input_types + @ensure_design_is_active def delete_beam(self, beam: Union[Beam, str]) -> None: """ Delete an existing beam belonging to this component (or its children). @@ -1119,7 +1133,11 @@ def plot( from ansys.geometry.core.plotting import PlotterHelper PlotterHelper(use_trame=use_trame).plot( - self, merge_bodies=merge_bodies, merge_component=merge_component, **plotting_options + self, + merge_bodies=merge_bodies, + merge_component=merge_component, + screenshot=screenshot, + **plotting_options, ) def __repr__(self) -> str: diff --git a/src/ansys/geometry/core/designer/design.py b/src/ansys/geometry/core/designer/design.py index 21dbf02702..c04683108f 100644 --- a/src/ansys/geometry/core/designer/design.py +++ b/src/ansys/geometry/core/designer/design.py @@ -46,7 +46,6 @@ import numpy as np from pint import Quantity -from ansys.geometry.core.connection.client import GrpcClient from ansys.geometry.core.connection.conversions import ( grpc_frame_to_frame, grpc_matrix_to_matrix, @@ -69,7 +68,9 @@ from ansys.geometry.core.math.plane import Plane from ansys.geometry.core.math.point import Point3D from ansys.geometry.core.math.vector import UnitVector3D, Vector3D +from ansys.geometry.core.misc.checks import ensure_design_is_active from ansys.geometry.core.misc.measurements import DEFAULT_UNITS, Distance +from ansys.geometry.core.modeler import Modeler from ansys.geometry.core.typing import RealSequence @@ -112,9 +113,9 @@ class Design(Component): @protect_grpc @check_input_types - def __init__(self, name: str, grpc_client: GrpcClient, read_existing_design: bool = False): + def __init__(self, name: str, modeler: Modeler, read_existing_design: bool = False): """Initialize the ``Design`` class.""" - super().__init__(name, None, grpc_client) + super().__init__(name, None, modeler.client) # Initialize the stubs needed self._design_stub = DesignsStub(self._grpc_client.channel) @@ -128,6 +129,8 @@ def __init__(self, name: str, grpc_client: GrpcClient, read_existing_design: boo self._named_selections = {} self._beam_profiles = {} self._design_id = "" + self._is_active = False + self._modeler = modeler # Check whether we want to process an existing design or create a new one. if read_existing_design: @@ -137,6 +140,7 @@ def __init__(self, name: str, grpc_client: GrpcClient, read_existing_design: boo new_design = self._design_stub.New(NewRequest(name=name)) self._design_id = new_design.id self._id = new_design.main_part.id + self._activate(called_after_design_creation=True) self._grpc_client.log.debug("Design object instantiated successfully.") @property @@ -159,9 +163,28 @@ def beam_profiles(self) -> List[BeamProfile]: """List of beam profile available for the design.""" return list(self._beam_profiles.values()) + @property + def is_active(self) -> bool: + """Whether the design is currently active.""" + return self._is_active + + @protect_grpc + def _activate(self, called_after_design_creation: bool = False) -> None: + """Activate the design.""" + # Deactivate all designs first + for design in self._modeler._designs.values(): + design._is_active = False + + # Activate the current design + if not called_after_design_creation: + self._design_stub.PutActive(EntityIdentifier(id=self._design_id)) + self._is_active = True + self._grpc_client.log.debug(f"Design {self.name} is activated.") + # TODO: allow for list of materials @protect_grpc @check_input_types + @ensure_design_is_active def add_material(self, material: Material) -> None: """ Add a material to the design. @@ -194,6 +217,7 @@ def add_material(self, material: Material) -> None: @protect_grpc @check_input_types + @ensure_design_is_active def save(self, file_location: Union[Path, str]) -> None: """ Save a design to disk on the active Geometry server instance. @@ -212,6 +236,7 @@ def save(self, file_location: Union[Path, str]) -> None: @protect_grpc @check_input_types + @ensure_design_is_active def download( self, file_location: Union[Path, str], @@ -268,6 +293,7 @@ def download( ) @check_input_types + @ensure_design_is_active def create_named_selection( self, name: str, @@ -319,6 +345,7 @@ def create_named_selection( @protect_grpc @check_input_types + @ensure_design_is_active def delete_named_selection(self, named_selection: Union[NamedSelection, str]) -> None: """ Delete a named selection on the active Geometry server instance. @@ -350,6 +377,7 @@ def delete_named_selection(self, named_selection: Union[NamedSelection, str]) -> pass @check_input_types + @ensure_design_is_active def delete_component(self, component: Union["Component", str]) -> None: """ Delete a component (itself or its children). @@ -393,6 +421,7 @@ def set_shared_topology(self, share_type: SharedTopologyType) -> None: @protect_grpc @check_input_types + @ensure_design_is_active def add_beam_circular_profile( self, name: str, @@ -448,6 +477,7 @@ def add_beam_circular_profile( @protect_grpc @check_input_types + @ensure_design_is_active def add_midsurface_thickness(self, thickness: Quantity, bodies: List[Body]) -> None: """ Add a mid-surface thickness to a list of bodies. @@ -488,6 +518,7 @@ def add_midsurface_thickness(self, thickness: Quantity, bodies: List[Body]) -> N @protect_grpc @check_input_types + @ensure_design_is_active def add_midsurface_offset(self, offset_type: MidSurfaceOffsetType, bodies: List[Body]) -> None: """ Add a mid-surface offset type to a list of bodies. @@ -526,6 +557,7 @@ def add_midsurface_offset(self, offset_type: MidSurfaceOffsetType, bodies: List[ @protect_grpc @check_input_types + @ensure_design_is_active def delete_beam_profile(self, beam_profile: Union[BeamProfile, str]) -> None: """ Remove a beam profile on the active geometry server instance. @@ -555,6 +587,7 @@ def __repr__(self) -> str: alive_comps = [1 if comp.is_alive else 0 for comp in self.components] lines = [f"ansys.geometry.core.designer.Design {hex(id(self))}"] lines.append(f" Name : {self.name}") + lines.append(f" Is active? : {self._is_active}") lines.append(f" N Bodies : {sum(alive_bodies)}") lines.append(f" N Components : {sum(alive_comps)}") lines.append(f" N Coordinate Systems : {len(self.coordinate_systems)}") @@ -601,6 +634,7 @@ def __read_existing_design(self) -> None: else: self._design_id = design.id self._id = design.main_part.id + self._activate(called_after_design_creation=True) # Here we may take the design's name instead of the main part's name. # Since they're the same in the backend. self._name = design.name diff --git a/src/ansys/geometry/core/designer/designpoint.py b/src/ansys/geometry/core/designer/designpoint.py index a8d9e8d1b4..7506446a36 100644 --- a/src/ansys/geometry/core/designer/designpoint.py +++ b/src/ansys/geometry/core/designer/designpoint.py @@ -28,6 +28,8 @@ from ansys.geometry.core.misc.units import UNITS if TYPE_CHECKING: # pragma: no cover + import pyvista as pv + from ansys.geometry.core.designer.component import Component diff --git a/src/ansys/geometry/core/designer/edge.py b/src/ansys/geometry/core/designer/edge.py index ffbd8afb87..7d3906a848 100644 --- a/src/ansys/geometry/core/designer/edge.py +++ b/src/ansys/geometry/core/designer/edge.py @@ -31,6 +31,7 @@ from ansys.geometry.core.connection.client import GrpcClient from ansys.geometry.core.errors import protect_grpc from ansys.geometry.core.math.point import Point3D +from ansys.geometry.core.misc.checks import ensure_design_is_active from ansys.geometry.core.misc.measurements import DEFAULT_UNITS if TYPE_CHECKING: # pragma: no cover @@ -88,6 +89,7 @@ def _grpc_id(self) -> EntityIdentifier: @property @protect_grpc + @ensure_design_is_active def length(self) -> Quantity: """Calculated length of the edge.""" self._grpc_client.log.debug("Requesting edge length from server.") @@ -101,6 +103,7 @@ def curve_type(self) -> CurveType: @property @protect_grpc + @ensure_design_is_active def faces(self) -> List["Face"]: """Faces that contain the edge.""" from ansys.geometry.core.designer.face import Face, SurfaceType @@ -114,6 +117,7 @@ def faces(self) -> List["Face"]: @property @protect_grpc + @ensure_design_is_active def start_point(self) -> Point3D: """Edge start point.""" self._grpc_client.log.debug("Requesting edge points from server.") @@ -122,6 +126,7 @@ def start_point(self) -> Point3D: @property @protect_grpc + @ensure_design_is_active def end_point(self) -> Point3D: """Edge end point.""" self._grpc_client.log.debug("Requesting edge points from server.") diff --git a/src/ansys/geometry/core/designer/face.py b/src/ansys/geometry/core/designer/face.py index 64bd5a6014..273116f9c6 100644 --- a/src/ansys/geometry/core/designer/face.py +++ b/src/ansys/geometry/core/designer/face.py @@ -36,6 +36,7 @@ from ansys.geometry.core.errors import protect_grpc from ansys.geometry.core.math.point import Point3D from ansys.geometry.core.math.vector import UnitVector3D +from ansys.geometry.core.misc.checks import ensure_design_is_active from ansys.geometry.core.misc.measurements import DEFAULT_UNITS if TYPE_CHECKING: # pragma: no cover @@ -172,6 +173,7 @@ def body(self) -> "Body": @property @protect_grpc + @ensure_design_is_active def area(self) -> Quantity: """Calculated area of the face.""" self._grpc_client.log.debug("Requesting face area from server.") @@ -185,6 +187,7 @@ def surface_type(self) -> SurfaceType: @property @protect_grpc + @ensure_design_is_active def edges(self) -> List[Edge]: """List of all edges of the face.""" self._grpc_client.log.debug("Requesting face edges from server.") @@ -193,6 +196,7 @@ def edges(self) -> List[Edge]: @property @protect_grpc + @ensure_design_is_active def loops(self) -> List[FaceLoop]: """List of all loops of the face.""" self._grpc_client.log.debug("Requesting face loops from server.") @@ -228,6 +232,7 @@ def loops(self) -> List[FaceLoop]: return loops @protect_grpc + @ensure_design_is_active def face_normal(self, u: float = 0.5, v: float = 0.5) -> UnitVector3D: """ Get the normal direction to the face evaluated at certain UV coordinates. @@ -259,6 +264,7 @@ def face_normal(self, u: float = 0.5, v: float = 0.5) -> UnitVector3D: return UnitVector3D([response.x, response.y, response.z]) @protect_grpc + @ensure_design_is_active def face_point(self, u: float = 0.5, v: float = 0.5) -> Point3D: """ Get a point of the face evaluated at certain UV coordinates. diff --git a/src/ansys/geometry/core/misc/checks.py b/src/ansys/geometry/core/misc/checks.py index 463af8eb0f..ecc28bb9f3 100644 --- a/src/ansys/geometry/core/misc/checks.py +++ b/src/ansys/geometry/core/misc/checks.py @@ -20,10 +20,65 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Provides functions for performing common checks.""" -from beartype.typing import Any, Optional, Tuple, Union +from beartype.typing import TYPE_CHECKING, Any, Optional, Tuple, Union import numpy as np from pint import Unit +if TYPE_CHECKING: + from ansys.geometry.core.designer import Design + + +def ensure_design_is_active(method): + """ + Make sure that the design is active before executing a method. + + This function is necessary to be called whenever we do any operation on the design. + If we are just accessing information of the class, it is not necessary to call this. + """ + + def wrapper(self, *args, **kwargs): + import ansys.geometry.core as pyansys_geometry + + if pyansys_geometry.DISABLE_MULTIPLE_DESIGN_CHECK: + # If the user has disabled the check, then we can skip it + return method(self, *args, **kwargs) + + # Check if the current design is active... otherwise activate it + def get_design_ref(obj) -> "Design": + if hasattr(obj, "_modeler"): # In case of a Design object + return obj + elif hasattr( + obj, "_parent_component" + ): # In case of a Body, Component, DesignPoint, Beam + # Recursive call + return get_design_ref(obj._parent_component) + elif hasattr(obj, "_body"): # In case of a Face, Edge + # Recursive call + return get_design_ref(obj._body._parent_component) + else: + raise ValueError("Unable to find the design reference.") + + # Get the design reference + design = get_design_ref(self) + + # Activate the design if it is not active + if not design.is_active: + # First, check the backend allows for multiple documents + if not design._grpc_client.multiple_designs_allowed: + from ansys.geometry.core.errors import GeometryRuntimeError + + raise GeometryRuntimeError( + "The design is not active and multiple designs are " + "not allowed with the current backend." + ) + else: + design._activate() + + # Finally, call method + return method(self, *args, **kwargs) + + return wrapper + def check_is_float_int(param: object, param_name: Optional[Union[str, None]] = None) -> None: """ diff --git a/src/ansys/geometry/core/modeler.py b/src/ansys/geometry/core/modeler.py index 1bbdbc49a8..32264cdcbc 100644 --- a/src/ansys/geometry/core/modeler.py +++ b/src/ansys/geometry/core/modeler.py @@ -124,8 +124,16 @@ def __init__( else: self._repair_tools = RepairTools(self._client) - # Design[] maintaining references to all designs within the modeler workspace - self._designs = [] + # 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: + logger.warning( + "Linux and Ansys Discovery backends do not support multiple " + "designs open in the same session. Only the last design created " + "will be available to perform modeling operations." + ) @property def client(self) -> GrpcClient: @@ -149,14 +157,14 @@ def create_design(self, name: str) -> "Design": from ansys.geometry.core.designer.design import Design check_type(name, str) - design = Design(name, self._client) - self._designs.append(design) + design = Design(name, self) + self._designs[design.design_id] = design if len(self._designs) > 1: logger.warning( - "Most backends only support one design. " + "Some backends only support one design. " + "Previous designs may be deleted (on the service) when creating a new one." ) - return self._designs[-1] + return self._designs[design.design_id] def read_existing_design(self) -> "Design": """ @@ -169,14 +177,14 @@ def read_existing_design(self) -> "Design": """ from ansys.geometry.core.designer.design import Design - design = Design("", self._client, read_existing_design=True) - self._designs.append(design) + design = Design("", self, read_existing_design=True) + self._designs[design.design_id] = design if len(self._designs) > 1: logger.warning( - "Most backends only support one design. " + "Some backends only support one design. " + "Previous designs may be deleted (on the service) when reading a new one." ) - return self._designs[-1] + return self._designs[design.design_id] def close(self) -> None: """``Modeler`` method for easily accessing the client's close method.""" diff --git a/src/ansys/geometry/core/plotting/plotter_helper.py b/src/ansys/geometry/core/plotting/plotter_helper.py index 4fb2280b8e..1c4db8bf20 100644 --- a/src/ansys/geometry/core/plotting/plotter_helper.py +++ b/src/ansys/geometry/core/plotting/plotter_helper.py @@ -68,9 +68,9 @@ def __init__( """Initialize ``use_trame`` and save current ``pv.OFF_SCREEN`` value.""" # Check if the use of trame was requested if use_trame is None: - import ansys.geometry.core as pygeom + import ansys.geometry.core as pyansys_geometry - use_trame = pygeom.USE_TRAME + use_trame = pyansys_geometry.USE_TRAME self._use_trame = use_trame self._allow_picking = allow_picking diff --git a/tests/integration/test_design.py b/tests/integration/test_design.py index 0f9f8716dd..1a436d46cd 100644 --- a/tests/integration/test_design.py +++ b/tests/integration/test_design.py @@ -6,6 +6,7 @@ from pint import Quantity import pytest import pyvista as pv +from pyvista.plotting.utilities.regression import compare_images as pv_compare_images from ansys.geometry.core import Modeler from ansys.geometry.core.connection import BackendType @@ -1681,3 +1682,60 @@ def test_child_component_instances(modeler: Modeler): assert len(comp1.components) == 2 assert len(base2.components[0].components) == 2 assert len(comp1.components) == len(base2.components[0].components) + + +def test_multiple_designs(modeler: Modeler, tmp_path_factory: pytest.TempPathFactory): + """Generate multiple designs, make sure they are all separate, and activate them + when needed.""" + # Check backend first + if modeler.client.backend_type in ( + BackendType.SPACECLAIM, + BackendType.WINDOWS_SERVICE, + ): + pass + else: + # Test is only available for DMS and SpaceClaim + pytest.skip("Test only available on DMS and SpaceClaim") + + # Create your design on the server side + design1 = modeler.create_design("Design1") + + # Create a Sketch object and draw a slot + sketch1 = Sketch() + sketch1.slot(Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm), Quantity(5, UNITS.mm)) + + # Extrude the sketch to create a body + design1.extrude_sketch("MySlot", sketch1, Quantity(10, UNITS.mm)) + + # Create a second design + design2 = modeler.create_design("Design2") + + # Create a Sketch object and draw a rectangle + sketch2 = Sketch() + sketch2.box(Point2D([-30, -30], UNITS.mm), 5 * UNITS.mm, 8 * UNITS.mm) + + # Extrude the sketch to create a body + design2.extrude_sketch("MyRectangle", sketch2, Quantity(10, UNITS.mm)) + + # Initiate expected output images + scshot_dir = tmp_path_factory.mktemp("test_multiple_designs") + scshot_1 = scshot_dir / "design1.png" + scshot_2 = scshot_dir / "design2.png" + + # Request plotting and store images + design2.plot(screenshot=scshot_1) + design1.plot(screenshot=scshot_2) + + # Check that the images are different + assert scshot_1.exists() + assert scshot_2.exists() + err = pv_compare_images(str(scshot_1), str(scshot_2)) + assert not err < 0.1 + + # Check that design2 is not active + assert not design2.is_active + assert design1.is_active + + # Check the same thing inside the modeler + assert not modeler._designs[design2.design_id].is_active + assert modeler._designs[design1.design_id].is_active From 8802cd3ee24c7a46b374bd74aee1c8a958a11aef Mon Sep 17 00:00:00 2001 From: Matteo Bini <91963243+b-matteo@users.noreply.github.com> Date: Mon, 30 Oct 2023 08:33:09 +0100 Subject: [PATCH 52/74] feat: add manifest path product launcher (#815) Co-authored-by: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../geometry/core/connection/launcher.py | 12 ++++++++++ .../core/connection/product_instance.py | 24 +++++++++++++++---- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/ansys/geometry/core/connection/launcher.py b/src/ansys/geometry/core/connection/launcher.py index 4e1c8c332c..1e2c67ba69 100644 --- a/src/ansys/geometry/core/connection/launcher.py +++ b/src/ansys/geometry/core/connection/launcher.py @@ -368,6 +368,7 @@ def launch_modeler_with_discovery( log_level: int = 2, api_version: ApiVersions = ApiVersions.LATEST, timeout: int = 150, + manifest_path: str = None, logs_folder: str = None, ): """ @@ -413,6 +414,10 @@ def launch_modeler_with_discovery( the latest. Default is ``ApiVersions.LATEST``. timeout : int, optional Timeout for starting the backend startup process. The default is 150. + manifest_path : str, optional + Used to specify a manifest file path for the ApiServerAddin. This way, + it is possible to run an ApiServerAddin from a version an older product + version. logs_folder : sets the backend's logs folder path. If nothing is defined, the backend will use its default path. @@ -458,6 +463,7 @@ def launch_modeler_with_discovery( log_level=log_level, api_version=api_version, timeout=timeout, + manifest_path=manifest_path, logs_folder=logs_folder, ) @@ -469,6 +475,7 @@ def launch_modeler_with_spaceclaim( log_level: int = 2, api_version: ApiVersions = ApiVersions.LATEST, timeout: int = 150, + manifest_path: str = None, logs_folder: str = None, ): """ @@ -511,6 +518,10 @@ def launch_modeler_with_spaceclaim( the latest. Default is ``ApiVersions.LATEST``. timeout : int, optional Timeout for starting the backend startup process. The default is 150. + manifest_path : str, optional + Used to specify a manifest file path for the ApiServerAddin. This way, + it is possible to run an ApiServerAddin from a version an older product + version. logs_folder : sets the backend's logs folder path. If nothing is defined, the backend will use its default path. @@ -556,6 +567,7 @@ def launch_modeler_with_spaceclaim( log_level=log_level, api_version=api_version, timeout=timeout, + manifest_path=manifest_path, logs_folder=logs_folder, ) diff --git a/src/ansys/geometry/core/connection/product_instance.py b/src/ansys/geometry/core/connection/product_instance.py index 5aa16d947e..a447cc652f 100644 --- a/src/ansys/geometry/core/connection/product_instance.py +++ b/src/ansys/geometry/core/connection/product_instance.py @@ -35,7 +35,7 @@ from ansys.geometry.core.modeler import Modeler -WINDOWS_GEOMETRY_SERVICE_FOLDER = "GeometryServices" +WINDOWS_GEOMETRY_SERVICE_FOLDER = "GeometryService" """Default Geometry Service's folder name into the unified installer.""" DISCOVERY_FOLDER = "Discovery" @@ -141,6 +141,7 @@ def prepare_and_start_backend( log_level: int = 2, api_version: ApiVersions = ApiVersions.LATEST, timeout: int = 150, + manifest_path: str = None, logs_folder: str = None, ) -> "Modeler": """ @@ -180,6 +181,10 @@ def prepare_and_start_backend( the latest. Default is ``ApiVersions.LATEST``. timeout : int, optional Timeout for starting the backend startup process. The default is 150. + manifest_path : str, optional + Used to specify a manifest file path for the ApiServerAddin. This way, + it is possible to run an ApiServerAddin from a version an older product + version. Only applicable for Ansys Discovery and Ansys SpaceClaim. logs_folder : sets the backend's logs folder path. If nothing is defined, the backend will use its default path. @@ -221,14 +226,14 @@ def prepare_and_start_backend( args.append(BACKEND_SPACECLAIM_OPTIONS) args.append( BACKEND_ADDIN_MANIFEST_ARGUMENT - + _manifest_path_provider(product_version, installations) + + _manifest_path_provider(product_version, installations, manifest_path) ) env_copy[BACKEND_API_VERSION_VARIABLE] = str(api_version) elif backend_type == BackendType.SPACECLAIM: args.append(os.path.join(installations[product_version], SPACECLAIM_FOLDER, SPACECLAIM_EXE)) args.append( BACKEND_ADDIN_MANIFEST_ARGUMENT - + _manifest_path_provider(product_version, installations) + + _manifest_path_provider(product_version, installations, manifest_path) ) env_copy[BACKEND_API_VERSION_VARIABLE] = str(api_version) elif backend_type == BackendType.WINDOWS_SERVICE: @@ -279,8 +284,19 @@ def _is_port_available(port: int, host: str = "localhost") -> bool: return False -def _manifest_path_provider(version: int, available_installations: Dict) -> str: +def _manifest_path_provider( + version: int, available_installations: Dict, manifest_path: str = None +) -> str: """Return the ApiServer's addin manifest file path.""" + if manifest_path: + if os.path.exists(manifest_path): + return manifest_path + else: + LOG.warning( + "Specified manifest file's path does not exist. Taking install default path." + ) + + # Default manifest path return os.path.join( available_installations[version], ADDINS_SUBFOLDER, BACKEND_SUBFOLDER, MANIFEST_FILENAME ) From 93e18bb541dfd6e52596d83c4b93d57bb9fa783a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Oct 2023 07:52:13 +0100 Subject: [PATCH 53/74] MAINT: Bump the docs-deps group with 1 update (#832) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bd2e39f99c..995c7ab529 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,7 +74,7 @@ doc = [ "jupyter_sphinx==0.4.0", "jupytext==1.15.2", "myst-parser==2.0.0", - "nbconvert==7.9.2", + "nbconvert==7.10.0", "nbsphinx==0.9.3", "notebook==7.0.6", "numpydoc==1.6.0", From e9fa2e7a46282b1460358e94d9cd20c61d5dc1de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Oct 2023 17:33:56 +0000 Subject: [PATCH 54/74] MAINT: Bump the grpc-deps group with 1 update (#833) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 995c7ab529..a3889f7d56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ tests = [ "ansys-tools-path==0.3.2", "beartype==0.16.4", "docker==6.1.3", - "google-api-python-client==2.105.0", + "google-api-python-client==2.106.0", "googleapis-common-protos==1.61.0", "grpcio==1.50.0", "grpcio-health-checking==1.48.2", From 29cc9e5fb475474c665f2caa2fc79a1a9ef9aa63 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Nov 2023 07:55:45 +0100 Subject: [PATCH 55/74] MAINT: Bump the docs-deps group with 1 update (#836) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a3889f7d56..70f05aad43 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,7 +78,7 @@ doc = [ "nbsphinx==0.9.3", "notebook==7.0.6", "numpydoc==1.6.0", - "panel==1.3.0", + "panel==1.3.1", "pyvista[trame]==0.41.1", "requests==2.31.0", "sphinx==7.2.5", From ea75b26d95f1c49cd351f37758dcf8c87cf0a965 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Nov 2023 08:40:49 +0000 Subject: [PATCH 56/74] MAINT: Bump ansys-api-geometry from 0.3.2 to 0.3.3 (#837) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 70f05aad43..4323a928e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ ] dependencies = [ - "ansys-api-geometry==0.3.2", + "ansys-api-geometry==0.3.3", "ansys-tools-path>=0.3", "beartype>=0.11.0", "google-api-python-client>=1.7.11", From b7f52aa5b1fb2bd6207161efc57906eeea008e17 Mon Sep 17 00:00:00 2001 From: Alex Fernandez <21alex295@gmail.com> Date: Fri, 3 Nov 2023 10:37:43 +0100 Subject: [PATCH 57/74] maint: Reactivate style action (#839) --- .github/workflows/ci_cd.yml | 11 ++++------- doc/.vale.ini | 1 + doc/source/assets.rst | 2 +- doc/source/getting_started/faq.rst | 2 +- doc/styles/Vocab/ANSYS/accept.txt | 25 +++++++++---------------- 5 files changed, 16 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 45c44d7c04..2af056fae5 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -35,13 +35,10 @@ jobs: name: Documentation Style Check runs-on: ubuntu-latest steps: - # TODO - Fix codestyle issues - # - name: PyAnsys documentation style checks - # uses: ansys/actions/doc-style@v4 - # with: - # token: ${{ secrets.GITHUB_TOKEN }} - - name : TODO - Reactivate code style - run : sleep 1 + - name: PyAnsys documentation style checks + uses: ansys/actions/doc-style@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} smoke-tests: name: Build and Smoke tests diff --git a/doc/.vale.ini b/doc/.vale.ini index 4f0b670983..f2adf5276e 100644 --- a/doc/.vale.ini +++ b/doc/.vale.ini @@ -26,3 +26,4 @@ Vocab = ANSYS # Apply the following styles BasedOnStyles = Vale, Google +Google.Headings = NO \ No newline at end of file diff --git a/doc/source/assets.rst b/doc/source/assets.rst index 74324d8d0e..bc7d045f10 100644 --- a/doc/source/assets.rst +++ b/doc/source/assets.rst @@ -6,7 +6,7 @@ In this section, users are able to download a set of assets related to PyAnsys G Documentation ------------- -The following links will provide users with downloadable documentation in various formats +The following links provide users with downloadable documentation in various formats * `Documentation in HTML format <_static/assets/download/documentation-html.zip>`_ * `Documentation in PDF format <_static/assets/download/ansys-geometry-core.pdf>`_ diff --git a/doc/source/getting_started/faq.rst b/doc/source/getting_started/faq.rst index 4a834c3ae9..35687ac79a 100644 --- a/doc/source/getting_started/faq.rst +++ b/doc/source/getting_started/faq.rst @@ -12,7 +12,7 @@ Design Language (APDL), Ansys Fluent, and other Ansys products. You can use PyAnsys libraries within a Python environment of your choice in conjunction with external Python libraries. -What Ansys license do I need to run the Geometry service? +What Ansys license is needed to run the Geometry service? --------------------------------------------------------- .. note:: diff --git a/doc/styles/Vocab/ANSYS/accept.txt b/doc/styles/Vocab/ANSYS/accept.txt index 9b24bc51bb..eda53231fc 100644 --- a/doc/styles/Vocab/ANSYS/accept.txt +++ b/doc/styles/Vocab/ANSYS/accept.txt @@ -1,31 +1,24 @@ -ANSYS -Ansys -ansys -API -api +(?i)ansys +(?i)api +(?i)check Direct API Dockerfile +(?i)docker dockerized Fluent API -Geometry -geometry +(?i)geometry Geometry service Geometry models -github -GitHub +(?i)github GitHub Container Registry namespace Polydata PROTO Protobuf -PyAnsys -pyansys -pyansys-geometry -PyAnsys Geometry +(?i)pyansys +(?i)pyansys-geometry PyPI PyVista SciPy subpackage -launch_modeler_with_discovery -launch_modeler_with_spaceclaim -launch_modeler_with_geometry_service \ No newline at end of file +launch_modeler_with_.* \ No newline at end of file From d914d9c21e264a54abed82a5e3336b0068ee8bb4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Nov 2023 20:16:36 +0100 Subject: [PATCH 58/74] MAINT: Bump the docs-deps group with 1 update (#840) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4323a928e4..87f57601da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,7 +74,7 @@ doc = [ "jupyter_sphinx==0.4.0", "jupytext==1.15.2", "myst-parser==2.0.0", - "nbconvert==7.10.0", + "nbconvert==7.11.0", "nbsphinx==0.9.3", "notebook==7.0.6", "numpydoc==1.6.0", From 090b0c7a7c34862165943b68a937bdfbce4538aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Nov 2023 10:04:41 +0100 Subject: [PATCH 59/74] MAINT: Bump the grpc-deps group with 1 update (#844) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 87f57601da..4c653b6d94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ tests = [ "ansys-tools-path==0.3.2", "beartype==0.16.4", "docker==6.1.3", - "google-api-python-client==2.106.0", + "google-api-python-client==2.107.0", "googleapis-common-protos==1.61.0", "grpcio==1.50.0", "grpcio-health-checking==1.48.2", From 8f52ffa62eb9ca452d72b7a28dc866974a0f155f Mon Sep 17 00:00:00 2001 From: Alex Fernandez <21alex295@gmail.com> Date: Thu, 9 Nov 2023 18:06:12 +0100 Subject: [PATCH 60/74] fix: Revert dependencies due to broken plotters (#843) --- src/ansys/geometry/core/plotting/plotter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ansys/geometry/core/plotting/plotter.py b/src/ansys/geometry/core/plotting/plotter.py index 6500bc77b8..cd6c69a011 100644 --- a/src/ansys/geometry/core/plotting/plotter.py +++ b/src/ansys/geometry/core/plotting/plotter.py @@ -509,7 +509,8 @@ def show( # Conditionally set the Jupyter backend as not all users will be within # a notebook environment to avoid a pyvista warning if self.scene.notebook and jupyter_backend is None: - jupyter_backend = "panel" + # TODO revert this once the dynamic plotters in documentation are back. + jupyter_backend = "static" # Enabling anti-aliasing by default on scene self.scene.enable_anti_aliasing("ssaa") From 17fea5a61d195cece293846b88d8d5295b0e4d0d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 08:27:16 +0100 Subject: [PATCH 61/74] MAINT: Bump the docs-deps group with 1 update (#847) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4c653b6d94..f681521319 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,7 +68,7 @@ tests = [ "vtk==9.2.6", ] doc = [ - "ansys-sphinx-theme==0.12.4", + "ansys-sphinx-theme==0.12.5", "docker==6.1.3", "ipyvtklink==0.2.3", "jupyter_sphinx==0.4.0", From cb2ffcb240a950426377d405854e0ae53735e57c Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Mon, 13 Nov 2023 12:02:42 -0500 Subject: [PATCH 62/74] fix: retain version docker images (#850) --- .github/workflows/docker_cleanup.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker_cleanup.yml b/.github/workflows/docker_cleanup.yml index 6bbea91620..67e5b28858 100644 --- a/.github/workflows/docker_cleanup.yml +++ b/.github/workflows/docker_cleanup.yml @@ -26,4 +26,4 @@ jobs: with: package-name: 'geometry' token: ${{ secrets.GITHUB_TOKEN }} - tags-kept: 'windows-latest, windows-latest-unstable, linux-latest, linux-latest-unstable' + tags-kept: 'windows-latest, windows-latest-unstable, linux-latest, linux-latest-unstable, 24.1, 24.2' From 893ebaa0967da469313479232fdeea7eb7a35a62 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 19:38:33 +0100 Subject: [PATCH 63/74] MAINT: Bump numpy from 1.26.1 to 1.26.2 (#851) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f681521319..b31ab20339 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,7 @@ tests = [ "googleapis-common-protos==1.61.0", "grpcio==1.50.0", "grpcio-health-checking==1.48.2", - "numpy==1.26.1", + "numpy==1.26.2", "Pint==0.22", "protobuf==3.20.3", "pytest==7.4.3", From 1a96d10e0ad38d5e4b8eb5a42c69d0db950e8cd4 Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Tue, 14 Nov 2023 09:55:59 +0100 Subject: [PATCH 64/74] feat: simplifying README (#852) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- README.rst | 227 ++++++++++++----------------------------------------- 1 file changed, 51 insertions(+), 176 deletions(-) diff --git a/README.rst b/README.rst index ce87b1b3f5..11c3055905 100644 --- a/README.rst +++ b/README.rst @@ -34,211 +34,86 @@ PyAnsys Geometry :target: https://results.pre-commit.ci/latest/github/ansys/pyansys-geometry/main :alt: pre-commit.ci -PyAnsys Geometry is a Python client library for the Ansys Geometry service. - .. contents:: -Usage ------ - -There are different ways of getting started with using the Geometry service and its client library, PyAnsys Geometry. +Overview +-------- -For more information, see -`Getting Started `_ documentation. +PyAnsys Geometry is a Python client library for the Ansys Geometry service, as well as other CAD Ansys products +such as Ansys Discovery and Ansys SpaceClaim. Installation ------------- - -PyAnsys Geometry has three installation modes: user, developer, and offline. - -Install in user mode -^^^^^^^^^^^^^^^^^^^^ - -Before installing PyAnsys Geometry in user mode, make sure you have the latest version of -`pip`_ with: +^^^^^^^^^^^^ +You can use `pip `_ to install PyAnsys Geometry. .. code:: bash - python -m pip install -U pip + pip install ansys-geometry-core -Then, install PyAnsys Geometry with: +To install the latest development version, run these commands: .. code:: bash - python -m pip install ansys-geometry-core - - -Install in developer mode -^^^^^^^^^^^^^^^^^^^^^^^^^ - -Installing PyAnsys Geometry in developer mode allows -you to modify the source and enhance it. - -.. note:: - - Before contributing to the project, ensure that you are thoroughly familiar - with the `PyAnsys Developer's Guide`_. - -To install PyAnsys Geometry in developer mode, perform these steps: - -#. Clone the ``pyansys-geometry`` repository: - - .. code:: bash - - git clone https://github.com/ansys/pyansys-geometry - -#. Access the ``pyansys-geometry`` directory where the repository has been cloned: - - .. code:: bash - - cd pyansys-geometry - -#. Create a clean Python virtual environment and activate it: - - .. code:: bash - - # Create a virtual environment - python -m venv .venv - - # Activate it in a POSIX system - source .venv/bin/activate - - # Activate it in Windows CMD environment - .venv\Scripts\activate.bat - - # Activate it in Windows Powershell - .venv\Scripts\Activate.ps1 - -#. Make sure you have the latest required build system tools: - - .. code:: bash - - python -m pip install -U pip tox - -#. Install the project in editable mode: - - .. code:: bash - - # Install the minimum requirements - python -m pip install -e . + git clone https://github.com/ansys/pyansys-geometry + cd pyansys-geometry + pip install -e . - # Install the minimum + tests requirements - python -m pip install -e .[tests] +For more information, see `Getting Started`_. - # Install the minimum + doc requirements - python -m pip install -e .[doc] - - # Install all requirements - python -m pip install -e .[tests,doc] - -Install in offline mode -^^^^^^^^^^^^^^^^^^^^^^^ - -If you lack an internet connection on your installation machine, you should install PyAnsys Geometry -by downloading the wheelhouse archive from the `Releases `_ -page for your corresponding machine architecture. - -Each wheelhouse archive contains all the Python wheels necessary to install PyAnsys Geometry from scratch on Windows, -Linux, and MacOS from Python 3.9 to 3.11. You can install this on an isolated system with a fresh Python -installation or on a virtual environment. - -For example, on Linux with Python 3.9, unzip the wheelhouse archive and install it with: - -.. code:: bash - - unzip ansys-geometry-core-v0.4.dev0-wheelhouse-ubuntu-latest-3.9.zip wheelhouse - pip install ansys-geometry-core -f wheelhouse --no-index --upgrade --ignore-installed - -If you're on Windows with Python 3.9, unzip to a wheelhouse directory and install using the preceding command. - -Consider installing using a `virtual environment `_. - -Testing -------- - -This project takes advantage of `tox`_. This tool automate common -development tasks (similar to Makefile), but it is oriented towards Python -development. - -Using ``tox`` -^^^^^^^^^^^^^ - -While Makefile has rules, `tox`_ has environments. In fact, ``tox`` creates its -own virtual environment so that anything being tested is isolated from the project -to guarantee the project's integrity. - -The following environments commands are provided: - -- **tox -e style**: Checks for coding style quality. -- **tox -e py**: Checks for unit tests. -- **tox -e py-coverage**: Checks for unit testing and code coverage. -- **tox -e doc**: Checks for documentation building process. - - .. admonition:: pyvista-pytest plugin - - This plugin facilitates the comparison of the images produced in PyAnsys Geometry for testing the plots. - If you are changing the images, use flag ``--reset_image_cache`` which is not recommended except - for testing or for potentially a major or minor release. For more information, see `pyvista-pytest`_. - -Raw testing +Basic usage ^^^^^^^^^^^ -If required, from the command line, you can call style commands, including -`black`_, `isort`_, and `flake8`_, and unit testing commands like `pytest`_. -However, this does not guarantee that your project is being tested in an isolated -environment, which is the reason why tools like `tox`_ exist. - - -Using ``pre-commit`` -^^^^^^^^^^^^^^^^^^^^ +This code shows how to import PyAnsys Geometry and use some basic capabilities: -The style checks take advantage of `pre-commit`_. Developers are not forced but -encouraged to install this tool with: +.. code:: python -.. code:: bash - - python -m pip install pre-commit && pre-commit install + from ansys.geometry.core import launch_modeler + from ansys.geometry.core.math import Plane, Point3D, Point2D + from ansys.geometry.core.misc import UNITS, Distance + from ansys.geometry.core.sketch import Sketch + # Define a sketch + origin = Point3D([0, 0, 10]) + plane = Plane(origin, direction_x=[1, 0, 0], direction_y=[0, 1, 0]) -Documentation -------------- + # Create a sketch + sketch = Sketch(plane) + sketch.circle(Point2D([1, 1]), 30 * UNITS.m) + sketch.plot() -For building documentation, you can run the usual rules provided in the -`Sphinx`_ Makefile, such as: + # Start a modeler session + modeler = launch_modeler() -.. code:: bash + # Create a design + design = modeler.create_design("ModelingDemo") - make -C doc/ html && your_browser_name doc/html/index.html + # Create a body directly on the design by extruding the sketch + body = design.extrude_sketch( + name="CylinderBody", sketch=sketch, distance=Distance(80, unit=UNITS.m) + ) -However, the recommended way of checking documentation integrity is to use -``tox``: + # Plot the body + design.plot() -.. code:: bash +For comprehensive usage information, see `Examples`_ in the `PyAnsys Geometry documentation`_. - tox -e doc && your_browser_name .tox/doc_out/index.html +Documentation and issues +^^^^^^^^^^^^^^^^^^^^^^^^ +Documentation for the latest stable release of PyAnsys Geometry is hosted at `PyAnsys Geometry documentation`_. +In the upper right corner of the documentation's title bar, there is an option for switching from +viewing the documentation for the latest stable release to viewing the documentation for the +development version or previously released versions. -Distributing ------------- - -If you would like to create either source or wheel files, start by installing -the building requirements and then executing the build module: - -.. code:: bash +On the `PyAnsys Geometry Issues `_ page, +you can create issues to report bugs and request new features. On the `PyAnsys Geometry Discussions +`_ page or the `Discussions `_ +page on the Ansys Developer portal, you can post questions, share ideas, and get community feedback. - python -m pip install -U pip - python -m build - python -m twine check dist/* +To reach the project support team, email `pyansys.core@ansys.com `_. .. LINKS AND REFERENCES -.. _black: https://github.com/psf/black -.. _flake8: https://flake8.pycqa.org/en/latest/ -.. _isort: https://github.com/PyCQA/isort -.. _pip: https://pypi.org/project/pip/ -.. _pre-commit: https://pre-commit.com/ -.. _PyAnsys Developer's Guide: https://dev.docs.pyansys.com/ -.. _pytest: https://docs.pytest.org/en/stable/ -.. _Sphinx: https://www.sphinx-doc.org/en/master/ -.. _tox: https://tox.wiki/ -.. _pyvista-pytest: https://github.com/pyvista/pytest-pyvista +.. _Getting Started: https://geometry.docs.pyansys.com/version/stable/getting_started/index.html +.. _Examples: https://geometry.docs.pyansys.com/version/stable/examples.html +.. _PyAnsys Geometry documentation: https://geometry.docs.pyansys.com/version/stable/index.html From 8bd754a9f9e4a24b5a236a3df18776bd3caefb2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Nov 2023 08:16:30 +0100 Subject: [PATCH 65/74] MAINT: Bump vtk from 9.2.6 to 9.3.0 (#854) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b31ab20339..49f1d84a67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ tests = [ "requests==2.31.0", "scipy==1.11.3", "six==1.16.0", - "vtk==9.2.6", + "vtk==9.3.0", ] doc = [ "ansys-sphinx-theme==0.12.5", @@ -87,7 +87,7 @@ doc = [ "sphinx-copybutton==0.5.2", "sphinx_design==0.5.0", "sphinx-jinja==2.0.2", - "vtk==9.2.6", + "vtk==9.3.0", ] [project.urls] From d1a217648d5a6412eb7104ec96853cbec126643b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Nov 2023 08:04:41 +0000 Subject: [PATCH 66/74] MAINT: Bump the grpc-deps group with 1 update (#853) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 49f1d84a67..3164818446 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ tests = [ "ansys-tools-path==0.3.2", "beartype==0.16.4", "docker==6.1.3", - "google-api-python-client==2.107.0", + "google-api-python-client==2.108.0", "googleapis-common-protos==1.61.0", "grpcio==1.50.0", "grpcio-health-checking==1.48.2", From 9219c1441b028d02a0304095ac15a35fe1727d1e Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Wed, 15 Nov 2023 09:45:32 +0100 Subject: [PATCH 67/74] feat: adding Python 3.12 support (#789) --- .github/workflows/ci_cd.yml | 2 +- doc/source/assets.rst | 2 +- doc/source/conf.py | 2 +- doc/source/getting_started/installation.rst | 2 +- pyproject.toml | 1 + tox.ini | 3 ++- 6 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 2af056fae5..40a645ab65 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -47,7 +47,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ['3.9', '3.10', '3.11'] + python-version: ['3.9', '3.10', '3.11', '3.12'] should-release: - ${{ github.event_name == 'push' && contains(github.ref, 'refs/tags') }} exclude: diff --git a/doc/source/assets.rst b/doc/source/assets.rst index bc7d045f10..53a639bde0 100644 --- a/doc/source/assets.rst +++ b/doc/source/assets.rst @@ -18,7 +18,7 @@ If you lack an internet connection on your installation machine, you should inst by downloading the wheelhouse archive. Each wheelhouse archive contains all the Python wheels necessary to install PyAnsys Geometry from scratch on Windows, -Linux, and MacOS from Python 3.9 to 3.11. You can install this on an isolated system with a fresh Python +Linux, and MacOS from Python 3.9 to 3.12. You can install this on an isolated system with a fresh Python installation or on a virtual environment. For example, on Linux with Python 3.9, unzip the wheelhouse archive and install it with: diff --git a/doc/source/conf.py b/doc/source/conf.py index 80b1c8e914..c7f8f202b5 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -26,7 +26,7 @@ def get_wheelhouse_assets_dictionary(): """Auxiliary method to build the wheelhouse assets dictionary.""" assets_context_os = ["Linux", "Windows", "MacOS"] assets_context_runners = ["ubuntu-latest", "windows-latest", "macos-latest"] - assets_context_python_versions = ["3.9", "3.10", "3.11"] + assets_context_python_versions = ["3.9", "3.10", "3.11", "3.12"] assets_context_version = json.loads( requests.get("https://api.github.com/repos/ansys/pyansys-geometry/releases/latest").content )["name"] diff --git a/doc/source/getting_started/installation.rst b/doc/source/getting_started/installation.rst index 78f0829016..c144ba0699 100644 --- a/doc/source/getting_started/installation.rst +++ b/doc/source/getting_started/installation.rst @@ -70,7 +70,7 @@ archive for your corresponding machine architecture from the repository's `Relea `_. Each wheelhouse archive contains all the Python wheels necessary to install PyAnsys Geometry from scratch on Windows, -Linux, and MacOS from Python 3.9 to 3.11. You can install this on an isolated system with a fresh Python +Linux, and MacOS from Python 3.9 to 3.12. You can install this on an isolated system with a fresh Python installation or on a virtual environment. For example, on Linux with Python 3.9, unzip the wheelhouse archive and install it with these commands: diff --git a/pyproject.toml b/pyproject.toml index 3164818446..3f6feed576 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ diff --git a/tox.ini b/tox.ini index c2c0a1403c..35a42ede3f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] description = Default tox environments list envlist = - style,{tests39,tests310,tests311}{,-coverage},doc + style,{tests39,tests310,tests311,tests312}{,-coverage},doc skip_missing_interpreters = true isolated_build = true isolated_build_env = build @@ -12,6 +12,7 @@ basepython = tests39: python3.9 tests310: python3.10 tests311: python3.11 + tests312: python3.12 {style,tests,doc}: python3 setenv = PYTHONUNBUFFERED = yes From 82712288a4231a934b61263789cf4797e133092d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 17 Nov 2023 07:35:10 +0000 Subject: [PATCH 68/74] [pre-commit.ci] pre-commit autoupdate (#841) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> --- .pre-commit-config.yaml | 15 ++-- .reuse/dep5 | 84 ---------------------- .reuse/templates/ansys.jinja2 | 27 ------- src/ansys/geometry/core/typing.py | 76 ++++++++++---------- tests/conftest.py | 21 ++++++ tests/integration/conftest.py | 21 ++++++ tests/integration/test_client.py | 21 ++++++ tests/integration/test_design.py | 21 ++++++ tests/integration/test_design_import.py | 21 ++++++ tests/integration/test_edges.py | 21 ++++++ tests/integration/test_launcher_local.py | 21 ++++++ tests/integration/test_launcher_product.py | 22 ++++++ tests/integration/test_launcher_remote.py | 21 ++++++ tests/integration/test_logging_client.py | 21 ++++++ tests/integration/test_material.py | 21 ++++++ tests/integration/test_plotter.py | 22 ++++++ tests/integration/test_repair_tools.py | 21 ++++++ tests/integration/test_runscript.py | 22 ++++++ tests/integration/test_tessellation.py | 21 ++++++ tests/test_connection.py | 22 ++++++ tests/test_logging.py | 21 ++++++ tests/test_math.py | 22 ++++++ tests/test_metadata.py | 22 ++++++ tests/test_misc_accuracy.py | 22 ++++++ tests/test_misc_checks.py | 22 ++++++ tests/test_misc_measurements.py | 22 ++++++ tests/test_parameterization.py | 22 ++++++ tests/test_primitives.py | 22 ++++++ tests/test_sketch.py | 22 ++++++ 29 files changed, 582 insertions(+), 157 deletions(-) delete mode 100644 .reuse/dep5 delete mode 100644 .reuse/templates/ansys.jinja2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b52cf98607..51d942ab0d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ exclude: "tests/integration/files" repos: - repo: https://github.com/psf/black - rev: 23.9.1 # IF VERSION CHANGES --> MODIFY "blacken-docs" MANUALLY AS WELL!! + rev: 23.11.0 # IF VERSION CHANGES --> MODIFY "blacken-docs" MANUALLY AS WELL!! hooks: - id: black @@ -10,7 +10,7 @@ repos: rev: 1.16.0 hooks: - id: blacken-docs - additional_dependencies: [black==23.9.1] + additional_dependencies: [black==23.11.0] - repo: https://github.com/pycqa/isort rev: 5.12.0 @@ -49,14 +49,13 @@ repos: - id: check-yaml - id: trailing-whitespace -- repo: https://github.com/ansys/pre-commit-hooks - rev: v0.1.3 - hooks: - - id: add-license-headers - args: ["--loc", "./"] +# - repo: https://github.com/ansys/pre-commit-hooks +# rev: v0.2.2 +# hooks: +# - id: add-license-headers # this validates our github workflow files - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.27.0 + rev: 0.27.1 hooks: - id: check-github-workflows diff --git a/.reuse/dep5 b/.reuse/dep5 deleted file mode 100644 index 3884ad10d0..0000000000 --- a/.reuse/dep5 +++ /dev/null @@ -1,84 +0,0 @@ -Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: pyansys-geometry -Upstream-Contact: pyansys.core@ansys.com -Source: https://github.com/ansys/pyansys-geometry - -Files: .github/* -Copyright: 2023 ANSYS, Inc. and/or its affiliates. -License: MIT - -Files: .reuse/* -Copyright: 2023 ANSYS, Inc. and/or its affiliates. -License: MIT - -Files: doc/* -Copyright: 2023 ANSYS, Inc. and/or its affiliates. -License: MIT - -Files: docker/* -Copyright: 2023 ANSYS, Inc. and/or its affiliates. -License: MIT - -Files: tests/* -Copyright: 2023 ANSYS, Inc. and/or its affiliates. -License: MIT - -Files: .flake8 -Copyright: 2023 ANSYS, Inc. and/or its affiliates. -License: MIT - -Files: .gitattributes -Copyright: 2023 ANSYS, Inc. and/or its affiliates. -License: MIT - -Files: .gitignore -Copyright: 2023 ANSYS, Inc. and/or its affiliates. -License: MIT - -Files: .pre-commit-config.yaml -Copyright: 2023 ANSYS, Inc. and/or its affiliates. -License: MIT - -Files: AUTHORS -Copyright: 2023 ANSYS, Inc. and/or its affiliates. -License: MIT - -Files: CHANGELOG.md -Copyright: 2023 ANSYS, Inc. and/or its affiliates. -License: MIT - -Files: CODE_OF_CONDUCT.md -Copyright: 2023 ANSYS, Inc. and/or its affiliates. -License: MIT - -Files: CONTRIBUTING.md -Copyright: 2023 ANSYS, Inc. and/or its affiliates. -License: MIT - -Files: CONTRIBUTORS.md -Copyright: 2023 ANSYS, Inc. and/or its affiliates. -License: MIT - -Files: LICENSE -Copyright: 2023 ANSYS, Inc. and/or its affiliates. -License: MIT - -Files: pyproject.toml -Copyright: 2023 ANSYS, Inc. and/or its affiliates. -License: MIT - -Files: README.rst -Copyright: 2023 ANSYS, Inc. and/or its affiliates. -License: MIT - -Files: tox.ini -Copyright: 2023 ANSYS, Inc. and/or its affiliates. -License: MIT - -Files: *.json -Copyright: 2023 ANSYS, Inc. and/or its affiliates. -License: MIT - -Files: *.png -Copyright: 2023 ANSYS, Inc. and/or its affiliates. -License: MIT \ No newline at end of file diff --git a/.reuse/templates/ansys.jinja2 b/.reuse/templates/ansys.jinja2 deleted file mode 100644 index 81e7f3df0d..0000000000 --- a/.reuse/templates/ansys.jinja2 +++ /dev/null @@ -1,27 +0,0 @@ -{% for copyright_line in copyright_lines %} -{{ copyright_line }} -{% endfor %} -{% for expression in spdx_expressions %} -SPDX-License-Identifier: {{ expression }} -{% endfor %} - - -{% if "MIT" in spdx_expressions %} -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. -{% endif %} \ No newline at end of file diff --git a/src/ansys/geometry/core/typing.py b/src/ansys/geometry/core/typing.py index 64d6198634..c60ba1dccf 100644 --- a/src/ansys/geometry/core/typing.py +++ b/src/ansys/geometry/core/typing.py @@ -1,38 +1,38 @@ -# Copyright (C) 2023 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 typing of values for PyAnsys Geometry.""" - -from beartype.typing import Sequence, Union -import numpy as np - -Real = Union[int, float, np.integer, np.floating] -"""Type used to refer to both integers and floats as possible values.""" - -RealSequence = Union[np.ndarray, Sequence[Real]] -""" -Type used to refer to ``Real`` types as a ``Sequence`` type. - -Notes ------ -:class:`numpy.ndarrays ` are also accepted because they are -the overlaying data structure behind most PyAnsys Geometry objects. -""" +# Copyright (C) 2023 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 typing of values for PyAnsys Geometry.""" + +from beartype.typing import Sequence, Union +import numpy as np + +Real = Union[int, float, np.integer, np.floating] +"""Type used to refer to both integers and floats as possible values.""" + +RealSequence = Union[np.ndarray, Sequence[Real]] +""" +Type used to refer to ``Real`` types as a ``Sequence`` type. + +Notes +----- +:class:`numpy.ndarrays ` are also accepted because they are +the overlaying data structure behind most PyAnsys Geometry objects. +""" diff --git a/tests/conftest.py b/tests/conftest.py index 10754b229a..7e894120ee 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,24 @@ +# Copyright (C) 2023 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. """"General testing fixtures.""" import logging as deflogging # Default logging diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index ed8700e663..b91417af89 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,3 +1,24 @@ +# Copyright (C) 2023 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. """ This testing module automatically connects to the Geometry service running at localhost:50051. diff --git a/tests/integration/test_client.py b/tests/integration/test_client.py index a6df35cf81..e2d75f3b36 100644 --- a/tests/integration/test_client.py +++ b/tests/integration/test_client.py @@ -1,3 +1,24 @@ +# Copyright (C) 2023 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. """Test basic client connection.""" from grpc import insecure_channel import pytest diff --git a/tests/integration/test_design.py b/tests/integration/test_design.py index 1a436d46cd..6550e9f596 100644 --- a/tests/integration/test_design.py +++ b/tests/integration/test_design.py @@ -1,3 +1,24 @@ +# Copyright (C) 2023 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. """Test design interaction.""" import os diff --git a/tests/integration/test_design_import.py b/tests/integration/test_design_import.py index a1f3b2baba..1a5de1b0e6 100644 --- a/tests/integration/test_design_import.py +++ b/tests/integration/test_design_import.py @@ -1,3 +1,24 @@ +# Copyright (C) 2023 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. """Test design import.""" import numpy as np diff --git a/tests/integration/test_edges.py b/tests/integration/test_edges.py index a48daab810..460d81a3f0 100644 --- a/tests/integration/test_edges.py +++ b/tests/integration/test_edges.py @@ -1,3 +1,24 @@ +# Copyright (C) 2023 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. """Test edges.""" from ansys.geometry.core import Modeler diff --git a/tests/integration/test_launcher_local.py b/tests/integration/test_launcher_local.py index 2c3e2b834d..9b0edc7322 100644 --- a/tests/integration/test_launcher_local.py +++ b/tests/integration/test_launcher_local.py @@ -1,3 +1,24 @@ +# Copyright (C) 2023 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. """Testing module for the local launcher.""" import os diff --git a/tests/integration/test_launcher_product.py b/tests/integration/test_launcher_product.py index bf4224bf75..1e4838cb85 100644 --- a/tests/integration/test_launcher_product.py +++ b/tests/integration/test_launcher_product.py @@ -1,3 +1,25 @@ +# Copyright (C) 2023 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. + import random import pytest diff --git a/tests/integration/test_launcher_remote.py b/tests/integration/test_launcher_remote.py index 6609c7d96f..06a028b73e 100644 --- a/tests/integration/test_launcher_remote.py +++ b/tests/integration/test_launcher_remote.py @@ -1,3 +1,24 @@ +# Copyright (C) 2023 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. """Test the PyPIM integration.""" from unittest.mock import create_autospec diff --git a/tests/integration/test_logging_client.py b/tests/integration/test_logging_client.py index c85522d60b..a19829b595 100644 --- a/tests/integration/test_logging_client.py +++ b/tests/integration/test_logging_client.py @@ -1,3 +1,24 @@ +# Copyright (C) 2023 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. """"Testing of log module with client connection.""" import logging as deflogging # Default logging import re diff --git a/tests/integration/test_material.py b/tests/integration/test_material.py index 749a97a591..340f89ef19 100644 --- a/tests/integration/test_material.py +++ b/tests/integration/test_material.py @@ -1,3 +1,24 @@ +# Copyright (C) 2023 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. """Test material assignment.""" from pint import Quantity diff --git a/tests/integration/test_plotter.py b/tests/integration/test_plotter.py index 43d2fc6db5..090de869da 100644 --- a/tests/integration/test_plotter.py +++ b/tests/integration/test_plotter.py @@ -1,3 +1,25 @@ +# Copyright (C) 2023 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. + from pathlib import Path import numpy as np diff --git a/tests/integration/test_repair_tools.py b/tests/integration/test_repair_tools.py index 0d81bc28a2..d4e04b6e92 100644 --- a/tests/integration/test_repair_tools.py +++ b/tests/integration/test_repair_tools.py @@ -1,3 +1,24 @@ +# Copyright (C) 2023 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. """"Testing of repair tools.""" import pytest diff --git a/tests/integration/test_runscript.py b/tests/integration/test_runscript.py index be62dc11a3..5e5283f447 100644 --- a/tests/integration/test_runscript.py +++ b/tests/integration/test_runscript.py @@ -1,3 +1,25 @@ +# Copyright (C) 2023 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. + import re import pytest diff --git a/tests/integration/test_tessellation.py b/tests/integration/test_tessellation.py index a1b1abc619..520624f4d9 100644 --- a/tests/integration/test_tessellation.py +++ b/tests/integration/test_tessellation.py @@ -1,3 +1,24 @@ +# Copyright (C) 2023 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. """Test tessellation and plotting.""" import pytest diff --git a/tests/test_connection.py b/tests/test_connection.py index 93f46e3c27..c6ccefc0c8 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -1,3 +1,25 @@ +# Copyright (C) 2023 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. + from beartype.roar import BeartypeCallHintParamViolation import grpc import numpy as np diff --git a/tests/test_logging.py b/tests/test_logging.py index 4cc1ef1de9..bdb3b21e08 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -1,3 +1,24 @@ +# Copyright (C) 2023 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. """"Testing of log module.""" import logging as deflogging # Default logging import re diff --git a/tests/test_math.py b/tests/test_math.py index 41e92b4254..5d819b56d2 100644 --- a/tests/test_math.py +++ b/tests/test_math.py @@ -1,3 +1,25 @@ +# Copyright (C) 2023 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. + from io import UnsupportedOperation from beartype.roar import BeartypeCallHintParamViolation diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 206afb25cb..9d66e197ae 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -1,3 +1,25 @@ +# Copyright (C) 2023 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. + from ansys.geometry.core import __version__ diff --git a/tests/test_misc_accuracy.py b/tests/test_misc_accuracy.py index f54984168c..3ae679554a 100644 --- a/tests/test_misc_accuracy.py +++ b/tests/test_misc_accuracy.py @@ -1,3 +1,25 @@ +# Copyright (C) 2023 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. + import math from ansys.geometry.core.misc.accuracy import Accuracy diff --git a/tests/test_misc_checks.py b/tests/test_misc_checks.py index 1b23110a4c..0eed280c7b 100644 --- a/tests/test_misc_checks.py +++ b/tests/test_misc_checks.py @@ -1,3 +1,25 @@ +# Copyright (C) 2023 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. + import numpy as np import pytest diff --git a/tests/test_misc_measurements.py b/tests/test_misc_measurements.py index 29db0b803e..e6aeaca8e0 100644 --- a/tests/test_misc_measurements.py +++ b/tests/test_misc_measurements.py @@ -1,3 +1,25 @@ +# Copyright (C) 2023 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. + from pint import Quantity import pytest diff --git a/tests/test_parameterization.py b/tests/test_parameterization.py index 408c8fc4e3..9937e92be0 100644 --- a/tests/test_parameterization.py +++ b/tests/test_parameterization.py @@ -1,3 +1,25 @@ +# Copyright (C) 2023 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. + from beartype.roar import BeartypeCallHintParamViolation import numpy as np import pytest diff --git a/tests/test_primitives.py b/tests/test_primitives.py index 20292c5c38..29c98f6422 100644 --- a/tests/test_primitives.py +++ b/tests/test_primitives.py @@ -1,3 +1,25 @@ +# Copyright (C) 2023 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. + from beartype.roar import BeartypeCallHintParamViolation import numpy as np from pint import Quantity diff --git a/tests/test_sketch.py b/tests/test_sketch.py index 524063fdc4..a97b532f11 100644 --- a/tests/test_sketch.py +++ b/tests/test_sketch.py @@ -1,3 +1,25 @@ +# Copyright (C) 2023 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. + from beartype.roar import BeartypeCallHintParamViolation import numpy as np from pint import Quantity From c63680fac0a7493ebbd557fe2920e1749b4cf284 Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Fri, 17 Nov 2023 11:50:59 +0100 Subject: [PATCH 69/74] fix: proper versions on assets folder --- doc/source/conf.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index c7f8f202b5..fb17de5941 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -27,9 +27,15 @@ def get_wheelhouse_assets_dictionary(): assets_context_os = ["Linux", "Windows", "MacOS"] assets_context_runners = ["ubuntu-latest", "windows-latest", "macos-latest"] assets_context_python_versions = ["3.9", "3.10", "3.11", "3.12"] - assets_context_version = json.loads( - requests.get("https://api.github.com/repos/ansys/pyansys-geometry/releases/latest").content - )["name"] + if get_version_match(__version__) == "dev": + # Just point to the latest version + assets_context_version = json.loads( + requests.get( + "https://api.github.com/repos/ansys/pyansys-geometry/releases/latest" + ).content + )["name"] + else: + assets_context_version = f"v{__version__}" assets = {} for assets_os, assets_runner in zip(assets_context_os, assets_context_runners): From c75943dba4be6e2f6823bc84882c2f57b413f3a6 Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Mon, 20 Nov 2023 15:11:27 +0100 Subject: [PATCH 70/74] feat: import surface body properly when reading design + boolean ops with multiple bodies (#846) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- src/ansys/geometry/core/designer/body.py | 86 +++++++++++++--------- src/ansys/geometry/core/designer/design.py | 3 +- tests/integration/test_design.py | 74 ++++++++++++++++++- tests/integration/test_design_import.py | 21 ++++++ 5 files changed, 146 insertions(+), 40 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3f6feed576..fc84c40339 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ ] dependencies = [ - "ansys-api-geometry==0.3.3", + "ansys-api-geometry==0.3.5", "ansys-tools-path>=0.3", "beartype>=0.11.0", "google-api-python-client>=1.7.11", diff --git a/src/ansys/geometry/core/designer/body.py b/src/ansys/geometry/core/designer/body.py index d38376a78f..3a74fada27 100644 --- a/src/ansys/geometry/core/designer/body.py +++ b/src/ansys/geometry/core/designer/body.py @@ -40,7 +40,7 @@ ) from ansys.api.geometry.v0.commands_pb2_grpc import CommandsStub from beartype import beartype as check_input_types -from beartype.typing import TYPE_CHECKING, List, Optional, Tuple, Union +from beartype.typing import TYPE_CHECKING, Iterable, List, Optional, Tuple, Union from pint import Quantity from ansys.geometry.core.connection.client import GrpcClient @@ -446,9 +446,9 @@ def plot( """ return - def intersect(self, other: "Body") -> None: + def intersect(self, other: Union["Body", Iterable["Body"]]) -> None: """ - Intersect two bodies. + Intersect two (or more) bodies. Notes ----- @@ -469,9 +469,9 @@ def intersect(self, other: "Body") -> None: return @protect_grpc - def subtract(self, other: "Body") -> None: + def subtract(self, other: Union["Body", Iterable["Body"]]) -> None: """ - Subtract two bodies. + Subtract two (or more) bodies. Notes ----- @@ -492,9 +492,9 @@ def subtract(self, other: "Body") -> None: return @protect_grpc - def unite(self, other: "Body") -> None: + def unite(self, other: Union["Body", Iterable["Body"]]) -> None: """ - Unite two bodies. + Unite two (or more) bodies. Notes ----- @@ -803,17 +803,17 @@ def plot( "MasterBody does not implement plot methods. Call this method on a body instead." ) - def intersect(self, other: "Body") -> None: # noqa: D102 + def intersect(self, other: Union["Body", Iterable["Body"]]) -> None: # noqa: D102 raise NotImplementedError( "MasterBody does not implement Boolean methods. Call this method on a body instead." ) - def subtract(self, other: "Body") -> None: # noqa: D102 + def subtract(self, other: Union["Body", Iterable["Body"]]) -> None: # noqa: D102 raise NotImplementedError( "MasterBody does not implement Boolean methods. Call this method on a body instead." ) - def unite(self, other: "Body") -> None: + def unite(self, other: Union["Body", Iterable["Body"]]) -> None: # noqa: D102 raise NotImplementedError( "MasterBody does not implement Boolean methods. Call this method on a body instead." @@ -1108,40 +1108,56 @@ def plot( self, merge_bodies=merge, screenshot=screenshot, **plotting_options ) - @protect_grpc - @reset_tessellation_cache - @ensure_design_is_active - def intersect(self, other: "Body") -> None: # noqa: D102 - response = self._template._bodies_stub.Boolean( - BooleanRequest(body1=self.id, body2=other.id, method="intersect") - ).empty_result + def intersect(self, other: Union["Body", Iterable["Body"]]) -> None: # noqa: D102 + self.__generic_boolean_op(other, "intersect", "bodies do not intersect") - if response == 1: - raise ValueError("Bodies do not intersect.") + def subtract(self, other: Union["Body", Iterable["Body"]]) -> None: # noqa: D102 + self.__generic_boolean_op(other, "subtract", "empty (complete) subtraction") - other.parent_component.delete_body(other) + def unite(self, other: Union["Body", Iterable["Body"]]) -> None: # noqa: D102 + self.__generic_boolean_op(other, "unite", "union operation failed") @protect_grpc @reset_tessellation_cache @ensure_design_is_active - def subtract(self, other: "Body") -> None: # noqa: D102 - response = self._template._bodies_stub.Boolean( - BooleanRequest(body1=self.id, body2=other.id, method="subtract") - ).empty_result + @check_input_types + def __generic_boolean_op( + self, other: Union["Body", Iterable["Body"]], type_bool_op: str, err_bool_op: str + ) -> None: + grpc_other = other if isinstance(other, Iterable) else [other] + try: + response = self._template._bodies_stub.Boolean( + BooleanRequest( + body1=self.id, tool_bodies=[b.id for b in grpc_other], method=type_bool_op + ) + ).empty_result + except Exception as err: + # TODO: to be deleted - old versions did not have "tool_bodies" in the request + # This is a temporary fix to support old versions of the server - should be deleted + # once the server is no longer supported. + if not isinstance(other, Iterable): + response = self._template._bodies_stub.Boolean( + BooleanRequest(body1=self.id, body2=other.id, method=type_bool_op) + ).empty_result + else: + all_response = [] + for body2 in other: + response = self._template._bodies_stub.Boolean( + BooleanRequest(body1=self.id, body2=body2.id, method=type_bool_op) + ).empty_result + all_response.append(response) + + if all_response.count(1) > 0: + response = 1 if response == 1: - raise ValueError("Subtraction of bodies results in an empty (complete) subtraction.") - - other.parent_component.delete_body(other) + raise ValueError( + f"Boolean operation of type '{type_bool_op}' failed: {err_bool_op}.\n" + f"Involving bodies:{self}, {grpc_other}" + ) - @protect_grpc - @reset_tessellation_cache - @ensure_design_is_active - def unite(self, other: "Body") -> None: # noqa: D102 - self._template._bodies_stub.Boolean( - BooleanRequest(body1=self.id, body2=other.id, method="unite") - ) - other.parent_component.delete_body(other) + for b in grpc_other: + b.parent_component.delete_body(b) def __repr__(self) -> str: """Represent the ``Body`` as a string.""" diff --git a/src/ansys/geometry/core/designer/design.py b/src/ansys/geometry/core/designer/design.py index c04683108f..db3db3ab95 100644 --- a/src/ansys/geometry/core/designer/design.py +++ b/src/ansys/geometry/core/designer/design.py @@ -676,10 +676,9 @@ def __read_existing_design(self) -> None: parent.components.append(c) # Create Bodies - # TODO: is_surface? for body in response.bodies: part = created_parts.get(body.parent_id) - tb = MasterBody(body.id, body.name, self._grpc_client) + tb = MasterBody(body.id, body.name, self._grpc_client, is_surface=body.is_surface) part.bodies.append(tb) created_bodies[body.id] = tb diff --git a/tests/integration/test_design.py b/tests/integration/test_design.py index 6550e9f596..dff0f5c0a7 100644 --- a/tests/integration/test_design.py +++ b/tests/integration/test_design.py @@ -1486,7 +1486,7 @@ def test_boolean_body_operations(modeler: Modeler, skip_not_on_linux_service): # 1.a.ii copy1 = body1.copy(comp1, "Copy1") copy3 = body3.copy(comp3, "Copy3") - with pytest.raises(ValueError, match="Bodies do not intersect."): + with pytest.raises(ValueError, match="bodies do not intersect"): copy1.intersect(copy3) assert copy1.is_alive @@ -1595,7 +1595,7 @@ def test_boolean_body_operations(modeler: Modeler, skip_not_on_linux_service): # 2.a.ii copy1 = body1.copy(comp1_i, "Copy1") copy3 = body3.copy(comp3_i, "Copy3") - with pytest.raises(ValueError, match="Bodies do not intersect."): + with pytest.raises(ValueError, match="bodies do not intersect"): copy1.intersect(copy3) assert copy1.is_alive @@ -1665,6 +1665,76 @@ def test_boolean_body_operations(modeler: Modeler, skip_not_on_linux_service): assert Accuracy.length_is_equal(copy1.volume.m, 1) +def test_multiple_bodies_boolean_operations(modeler: Modeler, skip_not_on_linux_service): + """Test boolean operations with multiple bodies.""" + + design = modeler.create_design("TestBooleanOperationsMultipleBodies") + + comp1 = design.add_component("Comp1") + comp2 = design.add_component("Comp2") + comp3 = design.add_component("Comp3") + + body1 = comp1.extrude_sketch("Body1", Sketch().box(Point2D([0, 0]), 1, 1), 1) + body2 = comp2.extrude_sketch("Body2", Sketch().box(Point2D([0.5, 0]), 1, 1), 1) + body3 = comp3.extrude_sketch("Body3", Sketch().box(Point2D([5, 0]), 1, 1), 1) + + ################# Check subtract operation ################# + copy1_sub = body1.copy(comp1, "Copy1_subtract") + copy2_sub = body2.copy(comp2, "Copy2_subtract") + copy3_sub = body3.copy(comp3, "Copy3_subtract") + copy1_sub.subtract([copy2_sub, copy3_sub]) + + assert not copy2_sub.is_alive + assert not copy3_sub.is_alive + assert body2.is_alive + assert body3.is_alive + assert len(comp1.bodies) == 2 + assert len(comp2.bodies) == 1 + assert len(comp3.bodies) == 1 + + # Cleanup previous subtest + comp1.delete_body(copy1_sub) + assert len(comp1.bodies) == 1 + + ################# Check unite operation ################# + copy1_uni = body1.copy(comp1, "Copy1_unite") + copy2_uni = body2.copy(comp2, "Copy2_unite") + copy3_uni = body3.copy(comp3, "Copy3_unite") + copy1_uni.unite([copy2_uni, copy3_uni]) + + assert not copy2_uni.is_alive + assert not copy3_uni.is_alive + assert body2.is_alive + assert body3.is_alive + assert len(comp1.bodies) == 2 + assert len(comp2.bodies) == 1 + assert len(comp3.bodies) == 1 + + # Cleanup previous subtest + comp1.delete_body(copy1_uni) + assert len(comp1.bodies) == 1 + + ################# Check intersect operation ################# + copy1_int = body1.copy(comp1, "Copy1_intersect") + copy2_int = body2.copy(comp2, "Copy2_intersect") + copy3_int = body3.copy(comp3, "Copy3_intersect") # Body 3 does not intersect them + copy1_int.intersect([copy2_int]) + + assert not copy2_int.is_alive + assert copy3_int.is_alive + assert body2.is_alive + assert body3.is_alive + assert len(comp1.bodies) == 2 + assert len(comp2.bodies) == 1 + assert len(comp3.bodies) == 2 + + # Cleanup previous subtest + comp1.delete_body(copy1_int) + comp3.delete_body(copy3_int) + assert len(comp1.bodies) == 1 + assert len(comp3.bodies) == 1 + + def test_child_component_instances(modeler: Modeler): """Test creation of child ``Component`` instances and check the data model reflects that.""" diff --git a/tests/integration/test_design_import.py b/tests/integration/test_design_import.py index 1a5de1b0e6..808b0fb59e 100644 --- a/tests/integration/test_design_import.py +++ b/tests/integration/test_design_import.py @@ -123,6 +123,27 @@ def test_design_import_simple_case(modeler: Modeler): _checker_method(read_design, design) +def test_design_import_with_surfaces_issue834(modeler: Modeler): + """ + Import a Design which is expected to contain surfaces. + + For more info see https://github.com/ansys/pyansys-geometry/issues/834 + """ + # TODO: to be reactivated by https://github.com/ansys/pyansys-geometry/issues/799 + if modeler.client.backend_type != BackendType.LINUX_SERVICE: + # Open the design + design = modeler.open_file("./tests/integration/files/DuplicateFacesDesignBefore.scdocx") + + # Check that there are two bodies + assert len(design.bodies) == 2 + + # Check some basic properties - whether they are surfaces or not! + assert design.bodies[0].name == "BoxBody" + assert design.bodies[0].is_surface == False + assert design.bodies[1].name == "DuplicatesSurface" + assert design.bodies[1].is_surface == True + + def test_open_file(modeler: Modeler, tmp_path_factory: pytest.TempPathFactory): """Test creation of a component, saving it to a file, and loading it again to a second component and make sure they have the same properties.""" From 2fdbf6f413795d0f03b2aa1df8386512eda12316 Mon Sep 17 00:00:00 2001 From: Jonah Boling <56607167+jonahrb@users.noreply.github.com> Date: Mon, 20 Nov 2023 10:05:21 -0500 Subject: [PATCH 71/74] Rotate a body (#849) Co-authored-by: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> --- src/ansys/geometry/core/designer/body.py | 59 +++++++++++++++++++++++- tests/integration/test_design.py | 23 +++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/ansys/geometry/core/designer/body.py b/src/ansys/geometry/core/designer/body.py index 3a74fada27..c6763dcd08 100644 --- a/src/ansys/geometry/core/designer/body.py +++ b/src/ansys/geometry/core/designer/body.py @@ -28,6 +28,7 @@ from ansys.api.geometry.v0.bodies_pb2 import ( BooleanRequest, CopyRequest, + RotateRequest, SetAssignedMaterialRequest, TranslateRequest, ) @@ -45,6 +46,7 @@ from ansys.geometry.core.connection.client import GrpcClient from ansys.geometry.core.connection.conversions import ( + point3d_to_grpc_point, sketch_shapes_to_grpc_geometries, tess_to_pd, unit_vector_to_grpc_direction, @@ -55,9 +57,10 @@ from ansys.geometry.core.materials.material import Material from ansys.geometry.core.math.constants import IDENTITY_MATRIX44 from ansys.geometry.core.math.matrix import Matrix44 +from ansys.geometry.core.math.point import Point3D from ansys.geometry.core.math.vector import UnitVector3D from ansys.geometry.core.misc.checks import check_type, ensure_design_is_active -from ansys.geometry.core.misc.measurements import DEFAULT_UNITS, Distance +from ansys.geometry.core.misc.measurements import DEFAULT_UNITS, Angle, Distance from ansys.geometry.core.sketch.sketch import Sketch from ansys.geometry.core.typing import Real @@ -319,6 +322,30 @@ def translate(self, direction: UnitVector3D, distance: Union[Quantity, Distance, """ return + @abstractmethod + def rotate( + self, + axis_origin: Point3D, + axis_direction: UnitVector3D, + angle: Union[Quantity, Angle, Real], + ) -> None: + """ + Rotate the geometry body around the specified axis by a given angle. + + Parameters + ---------- + axis_origin: Point3D + Origin of the rotational axis. + axis_direction: UnitVector3D + The axis of rotation. + angle: Union[~pint.Quantity, Angle, Real] + Angle (magnitude) of the rotation. + Returns + ------- + None + """ + return + @abstractmethod def copy(self, parent: "Component", name: str = None) -> "Body": """ @@ -740,6 +767,27 @@ def translate( ) ) + @protect_grpc + @check_input_types + @reset_tessellation_cache + def rotate( + self, + axis_origin: Point3D, + axis_direction: UnitVector3D, + angle: Union[Quantity, Angle, Real], + ) -> None: # noqa: D102 + angle = angle if isinstance(angle, Angle) else Angle(angle) + rotation_magnitude = angle.value.m_as(DEFAULT_UNITS.SERVER_ANGLE) + self._grpc_client.log.debug(f"Rotating body {self.id}.") + self._bodies_stub.Rotate( + RotateRequest( + id=self.id, + axis_origin=point3d_to_grpc_point(axis_origin), + axis_direction=unit_vector_to_grpc_direction(axis_direction), + angle=rotation_magnitude, + ) + ) + @protect_grpc def copy(self, parent: "Component", name: str = None) -> "Body": # noqa: D102 from ansys.geometry.core.designer.component import Component @@ -1083,6 +1131,15 @@ def translate( ) -> None: # noqa: D102 return self._template.translate(direction, distance) + @ensure_design_is_active + def rotate( + self, + axis_origin: Point3D, + axis_direction: UnitVector3D, + angle: Union[Quantity, Angle, Real], + ) -> None: # noqa: D102 + return self._template.rotate(axis_origin, axis_direction, angle) + @ensure_design_is_active def copy(self, parent: "Component", name: str = None) -> "Body": # noqa: D102 return self._template.copy(parent, name) diff --git a/tests/integration/test_design.py b/tests/integration/test_design.py index dff0f5c0a7..4a55df72f3 100644 --- a/tests/integration/test_design.py +++ b/tests/integration/test_design.py @@ -798,6 +798,29 @@ def test_bodies_translation(modeler: Modeler): ) +def test_body_rotation(modeler: Modeler): + """Test for verifying the correct rotation of a ``Body``.""" + + # Create your design on the server side + design = modeler.create_design("BodyRotation_Test") + + body = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) + + original_vertices = [] + for edge in body.edges: + original_vertices.extend([edge.start_point, edge.end_point]) + + body.rotate(Point3D([0, 0, 0]), UnitVector3D([0, 0, 1]), np.pi / 4) + + new_vertices = [] + for edge in body.edges: + new_vertices.extend([edge.start_point, edge.end_point]) + + # Make sure no vertices are in the same position as in before rotation + for old_vertex, new_vertex in zip(original_vertices, new_vertices): + assert not np.allclose(old_vertex, new_vertex) + + def test_download_file(modeler: Modeler, tmp_path_factory: pytest.TempPathFactory): """Test for downloading a design in multiple modes and verifying the correct download.""" From 99db520c4f9e00222cdeaea5eb5c3adfa21a42e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Nov 2023 07:44:07 +0100 Subject: [PATCH 72/74] MAINT: Bump ansys-tools-path from 0.3.2 to 0.4.0 (#856) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fc84c40339..8cb20aecf7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ all = [ ] tests = [ "ansys-platform-instancemanagement==1.1.2", - "ansys-tools-path==0.3.2", + "ansys-tools-path==0.4.0", "beartype==0.16.4", "docker==6.1.3", "google-api-python-client==2.108.0", From f5812ca2ea5338feca04b9b34ff7cc542202637c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Nov 2023 07:11:59 +0000 Subject: [PATCH 73/74] MAINT: Bump scipy from 1.11.3 to 1.11.4 (#855) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8cb20aecf7..31aea9ac9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ tests = [ "pytest-xvfb==3.0.0", "pyvista[trame]==0.41.1", "requests==2.31.0", - "scipy==1.11.3", + "scipy==1.11.4", "six==1.16.0", "vtk==9.3.0", ] From f3c32e80d4bcc187214e14526c5c509beb1fc741 Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Tue, 21 Nov 2023 09:06:19 +0100 Subject: [PATCH 74/74] fix: typo in docs (#857) --- doc/source/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/index.rst b/doc/source/index.rst index cec0e8af9f..e7e0ac50e2 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -122,7 +122,7 @@ PyAnsys Geometry is a Python client library for the Ansys Geometry service. :outline: :click-parent: - Assets + Assets .. jinja:: main_toctree