From cfac4529f099b4a2effba2cb3688f23fcfcf6c95 Mon Sep 17 00:00:00 2001 From: Kerry McAdams <58492561+klmcadams@users.noreply.github.com> Date: Thu, 14 Nov 2024 11:10:05 -0500 Subject: [PATCH 01/44] docs: add Mechanical API link to Mechanical Scripting page (#972) Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Co-authored-by: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> --- doc/changelog.d/972.documentation.md | 1 + doc/source/links.rst | 3 ++- doc/source/user_guide_scripting/index.rst | 7 +++++-- 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 doc/changelog.d/972.documentation.md diff --git a/doc/changelog.d/972.documentation.md b/doc/changelog.d/972.documentation.md new file mode 100644 index 000000000..6474743c5 --- /dev/null +++ b/doc/changelog.d/972.documentation.md @@ -0,0 +1 @@ +add Mechanical API link to Mechanical Scripting page \ No newline at end of file diff --git a/doc/source/links.rst b/doc/source/links.rst index 589b90e68..40bf1f4ba 100644 --- a/doc/source/links.rst +++ b/doc/source/links.rst @@ -19,7 +19,8 @@ .. _Chapter 7: https://ansyshelp.ansys.com/account/secured?returnurl=/Views/Secured/corp/%%VERSION%%/en/installation/win_silent.html .. # Mechanical related -.. _Mechanical scripting interface: https://developer.ansys.com/docs/mechanical-scripting-interface/api/index.md +.. _Mechanical API Documentation: https://scripting.mechanical.docs.pyansys.com/version/dev/api/index.html +.. _Mechanical scripting interface APIs: https://developer.ansys.com/docs/mechanical-scripting-interface/api/index.md .. _ACT API Reference Guide: https://ansyshelp.ansys.com/account/secured?returnurl=/Views/Secured/corp/v242/en/act_ref/act_ref.html .. _Mechanical API known issues and limitations: https://ansyshelp.ansys.com/account/secured?returnurl=/Views/Secured/corp/%%VERSION%%/en/act_script/mech_apis_KIL.html?q=known%20issues .. _ACT known issues and limitations: https://ansyshelp.ansys.com/account/secured?returnurl=/Views/Secured/corp/%%VERSION%%/en/act_dev/act_dev_knownissues.html diff --git a/doc/source/user_guide_scripting/index.rst b/doc/source/user_guide_scripting/index.rst index 2280a98e7..808d51e95 100644 --- a/doc/source/user_guide_scripting/index.rst +++ b/doc/source/user_guide_scripting/index.rst @@ -23,8 +23,11 @@ You could already perform scripting of Mechanical with Python from inside Mechanical. PyMechanical leverages the same APIs but allows you to run your automation from outside Mechanical. -For comprehensive information on these APIs, see the `Scripting in Mechanical Guide`_ in the -Ansys Help. +.. note:: + + For comprehensive information on these APIs, see the `Scripting in Mechanical Guide`_ in the + Ansys Help portal, the `Mechanical scripting interface APIs`_ in the Developer Portal, or the + `Mechanical API Documentation`_. Recording --------- From 1b83856aa6b2f9df3d2630f872a3873b20f48e24 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 19:26:55 +0000 Subject: [PATCH 02/44] CHORE: Bump grpcio from 1.67.1 to 1.68.0 in the core group (#981) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/981.maintenance.md | 1 + pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 doc/changelog.d/981.maintenance.md diff --git a/doc/changelog.d/981.maintenance.md b/doc/changelog.d/981.maintenance.md new file mode 100644 index 000000000..47c9b5c59 --- /dev/null +++ b/doc/changelog.d/981.maintenance.md @@ -0,0 +1 @@ +Bump grpcio from 1.67.1 to 1.68.0 in the core group \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 38bb6b30f..0942142ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,7 @@ tests = [ doc = [ "sphinx==8.1.3", "ansys-sphinx-theme[autoapi]==1.2.0", - "grpcio==1.67.1", + "grpcio==1.68.0", "imageio-ffmpeg==0.5.1", "imageio==2.36.0", "jupyter_sphinx==0.5.3", From 21ff37c5552fa49d48d8e166b5a369e5d5ab4bc2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 19:41:45 +0000 Subject: [PATCH 03/44] CHORE: Bump the doc group with 2 updates (#982) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/982.maintenance.md | 1 + pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 doc/changelog.d/982.maintenance.md diff --git a/doc/changelog.d/982.maintenance.md b/doc/changelog.d/982.maintenance.md new file mode 100644 index 000000000..46cb383cb --- /dev/null +++ b/doc/changelog.d/982.maintenance.md @@ -0,0 +1 @@ +Bump the doc group with 2 updates \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 0942142ca..f3bfd937c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ tests = [ ] doc = [ "sphinx==8.1.3", - "ansys-sphinx-theme[autoapi]==1.2.0", + "ansys-sphinx-theme[autoapi]==1.2.1", "grpcio==1.68.0", "imageio-ffmpeg==0.5.1", "imageio==2.36.0", @@ -67,7 +67,7 @@ doc = [ "numpy==2.1.3", "numpydoc==1.8.0", "pandas==2.2.3", - "panel==1.5.3", + "panel==1.5.4", "plotly==5.24.1", "pypandoc==1.14", "pytest-sphinx==0.6.3", From 87e3da7ea6ae122c4e5a02c2419fa4398bfb5109 Mon Sep 17 00:00:00 2001 From: Dipin <26918585+dipinknair@users.noreply.github.com> Date: Mon, 18 Nov 2024 14:30:08 -0600 Subject: [PATCH 04/44] FEAT: Adding new method for connecting to Mechanical instance (#980) Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/980.added.md | 1 + doc/source/cheatsheet/cheat_sheet.qmd | 6 +- src/ansys/mechanical/core/__init__.py | 1 + src/ansys/mechanical/core/mechanical.py | 83 +++++++++++++++++++++++++ tests/conftest.py | 13 +--- 5 files changed, 89 insertions(+), 15 deletions(-) create mode 100644 doc/changelog.d/980.added.md diff --git a/doc/changelog.d/980.added.md b/doc/changelog.d/980.added.md new file mode 100644 index 000000000..feb48b43a --- /dev/null +++ b/doc/changelog.d/980.added.md @@ -0,0 +1 @@ +Adding new method for connecting to Mechanical instance \ No newline at end of file diff --git a/doc/source/cheatsheet/cheat_sheet.qmd b/doc/source/cheatsheet/cheat_sheet.qmd index a3b0d01bb..a500813c2 100644 --- a/doc/source/cheatsheet/cheat_sheet.qmd +++ b/doc/source/cheatsheet/cheat_sheet.qmd @@ -75,12 +75,12 @@ ansys-mechanical -r 242 --port 10000 -g ## Manually connect to the Mechanical session ```{python} #| eval: false -import ansys.mechanical.core as pymechanical +from ansys.mechanical.core import connect_to_mechanical # Connect locally -mechanical = pymechanical.Mechanical(port=10000) +mechanical = connect_to_mechanical(port=10000) # Or # Connect remotely, to the IP address or hostname -mechanical = pymechanical.Mechanical( +mechanical = connect_to_mechanical( "192.168.0.1", port=10000 ) ``` diff --git a/src/ansys/mechanical/core/__init__.py b/src/ansys/mechanical/core/__init__.py index b16c3c325..d96a539db 100644 --- a/src/ansys/mechanical/core/__init__.py +++ b/src/ansys/mechanical/core/__init__.py @@ -50,6 +50,7 @@ from ansys.mechanical.core.mechanical import ( change_default_mechanical_path, close_all_local_instances, + connect_to_mechanical, get_mechanical_path, launch_mechanical, ) diff --git a/src/ansys/mechanical/core/mechanical.py b/src/ansys/mechanical/core/mechanical.py index ec4303152..55c7bc5fd 100644 --- a/src/ansys/mechanical/core/mechanical.py +++ b/src/ansys/mechanical/core/mechanical.py @@ -2242,3 +2242,86 @@ def launch_mechanical( raise exception return mechanical + + +def connect_to_mechanical( + ip=None, + port=None, + loglevel="ERROR", + log_file=False, + log_mechanical=None, + connect_timeout=120, + clear_on_connect=False, + cleanup_on_exit=False, + keep_connection_alive=True, +) -> Mechanical: + """Connect to an existing Mechanical server instance. + + Parameters + ---------- + ip : str, optional + IP address for connecting to an existing Mechanical instance. The + IP address defaults to ``"127.0.0.1"``. + port : int, optional + Port to listen on for an existing Mechanical instance. The default is ``None``, + in which case ``10000`` is used. You can override the + default behavior of this parameter with the + ``PYMECHANICAL_PORT=`` environment variable. + loglevel : str, optional + Level of messages to print to the console. + Options are: + + - ``"WARNING"``: Prints only Ansys warning messages. + - ``"ERROR"``: Prints only Ansys error messages. + - ``"INFO"``: Prints all Ansys messages. + + The default is ``WARNING``. + log_file : bool, optional + Whether to copy the messages to a file named ``logs.log``, which is + located where the Python script is executed. The default is ``False``. + log_mechanical : str, optional + Path to the output file on the local disk to write every script + command to. The default is ``None``. However, you might set + ``"log_mechanical='pymechanical_log.txt'"`` to write all commands that are + sent to Mechanical via PyMechanical to this file. You can then use these + commands to run a script within Mechanical without PyMechanical. + connect_timeout : float, optional + Maximum allowable time in seconds to connect to the Mechanical server. + The default is ``120``. + clear_on_connect : bool, optional + Whether to clear the Mechanical instance when connecting. The default is ``False``. + When ``True``, a fresh environment is provided when you connect to Mechanical. + cleanup_on_exit : bool, optional + Whether to exit Mechanical when Python exits. The default is ``False``. + When ``False``, Mechanical is not exited when the garbage for this Mechanical + instance is collected. + keep_connection_alive : bool, optional + Whether to keep the gRPC connection alive by running a background thread + and making dummy calls for remote connections. The default is ``True``. + + Returns + ------- + ansys.mechanical.core.mechanical.Mechanical + Instance of Mechanical. + + Examples + -------- + Connect to an existing Mechanical instance at IP address ``192.168.1.30`` on port + ``50001``.. + + + >>> from ansys.mechanical.core import connect_to_mechanical + >>> pymech = connect_to_mechanical(ip='192.168.1.30', port=50001) + """ + return launch_mechanical( + start_instance=False, + loglevel=loglevel, + log_file=log_file, + log_mechanical=log_mechanical, + start_timeout=connect_timeout, + port=port, + ip=ip, + clear_on_connect=clear_on_connect, + cleanup_on_exit=cleanup_on_exit, + keep_connection_alive=keep_connection_alive, + ) diff --git a/tests/conftest.py b/tests/conftest.py index 0030e9a1d..74de05054 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -203,17 +203,6 @@ def rootdir(): yield base.parent -# @pytest.fixture() -# def subprocess_pass_expected(pytestconfig): -# """Checks for conditions to see if scripts run in subprocess are expected to pass or not.""" - -# version = pytestconfig.getoption("ansys_version") -# if os.name != "nt" and int(version) < 251: -# yield False -# else: -# yield True - - @pytest.fixture() def disable_cli(): ansys.mechanical.core.run.DRY_RUN = True @@ -290,7 +279,7 @@ def connect_to_mechanical_instance(port=None, clear_on_connect=False): # ip needs to be passed or start instance takes precedence # typical for container scenarios use connect # and needs to be treated as remote scenarios - mechanical = pymechanical.launch_mechanical( + mechanical = pymechanical.connect_to_mechanical( ip=hostname, port=port, clear_on_connect=clear_on_connect, cleanup_on_exit=False ) return mechanical From b660716227ed38b625c008ea59a093de4d46be52 Mon Sep 17 00:00:00 2001 From: PyAnsys CI Bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Tue, 19 Nov 2024 02:25:39 +0100 Subject: [PATCH 05/44] chore: update CHANGELOG for v0.11.10 (#984) --- doc/changelog.d/963.maintenance.md | 1 - doc/changelog.d/965.maintenance.md | 1 - doc/changelog.d/966.maintenance.md | 1 - doc/changelog.d/967.maintenance.md | 1 - doc/changelog.d/968.maintenance.md | 1 - doc/changelog.d/969.maintenance.md | 1 - doc/changelog.d/971.maintenance.md | 1 - doc/changelog.d/972.documentation.md | 1 - doc/changelog.d/974.fixed.md | 1 - doc/changelog.d/977.maintenance.md | 1 - doc/changelog.d/979.added.md | 1 - doc/changelog.d/980.added.md | 1 - doc/changelog.d/981.maintenance.md | 1 - doc/changelog.d/982.maintenance.md | 1 - doc/changelog.d/984.maintenance.md | 1 + doc/source/changelog.rst | 35 ++++++++++++++++++++++++++++ 16 files changed, 36 insertions(+), 14 deletions(-) delete mode 100644 doc/changelog.d/963.maintenance.md delete mode 100644 doc/changelog.d/965.maintenance.md delete mode 100644 doc/changelog.d/966.maintenance.md delete mode 100644 doc/changelog.d/967.maintenance.md delete mode 100644 doc/changelog.d/968.maintenance.md delete mode 100644 doc/changelog.d/969.maintenance.md delete mode 100644 doc/changelog.d/971.maintenance.md delete mode 100644 doc/changelog.d/972.documentation.md delete mode 100644 doc/changelog.d/974.fixed.md delete mode 100644 doc/changelog.d/977.maintenance.md delete mode 100644 doc/changelog.d/979.added.md delete mode 100644 doc/changelog.d/980.added.md delete mode 100644 doc/changelog.d/981.maintenance.md delete mode 100644 doc/changelog.d/982.maintenance.md create mode 100644 doc/changelog.d/984.maintenance.md diff --git a/doc/changelog.d/963.maintenance.md b/doc/changelog.d/963.maintenance.md deleted file mode 100644 index a6b550420..000000000 --- a/doc/changelog.d/963.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -update CHANGELOG for v0.11.9 \ No newline at end of file diff --git a/doc/changelog.d/965.maintenance.md b/doc/changelog.d/965.maintenance.md deleted file mode 100644 index b238e087b..000000000 --- a/doc/changelog.d/965.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -Modify how job success is verified for CI/CD \ No newline at end of file diff --git a/doc/changelog.d/966.maintenance.md b/doc/changelog.d/966.maintenance.md deleted file mode 100644 index 107cc77ef..000000000 --- a/doc/changelog.d/966.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -Bump mikepenz/action-junit-report from 4 to 5 \ No newline at end of file diff --git a/doc/changelog.d/967.maintenance.md b/doc/changelog.d/967.maintenance.md deleted file mode 100644 index fb4a912cf..000000000 --- a/doc/changelog.d/967.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -Bump grpcio from 1.67.0 to 1.67.1 in the core group \ No newline at end of file diff --git a/doc/changelog.d/968.maintenance.md b/doc/changelog.d/968.maintenance.md deleted file mode 100644 index 46cb383cb..000000000 --- a/doc/changelog.d/968.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -Bump the doc group with 2 updates \ No newline at end of file diff --git a/doc/changelog.d/969.maintenance.md b/doc/changelog.d/969.maintenance.md deleted file mode 100644 index 29df49383..000000000 --- a/doc/changelog.d/969.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -Bump pytest-cov from 5.0.0 to 6.0.0 \ No newline at end of file diff --git a/doc/changelog.d/971.maintenance.md b/doc/changelog.d/971.maintenance.md deleted file mode 100644 index 1905364bf..000000000 --- a/doc/changelog.d/971.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -Update docs build action container \ No newline at end of file diff --git a/doc/changelog.d/972.documentation.md b/doc/changelog.d/972.documentation.md deleted file mode 100644 index 6474743c5..000000000 --- a/doc/changelog.d/972.documentation.md +++ /dev/null @@ -1 +0,0 @@ -add Mechanical API link to Mechanical Scripting page \ No newline at end of file diff --git a/doc/changelog.d/974.fixed.md b/doc/changelog.d/974.fixed.md deleted file mode 100644 index 528e24028..000000000 --- a/doc/changelog.d/974.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Update embedding script tests \ No newline at end of file diff --git a/doc/changelog.d/977.maintenance.md b/doc/changelog.d/977.maintenance.md deleted file mode 100644 index c655a0905..000000000 --- a/doc/changelog.d/977.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -pre-commit automatic update \ No newline at end of file diff --git a/doc/changelog.d/979.added.md b/doc/changelog.d/979.added.md deleted file mode 100644 index babdcd2c1..000000000 --- a/doc/changelog.d/979.added.md +++ /dev/null @@ -1 +0,0 @@ -Version input type check \ No newline at end of file diff --git a/doc/changelog.d/980.added.md b/doc/changelog.d/980.added.md deleted file mode 100644 index feb48b43a..000000000 --- a/doc/changelog.d/980.added.md +++ /dev/null @@ -1 +0,0 @@ -Adding new method for connecting to Mechanical instance \ No newline at end of file diff --git a/doc/changelog.d/981.maintenance.md b/doc/changelog.d/981.maintenance.md deleted file mode 100644 index 47c9b5c59..000000000 --- a/doc/changelog.d/981.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -Bump grpcio from 1.67.1 to 1.68.0 in the core group \ No newline at end of file diff --git a/doc/changelog.d/982.maintenance.md b/doc/changelog.d/982.maintenance.md deleted file mode 100644 index 46cb383cb..000000000 --- a/doc/changelog.d/982.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -Bump the doc group with 2 updates \ No newline at end of file diff --git a/doc/changelog.d/984.maintenance.md b/doc/changelog.d/984.maintenance.md new file mode 100644 index 000000000..1972c6ecb --- /dev/null +++ b/doc/changelog.d/984.maintenance.md @@ -0,0 +1 @@ +update CHANGELOG for v0.11.10 \ No newline at end of file diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 4b0c798fa..e9cd696b9 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -9,6 +9,41 @@ This document contains the release notes for the project. .. towncrier release notes start +`0.11.10 `_ - 2024-11-18 +===================================================================================== + +Added +^^^^^ + +- Version input type check `#979 `_ +- Adding new method for connecting to Mechanical instance `#980 `_ + + +Fixed +^^^^^ + +- Update embedding script tests `#974 `_ + + +Documentation +^^^^^^^^^^^^^ + +- add Mechanical API link to Mechanical Scripting page `#972 `_ + + +Maintenance +^^^^^^^^^^^ + +- update CHANGELOG for v0.11.9 `#963 `_ +- Modify how job success is verified for CI/CD `#965 `_ +- Bump mikepenz/action-junit-report from 4 to 5 `#966 `_ +- Bump grpcio from 1.67.0 to 1.67.1 in the core group `#967 `_ +- Bump the doc group with 2 updates `#968 `_, `#982 `_ +- Bump pytest-cov from 5.0.0 to 6.0.0 `#969 `_ +- Update docs build action container `#971 `_ +- pre-commit automatic update `#977 `_ +- Bump grpcio from 1.67.1 to 1.68.0 in the core group `#981 `_ + `0.11.9 `_ - 2024-10-29 =================================================================================== From cb0043b5baa3574233f05b5a9e6a22e37fb27058 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Nov 2024 10:13:35 -0600 Subject: [PATCH 06/44] CHORE: Bump codecov/codecov-action from 4 to 5 (#983) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- .github/workflows/ci_cd.yml | 2 +- doc/changelog.d/983.maintenance.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 doc/changelog.d/983.maintenance.md diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 6b9774ebf..584b1920b 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -706,7 +706,7 @@ jobs: retention-days: 7 - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} with: diff --git a/doc/changelog.d/983.maintenance.md b/doc/changelog.d/983.maintenance.md new file mode 100644 index 000000000..17893824c --- /dev/null +++ b/doc/changelog.d/983.maintenance.md @@ -0,0 +1 @@ +Bump codecov/codecov-action from 4 to 5 \ No newline at end of file From af612b65c5383b44fbe315c4a355c42e27a15eae Mon Sep 17 00:00:00 2001 From: Dipin <26918585+dipinknair@users.noreply.github.com> Date: Wed, 20 Nov 2024 14:25:59 -0600 Subject: [PATCH 07/44] FEAT: Update private app data creation and add tests (#986) Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Co-authored-by: Kerry McAdams <58492561+klmcadams@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- doc/changelog.d/986.added.md | 1 + src/ansys/mechanical/core/embedding/app.py | 3 +- .../mechanical/core/embedding/appdata.py | 9 +- src/ansys/mechanical/core/run.py | 2 +- tests/embedding/test_app.py | 31 ----- tests/embedding/test_appdata.py | 112 ++++++++++++++++++ tests/scripts/run_embedded_app.py | 2 +- 7 files changed, 122 insertions(+), 38 deletions(-) create mode 100644 doc/changelog.d/986.added.md create mode 100644 tests/embedding/test_appdata.py diff --git a/doc/changelog.d/986.added.md b/doc/changelog.d/986.added.md new file mode 100644 index 000000000..c7236f515 --- /dev/null +++ b/doc/changelog.d/986.added.md @@ -0,0 +1 @@ +Update private app data creation and add tests \ No newline at end of file diff --git a/src/ansys/mechanical/core/embedding/app.py b/src/ansys/mechanical/core/embedding/app.py index 4efb85e48..30741d8b9 100644 --- a/src/ansys/mechanical/core/embedding/app.py +++ b/src/ansys/mechanical/core/embedding/app.py @@ -148,8 +148,9 @@ def __init__(self, db_file=None, private_appdata=False, **kwargs): configuration = kwargs.get("config", _get_default_addin_configuration()) if private_appdata: + copy_profile = kwargs.get("copy_profile", True) new_profile_name = f"PyMechanical-{os.getpid()}" - profile = UniqueUserProfile(new_profile_name) + profile = UniqueUserProfile(new_profile_name, copy_profile=copy_profile) profile.update_environment(os.environ) atexit.register(_cleanup_private_appdata, profile) diff --git a/src/ansys/mechanical/core/embedding/appdata.py b/src/ansys/mechanical/core/embedding/appdata.py index ad459769a..e484f3dca 100644 --- a/src/ansys/mechanical/core/embedding/appdata.py +++ b/src/ansys/mechanical/core/embedding/appdata.py @@ -31,26 +31,27 @@ class UniqueUserProfile: """Create Unique User Profile (for AppData).""" - def __init__(self, profile_name: str, dry_run: bool = False): + def __init__(self, profile_name: str, copy_profile: bool = True, dry_run: bool = False): """Initialize UniqueUserProfile class.""" self._default_profile = os.path.expanduser("~") self._location = os.path.join(self._default_profile, "PyMechanical-AppData", profile_name) self._dry_run = dry_run + self.copy_profile = copy_profile self.initialize() - def initialize(self, copy_profiles=True) -> None: + def initialize(self) -> None: """ Initialize the new profile location. Args: - copy_profiles (bool): If False, the copy_profiles method will be skipped. + copy_profile (bool): If False, the copy_profile method will be skipped. """ if self._dry_run: return if self.exists(): self.cleanup() self.mkdirs() - if copy_profiles: + if self.copy_profile: self.copy_profiles() def cleanup(self) -> None: diff --git a/src/ansys/mechanical/core/run.py b/src/ansys/mechanical/core/run.py index bb6b9e3ea..b29989b02 100644 --- a/src/ansys/mechanical/core/run.py +++ b/src/ansys/mechanical/core/run.py @@ -172,7 +172,7 @@ def _cli_impl( profile: UniqueUserProfile = None if private_appdata: new_profile_name = f"Mechanical-{os.getpid()}" - profile = UniqueUserProfile(new_profile_name, DRY_RUN) + profile = UniqueUserProfile(new_profile_name, dry_run=DRY_RUN) profile.update_environment(env) if not DRY_RUN: diff --git a/tests/embedding/test_app.py b/tests/embedding/test_app.py index fbf1809cd..9c37b8139 100644 --- a/tests/embedding/test_app.py +++ b/tests/embedding/test_app.py @@ -239,37 +239,6 @@ def test_warning_message(test_env, pytestconfig, run_subprocess, rootdir): assert warning, "UserWarning should appear in the output of the script" -@pytest.mark.embedding_scripts -@pytest.mark.python_env -def test_private_appdata(pytestconfig, run_subprocess, rootdir): - """Test embedded instance does not save ShowTriad using a test-scoped Python environment.""" - - version = pytestconfig.getoption("ansys_version") - embedded_py = os.path.join(rootdir, "tests", "scripts", "run_embedded_app.py") - - run_subprocess([sys.executable, embedded_py, version, "True", "Set"]) - process, stdout, stderr = run_subprocess([sys.executable, embedded_py, version, "True", "Run"]) - stdout = stdout.decode() - assert "ShowTriad value is True" in stdout - - -@pytest.mark.embedding_scripts -@pytest.mark.python_env -def test_normal_appdata(pytestconfig, run_subprocess, rootdir): - """Test embedded instance saves ShowTriad value using a test-scoped Python environment.""" - version = pytestconfig.getoption("ansys_version") - - embedded_py = os.path.join(rootdir, "tests", "scripts", "run_embedded_app.py") - - run_subprocess([sys.executable, embedded_py, version, "False", "Set"]) - process, stdout, stderr = run_subprocess([sys.executable, embedded_py, version, "False", "Run"]) - run_subprocess([sys.executable, embedded_py, version, "False", "Reset"]) - - stdout = stdout.decode() - # Assert ShowTriad was set to False for regular embedded session - assert "ShowTriad value is False" in stdout - - @pytest.mark.embedding_scripts def test_building_gallery(pytestconfig, run_subprocess, rootdir): """Test for building gallery check. diff --git a/tests/embedding/test_appdata.py b/tests/embedding/test_appdata.py new file mode 100644 index 000000000..088eba919 --- /dev/null +++ b/tests/embedding/test_appdata.py @@ -0,0 +1,112 @@ +# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Embedding tests related to app data""" + +import os +from pathlib import Path +import sys +from unittest import mock + +import pytest + +from ansys.mechanical.core.embedding.appdata import UniqueUserProfile + + +@pytest.mark.embedding_scripts +@pytest.mark.python_env +def test_private_appdata(pytestconfig, run_subprocess, rootdir): + """Test embedded instance does not save ShowTriad using a test-scoped Python environment.""" + + version = pytestconfig.getoption("ansys_version") + embedded_py = os.path.join(rootdir, "tests", "scripts", "run_embedded_app.py") + + run_subprocess([sys.executable, embedded_py, version, "True", "Set"]) + process, stdout, stderr = run_subprocess([sys.executable, embedded_py, version, "True", "Run"]) + stdout = stdout.decode() + assert "ShowTriad value is True" in stdout + + +@pytest.mark.embedding_scripts +@pytest.mark.python_env +def test_normal_appdata(pytestconfig, run_subprocess, rootdir): + """Test embedded instance saves ShowTriad value using a test-scoped Python environment.""" + version = pytestconfig.getoption("ansys_version") + + embedded_py = os.path.join(rootdir, "tests", "scripts", "run_embedded_app.py") + + run_subprocess([sys.executable, embedded_py, version, "False", "Set"]) + process, stdout, stderr = run_subprocess([sys.executable, embedded_py, version, "False", "Run"]) + run_subprocess([sys.executable, embedded_py, version, "False", "Reset"]) + + stdout = stdout.decode() + # Assert ShowTriad was set to False for regular embedded session + assert "ShowTriad value is False" in stdout + + +@pytest.mark.embedding +def test_uniqueprofile_creation(): + """Test profile is copied when copy_profile is ``True`` and is not copied when ``False``.""" + folder_to_check = Path("AppData") / "Local" / "Ansys" if os.name == "nt" else Path(".mw") + + # Create private app data without copying profiles + private_data2 = UniqueUserProfile(profile_name="test1", copy_profile=False) + assert not os.path.exists(os.path.join(private_data2.location, folder_to_check)) + + # Check if location is same with same profile name + private_data1 = UniqueUserProfile(profile_name="test1") + assert private_data1.location == private_data2.location + + # Check if folder exists after copying profiles + assert Path(private_data1.location / folder_to_check).exists() + + # Create new profile + private_data3 = UniqueUserProfile(profile_name="test2") + assert private_data2.location != private_data3.location + + +@pytest.mark.embedding +def test_uniqueprofile_env(): + """Test the environment is correctly updated for the profile based on operating system.""" + profile = UniqueUserProfile("test_env") + env = {} + platforms = ["win32", "linux"] + + for platform in platforms: + with mock.patch.object(sys, "platform", platform): + profile.update_environment(env) + + if platform == "win32": + env["USERPROFILE"] = profile.location + env["APPDATA"] = Path(profile.location) / "AppData" / "Roaming" + env["LOCALAPPDATA"] = Path(profile.location) / "AppData" / "Local" + env["TMP"] = Path(profile.location) / "AppData" / "Local" / "Temp" + env["TEMP"] = Path(profile.location) / "AppData" / "Local " / "Temp" + else: + env["HOME"] = profile.location + + +@pytest.mark.embedding +def test_uniqueprofile_dryrun(): + """Test the profile is not copied during dry runs.""" + profile = UniqueUserProfile("test_dry_run", dry_run=True) + assert not Path(profile.location).exists() diff --git a/tests/scripts/run_embedded_app.py b/tests/scripts/run_embedded_app.py index 4b41e7ed7..43acf3ff4 100644 --- a/tests/scripts/run_embedded_app.py +++ b/tests/scripts/run_embedded_app.py @@ -32,7 +32,7 @@ def launch_app(version, private_appdata): """Launch embedded instance of app.""" # Configuration.configure(level=logging.DEBUG, to_stdout=True, base_directory=None) - app = pymechanical.App(version=version, private_appdata=private_appdata) + app = pymechanical.App(version=version, private_appdata=private_appdata, copy_profile=True) return app From 919ffd2685040c85cd36273416506a7bc98fab82 Mon Sep 17 00:00:00 2001 From: Dipin <26918585+dipinknair@users.noreply.github.com> Date: Thu, 21 Nov 2024 10:00:13 -0600 Subject: [PATCH 08/44] FEAT: Add tests for transaction (#985) Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/985.added.md | 1 + pyproject.toml | 2 ++ tests/embedding/test_app.py | 8 ++++---- tests/embedding/test_globals.py | 12 ++++++++++++ tests/scripts/pythonnet_warning.py | 30 ++++++++++++++++++++++++++++++ 5 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 doc/changelog.d/985.added.md create mode 100644 tests/scripts/pythonnet_warning.py diff --git a/doc/changelog.d/985.added.md b/doc/changelog.d/985.added.md new file mode 100644 index 000000000..5806c43fe --- /dev/null +++ b/doc/changelog.d/985.added.md @@ -0,0 +1 @@ +Add tests for transaction \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index f3bfd937c..ef86a4db6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -146,6 +146,8 @@ markers = [ "windows_only: tests that run if the testing platform is on Windows", "linux_only: tests that run if the testing platform is on Linux", "cli: tests for the Command Line Interface", + "embedding_backgroundapp: tests for the BackgroundApp", + "embedding_logging: tests for the logging with Embedded App", ] xfail_strict = true diff --git a/tests/embedding/test_app.py b/tests/embedding/test_app.py index 9c37b8139..f4c202278 100644 --- a/tests/embedding/test_app.py +++ b/tests/embedding/test_app.py @@ -225,17 +225,17 @@ def test_warning_message(test_env, pytestconfig, run_subprocess, rootdir): # Install pythonnet subprocess.check_call([test_env.python, "-m", "pip", "install", "pythonnet"], env=test_env.env) - # Run embedded instance in virtual env with pythonnet installed - embedded_py = os.path.join(rootdir, "tests", "scripts", "run_embedded_app.py") + # Initialize with pythonnet + embedded_pythonnet_py = os.path.join(rootdir, "tests", "scripts", "pythonnet_warning.py") process, stdout, stderr = run_subprocess( - [test_env.python, embedded_py, pytestconfig.getoption("ansys_version")] + [test_env.python, embedded_pythonnet_py, pytestconfig.getoption("ansys_version")] ) # If UserWarning & pythonnet are in the stderr output, set warning to True. # Otherwise, set warning to False warning = True if "UserWarning" and "pythonnet" in stderr.decode() else False - # Assert warning message appears for embedded app + # # Assert warning message appears for embedded app assert warning, "UserWarning should appear in the output of the script" diff --git a/tests/embedding/test_globals.py b/tests/embedding/test_globals.py index 4f84186a8..5893c7e13 100644 --- a/tests/embedding/test_globals.py +++ b/tests/embedding/test_globals.py @@ -24,6 +24,7 @@ import pytest from ansys.mechanical.core import global_variables +from ansys.mechanical.core.embedding.imports import Transaction @pytest.mark.embedding @@ -50,3 +51,14 @@ def test_global_variables(embedded_app): globals_dict = global_variables(embedded_app, True) for attribute in attributes: assert attribute in globals_dict + + +@pytest.mark.embedding +def test_global_variable_transaction(embedded_app): + embedded_app.update_globals(globals()) + project_name = DataModel.Project.Name + assert project_name == "Project" + with Transaction(): + DataModel.Project.Name = "New Project" + project_name = DataModel.Project.Name + assert project_name == "New Project" diff --git a/tests/scripts/pythonnet_warning.py b/tests/scripts/pythonnet_warning.py new file mode 100644 index 000000000..207f5bc52 --- /dev/null +++ b/tests/scripts/pythonnet_warning.py @@ -0,0 +1,30 @@ +# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Test script for checking pythonnet warning.""" +import sys + +from ansys.mechanical.core.embedding import initializer + +if __name__ == "__main__": + version = int(sys.argv[1]) + initializer.initialize(version) From 8f7301dd7444058cdbd669f65b1b5cd0cd68b645 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 19:30:41 +0000 Subject: [PATCH 09/44] CHORE: Bump ansys-sphinx-theme[autoapi] from 1.2.1 to 1.2.2 in the doc group (#988) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/988.maintenance.md | 1 + pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 doc/changelog.d/988.maintenance.md diff --git a/doc/changelog.d/988.maintenance.md b/doc/changelog.d/988.maintenance.md new file mode 100644 index 000000000..83a8797ce --- /dev/null +++ b/doc/changelog.d/988.maintenance.md @@ -0,0 +1 @@ +Bump ansys-sphinx-theme[autoapi] from 1.2.1 to 1.2.2 in the doc group \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index ef86a4db6..6d1a0265d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ tests = [ ] doc = [ "sphinx==8.1.3", - "ansys-sphinx-theme[autoapi]==1.2.1", + "ansys-sphinx-theme[autoapi]==1.2.2", "grpcio==1.68.0", "imageio-ffmpeg==0.5.1", "imageio==2.36.0", From 358e6d847fc581ea8b76cdb436ac2a46563db22f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 20:26:58 +0000 Subject: [PATCH 10/44] CHORE: Bump grpcio from 1.68.0 to 1.68.1 in the core group (#990) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/990.maintenance.md | 1 + pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 doc/changelog.d/990.maintenance.md diff --git a/doc/changelog.d/990.maintenance.md b/doc/changelog.d/990.maintenance.md new file mode 100644 index 000000000..cb92f6c00 --- /dev/null +++ b/doc/changelog.d/990.maintenance.md @@ -0,0 +1 @@ +Bump grpcio from 1.68.0 to 1.68.1 in the core group \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 6d1a0265d..1ff392498 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,7 @@ tests = [ doc = [ "sphinx==8.1.3", "ansys-sphinx-theme[autoapi]==1.2.2", - "grpcio==1.68.0", + "grpcio==1.68.1", "imageio-ffmpeg==0.5.1", "imageio==2.36.0", "jupyter_sphinx==0.5.3", From 0ee63bb86e9615ccc89e752d435905125166d0dd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 20:41:32 +0000 Subject: [PATCH 11/44] CHORE: Bump pytest from 8.3.3 to 8.3.4 in the tests group (#991) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/991.maintenance.md | 1 + pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 doc/changelog.d/991.maintenance.md diff --git a/doc/changelog.d/991.maintenance.md b/doc/changelog.d/991.maintenance.md new file mode 100644 index 000000000..0798d65b6 --- /dev/null +++ b/doc/changelog.d/991.maintenance.md @@ -0,0 +1 @@ +Bump pytest from 8.3.3 to 8.3.4 in the tests group \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 1ff392498..2a5018772 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ Changelog = "https://mechanical.docs.pyansys.com/version/stable/changelog.html" [project.optional-dependencies] tests = [ - "pytest==8.3.3", + "pytest==8.3.4", "pytest-cov==6.0.0", "pytest-print==1.0.2", "psutil==6.1.0" From c91f57dbac8eedce4a02e5dde0a9b8231603ddf9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 20:58:15 +0000 Subject: [PATCH 12/44] CHORE: Bump the doc group with 2 updates (#992) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/992.maintenance.md | 1 + pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 doc/changelog.d/992.maintenance.md diff --git a/doc/changelog.d/992.maintenance.md b/doc/changelog.d/992.maintenance.md new file mode 100644 index 000000000..46cb383cb --- /dev/null +++ b/doc/changelog.d/992.maintenance.md @@ -0,0 +1 @@ +Bump the doc group with 2 updates \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 2a5018772..518675e5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,10 +60,10 @@ doc = [ "ansys-sphinx-theme[autoapi]==1.2.2", "grpcio==1.68.1", "imageio-ffmpeg==0.5.1", - "imageio==2.36.0", + "imageio==2.36.1", "jupyter_sphinx==0.5.3", "jupyterlab>=3.2.8", - "matplotlib==3.9.2", + "matplotlib==3.9.3", "numpy==2.1.3", "numpydoc==1.8.0", "pandas==2.2.3", From d681de91be863e8c8c1370aa5651e3708292ea57 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 3 Dec 2024 07:35:06 -0600 Subject: [PATCH 13/44] chore: pre-commit automatic update (#993) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- doc/changelog.d/993.maintenance.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 doc/changelog.d/993.maintenance.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e333711d1..a45dce36e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -76,6 +76,6 @@ repos: - id: check-added-large-files - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.29.4 + rev: 0.30.0 hooks: - id: check-github-workflows \ No newline at end of file diff --git a/doc/changelog.d/993.maintenance.md b/doc/changelog.d/993.maintenance.md new file mode 100644 index 000000000..c655a0905 --- /dev/null +++ b/doc/changelog.d/993.maintenance.md @@ -0,0 +1 @@ +pre-commit automatic update \ No newline at end of file From 4b809be8ded094de9b9eaf52ca5946ecb6e3a96c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Dec 2024 14:52:19 +0000 Subject: [PATCH 14/44] CHORE: Bump the doc group with 2 updates (#999) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/999.maintenance.md | 1 + pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 doc/changelog.d/999.maintenance.md diff --git a/doc/changelog.d/999.maintenance.md b/doc/changelog.d/999.maintenance.md new file mode 100644 index 000000000..46cb383cb --- /dev/null +++ b/doc/changelog.d/999.maintenance.md @@ -0,0 +1 @@ +Bump the doc group with 2 updates \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 518675e5d..1cb5e535d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,14 +57,14 @@ tests = [ ] doc = [ "sphinx==8.1.3", - "ansys-sphinx-theme[autoapi]==1.2.2", + "ansys-sphinx-theme[autoapi]==1.2.3", "grpcio==1.68.1", "imageio-ffmpeg==0.5.1", "imageio==2.36.1", "jupyter_sphinx==0.5.3", "jupyterlab>=3.2.8", "matplotlib==3.9.3", - "numpy==2.1.3", + "numpy==2.2.0", "numpydoc==1.8.0", "pandas==2.2.3", "panel==1.5.4", From 8517e6c7ec08cd0e5dcf55e3423877b4029958cc Mon Sep 17 00:00:00 2001 From: Dipin <26918585+dipinknair@users.noreply.github.com> Date: Wed, 11 Dec 2024 13:16:19 -0600 Subject: [PATCH 15/44] DOCS: Update docs with new api (#1000) Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/1000.documentation.md | 1 + .../getting_started/running_mechanical.rst | 29 +++++++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 doc/changelog.d/1000.documentation.md diff --git a/doc/changelog.d/1000.documentation.md b/doc/changelog.d/1000.documentation.md new file mode 100644 index 000000000..c8dc88271 --- /dev/null +++ b/doc/changelog.d/1000.documentation.md @@ -0,0 +1 @@ +Update docs with new api \ No newline at end of file diff --git a/doc/source/getting_started/running_mechanical.rst b/doc/source/getting_started/running_mechanical.rst index 84d58dee6..ac8c2773e 100644 --- a/doc/source/getting_started/running_mechanical.rst +++ b/doc/source/getting_started/running_mechanical.rst @@ -124,6 +124,17 @@ default port (``10000``), you would use this code to connect to it with this cod mechanical = Mechanical() +Alternatively, you can use the +`connect_to_mechanical() <../api/ansys/mechanical/core/mechanical/index.html#mechanical.connect_to_mechanical>`_ +for same functionality. + +.. code:: python + + from ansys.mechanical.core import connect_to_mechanical + + mechanical = connect_to_mechanical() + + Now assume that a remote instance of Mechanical has been started in server mode. To connect to the computer on your local area network that is running Mechanical, you can use either an IP address and port or a hostname and port. @@ -138,6 +149,12 @@ You would connect to it with this code: mechanical = Mechanical("192.168.0.1", port=10000) +or + +.. code:: python + + mechanical = connect_to_mechanical("192.168.0.1", port=10000) + **Hostname and port** Assume that Mechanical is running remotely at hostname ``myremotemachine`` on port ``10000``. @@ -148,6 +165,12 @@ You would connect to it with this code: mechanical = Mechanical("myremotemachine", port=10000) +or + +.. code:: python + + mechanical = connect_to_mechanical("myremotemachine", port=10000) + Launching issues ---------------- @@ -204,9 +227,9 @@ Open a terminal and run the following command: If the preceding command for your operating system doesn't launch Mechanical, you might have a variety of issues, including: - - License server setup - - Running behind a VPN - - Missing dependencies +- License server setup +- Running behind a VPN +- Missing dependencies Embed a Mechanical instance --------------------------- From c416441a64eed138c3cd941fdbcc9f981fca6e74 Mon Sep 17 00:00:00 2001 From: Dipin <26918585+dipinknair@users.noreply.github.com> Date: Fri, 13 Dec 2024 17:21:02 -0600 Subject: [PATCH 16/44] FEAT: Update docstring and ``App.save_as()`` (#1001) Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/1001.added.md | 1 + src/ansys/mechanical/core/embedding/app.py | 114 +++++++++++++++------ 2 files changed, 82 insertions(+), 33 deletions(-) create mode 100644 doc/changelog.d/1001.added.md diff --git a/doc/changelog.d/1001.added.md b/doc/changelog.d/1001.added.md new file mode 100644 index 000000000..b592af031 --- /dev/null +++ b/doc/changelog.d/1001.added.md @@ -0,0 +1 @@ +Update docstring and ``App.save_as()`` \ No newline at end of file diff --git a/src/ansys/mechanical/core/embedding/app.py b/src/ansys/mechanical/core/embedding/app.py index 30741d8b9..9c7c6a108 100644 --- a/src/ansys/mechanical/core/embedding/app.py +++ b/src/ansys/mechanical/core/embedding/app.py @@ -112,18 +112,45 @@ def __setattr__(self, attr, value): class App: - """Mechanical embedding Application.""" + """Mechanical embedding Application. - def __init__(self, db_file=None, private_appdata=False, **kwargs): - """Construct an instance of the mechanical Application. + Parameters + ---------- + db_file : str, optional + Path to a mechanical database file (.mechdat or .mechdb). + version : int, optional + Version number of the Mechanical application. + private_appdata : bool, optional + Setting for a temporary AppData directory. Default is False. + Enables running parallel instances of Mechanical. + config : AddinConfiguration, optional + Configuration for addins. By default "Mechanical" is used and ACT Addins are disabled. + copy_profile : bool, optional + Whether to copy the user profile when private_appdata is True. Default is True. - db_file is an optional path to a mechanical database file (.mechdat or .mechdb) - you may set a version number with the `version` keyword argument. + Examples + -------- + Create App with Mechanical project file and version: - private_appdata is an optional setting for a temporary AppData directory. - By default, private_appdata is False. This enables you to run parallel - instances of Mechanical. - """ + >>> from ansys.mechanical.core import App + >>> app = App(db_file="path/to/file.mechdat", version=241, pri) + + + Disable copying the user profile when private appdata is enabled + + >>> app = App(private_appdata=True, copy_profile=False) + + Create App with "Mechanical" configuration and no ACT Addins + + >>> from ansys.mechanical.core.embedding import AddinConfiguration + >>> from ansys.mechanical.core import App + >>> config = AddinConfiguration("Mechanical") + >>> config.no_act_addins = True + >>> app = App(config=config) + """ + + def __init__(self, db_file=None, private_appdata=False, **kwargs): + """Construct an instance of the mechanical Application.""" global INSTANCES from ansys.mechanical.core import BUILDING_GALLERY @@ -167,8 +194,6 @@ def __init__(self, db_file=None, private_appdata=False, **kwargs): def __repr__(self): """Get the product info.""" - if self._version < 232: # pragma: no cover - return "Ansys Mechanical" import clr clr.AddReference("Ansys.Mechanical.Application") @@ -204,18 +229,27 @@ def save(self, path=None): self.DataModel.Project.Save() def save_as(self, path: str, overwrite: bool = False): - """Save the project as a new file. + """ + Save the project as a new file. - If the `overwrite` flag is enabled, the current saved file is temporarily moved - to a backup location. The new file is then saved in its place. If the process fails, - the backup file is restored to its original location. + If the `overwrite` flag is enabled, the current saved file is replaced with the new file. Parameters ---------- - path: int, optional - The path where file needs to be saved. - overwrite: bool, optional + path : str + The path where the file needs to be saved. + overwrite : bool, optional Whether the file should be overwritten if it already exists (default is False). + + Raises + ------ + Exception + If the file already exists at the specified path and `overwrite` is False. + + Notes + ----- + For version 232, if `overwrite` is True, the existing file and its associated directory + (if any) will be removed before saving the new file. """ if not os.path.exists(path): self.DataModel.Project.SaveAs(path) @@ -226,17 +260,19 @@ def save_as(self, path: str, overwrite: bool = False): f"File already exists in {path}, Use ``overwrite`` flag to " "replace the existing file." ) - - file_name = os.path.basename(path) - file_dir = os.path.dirname(path) - associated_dir = os.path.join(file_dir, os.path.splitext(file_name)[0] + "_Mech_Files") - - # Remove existing files and associated folder - os.remove(path) - if os.path.exists(associated_dir): - shutil.rmtree(associated_dir) - # Save the new file - self.DataModel.Project.SaveAs(path) + if self.version < 241: # pragma: no cover + file_name = os.path.basename(path) + file_dir = os.path.dirname(path) + associated_dir = os.path.join(file_dir, os.path.splitext(file_name)[0] + "_Mech_Files") + + # Remove existing files and associated folder + os.remove(path) + if os.path.exists(associated_dir): + shutil.rmtree(associated_dir) + # Save the new file + self.DataModel.Project.SaveAs(path) + else: + self.DataModel.Project.SaveAs(path, overwrite) def launch_gui(self, delete_tmp_on_close: bool = True, dry_run: bool = False): """Launch the GUI.""" @@ -317,6 +353,13 @@ def plot(self) -> None: Requires installation using the viz option. E.g. pip install ansys-mechanical-core[viz] + + Examples + -------- + >>> from ansys.mechanical.core import App + >>> app = App() + >>> app.open("path/to/file.mechdat") + >>> app.plot() """ _plotter = self.plotter() @@ -421,15 +464,20 @@ def _on_workbench_ready(self, sender, args) -> None: def update_globals( self, globals_dict: typing.Dict[str, typing.Any], enums: bool = True ) -> None: - """Use to update globals variables. + """Update global variables. - When scripting inside Mechanical, the Mechanical UI will automatically - set global variables in python. PyMechanical can not do that automatically, + When scripting inside Mechanical, the Mechanical UI automatically + sets global variables in Python. PyMechanical cannot do that automatically, but this method can be used. - `app.update_globals(globals())` By default, all enums will be imported too. To avoid including enums, set the `enums` argument to False. + + Examples + -------- + >>> from ansys.mechanical.core import App + >>> app = App() + >>> app.update_globals(globals()) """ self._updated_scopes.append(globals_dict) globals_dict.update(global_variables(self, enums)) From 427077de50ff755d5839fdc38afa519ec6be333a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 20:09:06 +0000 Subject: [PATCH 17/44] CHORE: Bump clr-loader from 0.2.6 to 0.2.7.post0 in the core group (#1003) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/1003.maintenance.md | 1 + pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 doc/changelog.d/1003.maintenance.md diff --git a/doc/changelog.d/1003.maintenance.md b/doc/changelog.d/1003.maintenance.md new file mode 100644 index 000000000..da9752730 --- /dev/null +++ b/doc/changelog.d/1003.maintenance.md @@ -0,0 +1 @@ +Bump clr-loader from 0.2.6 to 0.2.7.post0 in the core group \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 1cb5e535d..1297b982c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ "ansys-tools-path>=0.3.1", "appdirs>=1.4.0", "click>=8.1.3", # for CLI interface - "clr-loader==0.2.6", + "clr-loader==0.2.7.post0", "grpcio>=1.30.0", "protobuf>=3.12.2,<6", "psutil==6.1.0", From 033053b4e02c90d605799fbbcccd062cf1637d6e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 20:25:19 +0000 Subject: [PATCH 18/44] CHORE: Bump matplotlib from 3.9.3 to 3.10.0 in the doc group (#1004) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/1004.maintenance.md | 1 + pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 doc/changelog.d/1004.maintenance.md diff --git a/doc/changelog.d/1004.maintenance.md b/doc/changelog.d/1004.maintenance.md new file mode 100644 index 000000000..dbd883510 --- /dev/null +++ b/doc/changelog.d/1004.maintenance.md @@ -0,0 +1 @@ +Bump matplotlib from 3.9.3 to 3.10.0 in the doc group \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 1297b982c..de9d48da7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ doc = [ "imageio==2.36.1", "jupyter_sphinx==0.5.3", "jupyterlab>=3.2.8", - "matplotlib==3.9.3", + "matplotlib==3.10.0", "numpy==2.2.0", "numpydoc==1.8.0", "pandas==2.2.3", From 8a2f4f51d183deea2573d2cad4f96ff5777fb808 Mon Sep 17 00:00:00 2001 From: Dipin <26918585+dipinknair@users.noreply.github.com> Date: Fri, 20 Dec 2024 13:38:49 -0800 Subject: [PATCH 19/44] FEAT: Update object state for `print_tree()` (#1005) Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/1005.added.md | 1 + src/ansys/mechanical/core/embedding/app.py | 31 ++++++++++++++-------- tests/embedding/test_app.py | 7 +++++ 3 files changed, 28 insertions(+), 11 deletions(-) create mode 100644 doc/changelog.d/1005.added.md diff --git a/doc/changelog.d/1005.added.md b/doc/changelog.d/1005.added.md new file mode 100644 index 000000000..142ae433d --- /dev/null +++ b/doc/changelog.d/1005.added.md @@ -0,0 +1 @@ +Update object state for `print_tree()` \ No newline at end of file diff --git a/src/ansys/mechanical/core/embedding/app.py b/src/ansys/mechanical/core/embedding/app.py index 9c7c6a108..a91512c45 100644 --- a/src/ansys/mechanical/core/embedding/app.py +++ b/src/ansys/mechanical/core/embedding/app.py @@ -512,6 +512,15 @@ def _print_tree(self, node, max_lines, lines_count, indentation): node_name = node.Name if hasattr(node, "Suppressed") and node.Suppressed is True: node_name += " (Suppressed)" + if hasattr(node, "ObjectState"): + if str(node.ObjectState) == "UnderDefined": + node_name += " (?)" + elif str(node.ObjectState) == "Solved" or str(node.ObjectState) == "FullyDefined": + node_name += " (✓)" + elif str(node.ObjectState) == "NotSolved" or str(node.ObjectState) == "Obsolete": + node_name += " (⚡︎)" + elif str(node.ObjectState) == "SolveFailed": + node_name += " (✕)" print(f"{indentation}├── {node_name}") lines_count += 1 @@ -545,24 +554,24 @@ def print_tree(self, node=None, max_lines=80, lines_count=0, indentation=""): Examples -------- - >>> import ansys.mechanical.core as mech - >>> app = mech.App() + >>> from ansys.mechanical.core import App + >>> app = App() >>> app.update_globals(globals()) >>> app.print_tree() ... ├── Project ... | ├── Model - ... | | ├── Geometry Imports - ... | | ├── Geometry - ... | | ├── Materials - ... | | ├── Coordinate Systems - ... | | | ├── Global Coordinate System - ... | | ├── Remote Points - ... | | ├── Mesh + ... | | ├── Geometry Imports (⚡︎) + ... | | ├── Geometry (?) + ... | | ├── Materials (✓) + ... | | ├── Coordinate Systems (✓) + ... | | | ├── Global Coordinate System (✓) + ... | | ├── Remote Points (✓) + ... | | ├── Mesh (?) >>> app.print_tree(Model, 3) ... ├── Model - ... | ├── Geometry Imports - ... | ├── Geometry + ... | ├── Geometry Imports (⚡︎) + ... | ├── Geometry (?) ... ... truncating after 3 lines >>> app.print_tree(max_lines=2) diff --git a/tests/embedding/test_app.py b/tests/embedding/test_app.py index f4c202278..7f85030b6 100644 --- a/tests/embedding/test_app.py +++ b/tests/embedding/test_app.py @@ -132,6 +132,13 @@ def test_app_print_tree(embedded_app, capsys, assets): with pytest.raises(AttributeError): embedded_app.print_tree(DataModel) + Modal = Model.AddModalAnalysis() + Modal.Solution.Solve(True) + embedded_app.print_tree() + captured = capsys.readouterr() + printed_output = captured.out.strip() + assert all(symbol in printed_output for symbol in ["?", "⚡︎", "✕", "✓"]) + @pytest.mark.embedding def test_app_poster(embedded_app): From a8f1e8ddf6ac5409a525a146ab3a3abde8e7305d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 19:38:14 +0000 Subject: [PATCH 20/44] CHORE: Bump psutil from 6.1.0 to 6.1.1 (#1009) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/1009.maintenance.md | 1 + pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 doc/changelog.d/1009.maintenance.md diff --git a/doc/changelog.d/1009.maintenance.md b/doc/changelog.d/1009.maintenance.md new file mode 100644 index 000000000..688c442a5 --- /dev/null +++ b/doc/changelog.d/1009.maintenance.md @@ -0,0 +1 @@ +Bump psutil from 6.1.0 to 6.1.1 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index de9d48da7..5fc9931ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dependencies = [ "clr-loader==0.2.7.post0", "grpcio>=1.30.0", "protobuf>=3.12.2,<6", - "psutil==6.1.0", + "psutil==6.1.1", "tqdm>=4.45.0", "requests>=2,<3", ] @@ -53,7 +53,7 @@ tests = [ "pytest==8.3.4", "pytest-cov==6.0.0", "pytest-print==1.0.2", - "psutil==6.1.0" + "psutil==6.1.1" ] doc = [ "sphinx==8.1.3", From a78f5e3c6711011f2b0deaae77accc313e38f4e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 19:48:47 +0000 Subject: [PATCH 21/44] CHORE: Bump the doc group with 3 updates (#1008) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Co-authored-by: Dipin <26918585+dipinknair@users.noreply.github.com> --- doc/changelog.d/1008.maintenance.md | 1 + pyproject.toml | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 doc/changelog.d/1008.maintenance.md diff --git a/doc/changelog.d/1008.maintenance.md b/doc/changelog.d/1008.maintenance.md new file mode 100644 index 000000000..416b898e2 --- /dev/null +++ b/doc/changelog.d/1008.maintenance.md @@ -0,0 +1 @@ +Bump the doc group with 3 updates \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 5fc9931ef..2250ff4ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,17 +57,17 @@ tests = [ ] doc = [ "sphinx==8.1.3", - "ansys-sphinx-theme[autoapi]==1.2.3", + "ansys-sphinx-theme[autoapi]==1.2.4", "grpcio==1.68.1", "imageio-ffmpeg==0.5.1", "imageio==2.36.1", "jupyter_sphinx==0.5.3", "jupyterlab>=3.2.8", "matplotlib==3.10.0", - "numpy==2.2.0", + "numpy==2.2.1", "numpydoc==1.8.0", "pandas==2.2.3", - "panel==1.5.4", + "panel==1.5.5", "plotly==5.24.1", "pypandoc==1.14", "pytest-sphinx==0.6.3", From 6b95f2a2f57cdb5ac6564aaa49dee4527e05f735 Mon Sep 17 00:00:00 2001 From: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> Date: Thu, 2 Jan 2025 14:15:40 +0100 Subject: [PATCH 22/44] REFACTOR: Remove f-string without placeholders and specify exception type. (#1011) Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/1011.miscellaneous.md | 1 + src/ansys/mechanical/core/embedding/app.py | 2 +- .../mechanical/core/embedding/app_libraries.py | 2 +- .../mechanical/core/embedding/initializer.py | 4 ++-- .../core/embedding/logger/__init__.py | 4 ++-- .../core/embedding/logger/windows_api.py | 2 +- src/ansys/mechanical/core/mechanical.py | 17 +++++++---------- src/ansys/mechanical/core/misc.py | 2 +- src/ansys/mechanical/core/pool.py | 6 +++--- tests/conftest.py | 6 +++--- 10 files changed, 22 insertions(+), 24 deletions(-) create mode 100644 doc/changelog.d/1011.miscellaneous.md diff --git a/doc/changelog.d/1011.miscellaneous.md b/doc/changelog.d/1011.miscellaneous.md new file mode 100644 index 000000000..975814ed1 --- /dev/null +++ b/doc/changelog.d/1011.miscellaneous.md @@ -0,0 +1 @@ +Remove f-string without placeholders and specify exception type. \ No newline at end of file diff --git a/src/ansys/mechanical/core/embedding/app.py b/src/ansys/mechanical/core/embedding/app.py index a91512c45..a839eab06 100644 --- a/src/ansys/mechanical/core/embedding/app.py +++ b/src/ansys/mechanical/core/embedding/app.py @@ -47,7 +47,7 @@ HAS_ANSYS_VIZ = True """Whether or not PyVista exists.""" -except: +except ImportError: HAS_ANSYS_VIZ = False diff --git a/src/ansys/mechanical/core/embedding/app_libraries.py b/src/ansys/mechanical/core/embedding/app_libraries.py index eb8bff5df..69a75e88c 100644 --- a/src/ansys/mechanical/core/embedding/app_libraries.py +++ b/src/ansys/mechanical/core/embedding/app_libraries.py @@ -74,7 +74,7 @@ def add_mechanical_python_libraries(app_or_version): elif isinstance(app_or_version, App): installdir.append(os.environ[f"AWP_ROOT{app_or_version.version}"]) else: - raise ValueError(f"Invalid input: expected an integer (version) or an instance of App().") + raise ValueError("Invalid input: expected an integer (version) or an instance of App().") location = os.path.join(installdir[0], "Addins", "ACT", "libraries", "Mechanical") sys.path.append(location) diff --git a/src/ansys/mechanical/core/embedding/initializer.py b/src/ansys/mechanical/core/embedding/initializer.py index 05c1ead6e..10cd673bb 100644 --- a/src/ansys/mechanical/core/embedding/initializer.py +++ b/src/ansys/mechanical/core/embedding/initializer.py @@ -138,7 +138,7 @@ def __is_lib_loaded(libname: str): # pragma: no cover RTLD_NOLOAD = 4 try: ctypes.CDLL(libname, RTLD_NOLOAD) - except: + except OSError: return False return True @@ -174,7 +174,7 @@ def initialize(version: int = None): ) return - if version == None: + if version is None: version = _get_latest_default_version() version = __check_for_supported_version(version=version) diff --git a/src/ansys/mechanical/core/embedding/logger/__init__.py b/src/ansys/mechanical/core/embedding/logger/__init__.py index bf8133d48..c96922631 100644 --- a/src/ansys/mechanical/core/embedding/logger/__init__.py +++ b/src/ansys/mechanical/core/embedding/logger/__init__.py @@ -144,14 +144,14 @@ def set_log_level(cls, level: int) -> None: @classmethod def set_log_directory(cls, value: str) -> None: """Configure logging to write to a directory.""" - if value == None: + if value is None: return _get_backend().set_directory(value) @classmethod def set_log_base_directory(cls, directory: str) -> None: """Configure logging to write in a time-stamped subfolder in this directory.""" - if directory == None: + if directory is None: return _get_backend().set_base_directory(directory) diff --git a/src/ansys/mechanical/core/embedding/logger/windows_api.py b/src/ansys/mechanical/core/embedding/logger/windows_api.py index 8e5bcbc3a..a301b42e0 100644 --- a/src/ansys/mechanical/core/embedding/logger/windows_api.py +++ b/src/ansys/mechanical/core/embedding/logger/windows_api.py @@ -41,7 +41,7 @@ def _get_logger(): import Ansys return Ansys.Common.WB1ManagedUtils.Logger - except: + except (ImportError, RuntimeError): raise Exception("Logging cannot be used until after Mechanical embedding is initialized.") diff --git a/src/ansys/mechanical/core/mechanical.py b/src/ansys/mechanical/core/mechanical.py index 55c7bc5fd..18652eaac 100644 --- a/src/ansys/mechanical/core/mechanical.py +++ b/src/ansys/mechanical/core/mechanical.py @@ -444,14 +444,14 @@ def __init__( self._disable_logging = False if self._local: - self.log_info(f"Mechanical connection is treated as local.") + self.log_info("Mechanical connection is treated as local.") else: - self.log_info(f"Mechanical connection is treated as remote.") + self.log_info("Mechanical connection is treated as remote.") # connect and validate to the channel self._multi_connect(timeout=timeout) - self.log_info("Mechanical is ready to accept grpc calls") + self.log_info("Mechanical is ready to accept grpc calls.") def __del__(self): # pragma: no cover """Clean up on exit.""" @@ -480,9 +480,8 @@ def version(self) -> str: >>> mechanical.version '242' - """ - if self._version == None: + if self._version is None: try: self._disable_logging = True script = ( @@ -537,7 +536,6 @@ def _multi_connect(self, n_attempts=5, timeout=60): timeout : float, optional Maximum allowable time in seconds for establishing a connection. The default is ``60``. - """ # This prevents a single failed connection from blocking other attempts connected = False @@ -1399,7 +1397,7 @@ def download( if chunk_size > 4 * 1024 * 1024: # 4MB raise ValueError( - f"Chunk sizes bigger than 4 MB can generate unstable behaviour in PyMechanical. " + "Chunk sizes bigger than 4 MB can generate unstable behaviour in PyMechanical. " "Decrease the ``chunk_size`` value." ) @@ -1518,8 +1516,8 @@ def save_chunks_to_file(self, responses, filename, progress_bar=False, target_na if progress_bar: if not _HAS_TQDM: # pragma: no cover raise ModuleNotFoundError( - f"To use the keyword argument 'progress_bar', you need to have installed " - f"the 'tqdm' package.To avoid this message you can set 'progress_bar=False'." + "To use the keyword argument 'progress_bar', you need to have installed " + "the 'tqdm' package.To avoid this message you can set 'progress_bar=False'." ) file_size = 0 @@ -1572,7 +1570,6 @@ def download_project(self, extensions=None, target_dir=None, progress_bar=False) Download all the files in the project. >>> local_file_path_list = mechanical.download_project() - """ destination_directory = target_dir.rstrip("\\/") diff --git a/src/ansys/mechanical/core/misc.py b/src/ansys/mechanical/core/misc.py index 84d45789d..0469b81e5 100644 --- a/src/ansys/mechanical/core/misc.py +++ b/src/ansys/mechanical/core/misc.py @@ -150,7 +150,7 @@ def check_valid_start_instance(start_instance): if start_instance.lower() not in ["true", "false"]: raise ValueError( - f"The value for 'start_instance' should be 'True' or 'False' (case insensitive)." + "The value for 'start_instance' should be 'True' or 'False' (case insensitive)." ) return start_instance.lower() == "true" diff --git a/src/ansys/mechanical/core/pool.py b/src/ansys/mechanical/core/pool.py index 0bad20c81..5f58d8882 100644 --- a/src/ansys/mechanical/core/pool.py +++ b/src/ansys/mechanical/core/pool.py @@ -342,8 +342,8 @@ def map( if progress_bar: if not _HAS_TQDM: # pragma: no cover raise ModuleNotFoundError( - f"To use the keyword argument 'progress_bar', you must have installed " - f"the 'tqdm' package. To avoid this message, you can set 'progress_bar=False'." + "To use the keyword argument 'progress_bar', you must have installed " + "the 'tqdm' package. To avoid this message, you can set 'progress_bar=False'." ) pbar = tqdm(total=jobs_count, desc="Mechanical Running") @@ -386,7 +386,7 @@ def run(name_local=""): else: run_thread.join() if not complete[0]: # pragma: no cover - LOG.error(f"Stopped instance because running failed.") + LOG.error("Stopped instance because running failed.") try: obj.exit() except Exception as e: diff --git a/tests/conftest.py b/tests/conftest.py index 74de05054..bffde69ad 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -167,7 +167,7 @@ def embedded_app(pytestconfig, request): @pytest.fixture(autouse=True) def mke_app_reset(request): global EMBEDDED_APP - if EMBEDDED_APP == None: + if EMBEDDED_APP is None: # embedded app was not started - no need to do anything return terminal_reporter = request.config.pluginmanager.getplugin("terminalreporter") @@ -178,7 +178,7 @@ def mke_app_reset(request): _CHECK_PROCESS_RETURN_CODE = os.name == "nt" -# set to true if you want to see all the subprocess stdout/stderr +# set to True if you want to see all the subprocess stdout/stderr _PRINT_SUBPROCESS_OUTPUT_TO_CONSOLE = False @@ -374,7 +374,7 @@ def mechanical_pool(): def pytest_addoption(parser): mechanical_path = atp.get_mechanical_path(False) - if mechanical_path == None: + if mechanical_path is None: parser.addoption("--ansys-version", default="242") else: mechanical_version = atp.version_from_path("mechanical", mechanical_path) From 7a10b5d4370c187ca40ad35d107dd7d778016e7d Mon Sep 17 00:00:00 2001 From: Kerry McAdams <58492561+klmcadams@users.noreply.github.com> Date: Thu, 2 Jan 2025 12:08:51 -0500 Subject: [PATCH 23/44] chore: Update license headers for 2025 (#1014) Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- LICENSE | 2 +- doc/changelog.d/1014.maintenance.md | 1 + examples/embedding_n_remote/embedding_remote.py | 2 +- src/ansys/mechanical/core/__init__.py | 2 +- src/ansys/mechanical/core/_version.py | 2 +- src/ansys/mechanical/core/embedding/__init__.py | 2 +- src/ansys/mechanical/core/embedding/addins.py | 2 +- src/ansys/mechanical/core/embedding/app.py | 2 +- src/ansys/mechanical/core/embedding/app_libraries.py | 2 +- src/ansys/mechanical/core/embedding/appdata.py | 2 +- src/ansys/mechanical/core/embedding/background.py | 2 +- src/ansys/mechanical/core/embedding/cleanup_gui.py | 2 +- src/ansys/mechanical/core/embedding/enum_importer.py | 2 +- src/ansys/mechanical/core/embedding/imports.py | 2 +- src/ansys/mechanical/core/embedding/initializer.py | 2 +- src/ansys/mechanical/core/embedding/loader.py | 2 +- src/ansys/mechanical/core/embedding/logger/__init__.py | 2 +- src/ansys/mechanical/core/embedding/logger/environ.py | 2 +- src/ansys/mechanical/core/embedding/logger/linux_api.py | 2 +- src/ansys/mechanical/core/embedding/logger/sinks.py | 2 +- src/ansys/mechanical/core/embedding/logger/windows_api.py | 2 +- src/ansys/mechanical/core/embedding/poster.py | 2 +- src/ansys/mechanical/core/embedding/resolver.py | 2 +- src/ansys/mechanical/core/embedding/runtime.py | 2 +- src/ansys/mechanical/core/embedding/shims.py | 2 +- src/ansys/mechanical/core/embedding/ui.py | 2 +- src/ansys/mechanical/core/embedding/utils.py | 2 +- src/ansys/mechanical/core/embedding/viz/__init__.py | 2 +- src/ansys/mechanical/core/embedding/viz/embedding_plotter.py | 2 +- src/ansys/mechanical/core/embedding/viz/usd_converter.py | 2 +- src/ansys/mechanical/core/embedding/viz/utils.py | 2 +- src/ansys/mechanical/core/embedding/warnings.py | 2 +- src/ansys/mechanical/core/errors.py | 2 +- src/ansys/mechanical/core/examples/__init__.py | 2 +- src/ansys/mechanical/core/examples/downloads.py | 2 +- src/ansys/mechanical/core/feature_flags.py | 2 +- src/ansys/mechanical/core/ide_config.py | 2 +- src/ansys/mechanical/core/launcher.py | 2 +- src/ansys/mechanical/core/logging.py | 2 +- src/ansys/mechanical/core/mechanical.py | 2 +- src/ansys/mechanical/core/misc.py | 2 +- src/ansys/mechanical/core/pool.py | 2 +- src/ansys/mechanical/core/run.py | 2 +- tests/conftest.py | 2 +- tests/embedding/__init__.py | 2 +- tests/embedding/test_app.py | 2 +- tests/embedding/test_app_libraries.py | 2 +- tests/embedding/test_appdata.py | 2 +- tests/embedding/test_background.py | 2 +- tests/embedding/test_dyna.py | 2 +- tests/embedding/test_globals.py | 2 +- tests/embedding/test_graphics_export.py | 2 +- tests/embedding/test_logger.py | 2 +- tests/embedding/test_qk_eng_wb2.py | 2 +- tests/embedding/test_remote_solve.py | 2 +- tests/scripts/api.py | 2 +- tests/scripts/background_app_test.py | 2 +- tests/scripts/build_gallery_test.py | 2 +- tests/scripts/embedding_log_test.py | 2 +- tests/scripts/log_message.py | 2 +- tests/scripts/pythonnet_warning.py | 2 +- tests/scripts/run_embedded_app.py | 2 +- tests/scripts/run_python_error.py | 2 +- tests/scripts/run_python_success.py | 2 +- tests/scripts/run_remote_session.py | 2 +- tests/test_cli.py | 2 +- tests/test_examples_downloads.py | 2 +- tests/test_launcher.py | 2 +- tests/test_logging.py | 2 +- tests/test_mechanical.py | 2 +- tests/test_misc.py | 2 +- tests/test_pool.py | 2 +- 72 files changed, 72 insertions(+), 71 deletions(-) create mode 100644 doc/changelog.d/1014.maintenance.md diff --git a/LICENSE b/LICENSE index 509f0066b..e40f17b8d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +Copyright (c) 2022 - 2025 ANSYS, Inc. and/or its affiliates. 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 diff --git a/doc/changelog.d/1014.maintenance.md b/doc/changelog.d/1014.maintenance.md new file mode 100644 index 000000000..99aeeef5f --- /dev/null +++ b/doc/changelog.d/1014.maintenance.md @@ -0,0 +1 @@ +Update license headers for 2025 \ No newline at end of file diff --git a/examples/embedding_n_remote/embedding_remote.py b/examples/embedding_n_remote/embedding_remote.py index c061151fd..f214d968c 100644 --- a/examples/embedding_n_remote/embedding_remote.py +++ b/examples/embedding_n_remote/embedding_remote.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/__init__.py b/src/ansys/mechanical/core/__init__.py index d96a539db..49f4670fe 100644 --- a/src/ansys/mechanical/core/__init__.py +++ b/src/ansys/mechanical/core/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/_version.py b/src/ansys/mechanical/core/_version.py index f623dfcbb..6d535e240 100644 --- a/src/ansys/mechanical/core/_version.py +++ b/src/ansys/mechanical/core/_version.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/embedding/__init__.py b/src/ansys/mechanical/core/embedding/__init__.py index c6d8258d1..ea6614f50 100644 --- a/src/ansys/mechanical/core/embedding/__init__.py +++ b/src/ansys/mechanical/core/embedding/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/embedding/addins.py b/src/ansys/mechanical/core/embedding/addins.py index 3a865a616..2f3a14c2a 100644 --- a/src/ansys/mechanical/core/embedding/addins.py +++ b/src/ansys/mechanical/core/embedding/addins.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/embedding/app.py b/src/ansys/mechanical/core/embedding/app.py index a839eab06..e93864239 100644 --- a/src/ansys/mechanical/core/embedding/app.py +++ b/src/ansys/mechanical/core/embedding/app.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/embedding/app_libraries.py b/src/ansys/mechanical/core/embedding/app_libraries.py index 69a75e88c..15cd06c0c 100644 --- a/src/ansys/mechanical/core/embedding/app_libraries.py +++ b/src/ansys/mechanical/core/embedding/app_libraries.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/embedding/appdata.py b/src/ansys/mechanical/core/embedding/appdata.py index e484f3dca..2d1c34c24 100644 --- a/src/ansys/mechanical/core/embedding/appdata.py +++ b/src/ansys/mechanical/core/embedding/appdata.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/embedding/background.py b/src/ansys/mechanical/core/embedding/background.py index dddec157d..0305d3344 100644 --- a/src/ansys/mechanical/core/embedding/background.py +++ b/src/ansys/mechanical/core/embedding/background.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/embedding/cleanup_gui.py b/src/ansys/mechanical/core/embedding/cleanup_gui.py index 4294f67c7..511f72e0b 100644 --- a/src/ansys/mechanical/core/embedding/cleanup_gui.py +++ b/src/ansys/mechanical/core/embedding/cleanup_gui.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/embedding/enum_importer.py b/src/ansys/mechanical/core/embedding/enum_importer.py index 2231b69b4..059a294cc 100644 --- a/src/ansys/mechanical/core/embedding/enum_importer.py +++ b/src/ansys/mechanical/core/embedding/enum_importer.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/embedding/imports.py b/src/ansys/mechanical/core/embedding/imports.py index b5441da44..0e0f42879 100644 --- a/src/ansys/mechanical/core/embedding/imports.py +++ b/src/ansys/mechanical/core/embedding/imports.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/embedding/initializer.py b/src/ansys/mechanical/core/embedding/initializer.py index 10cd673bb..fdf4d930f 100644 --- a/src/ansys/mechanical/core/embedding/initializer.py +++ b/src/ansys/mechanical/core/embedding/initializer.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/embedding/loader.py b/src/ansys/mechanical/core/embedding/loader.py index 58d16b103..51d0aa6b5 100644 --- a/src/ansys/mechanical/core/embedding/loader.py +++ b/src/ansys/mechanical/core/embedding/loader.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/embedding/logger/__init__.py b/src/ansys/mechanical/core/embedding/logger/__init__.py index c96922631..5d136cdd6 100644 --- a/src/ansys/mechanical/core/embedding/logger/__init__.py +++ b/src/ansys/mechanical/core/embedding/logger/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/embedding/logger/environ.py b/src/ansys/mechanical/core/embedding/logger/environ.py index 244573cc7..7c4cf2a9d 100644 --- a/src/ansys/mechanical/core/embedding/logger/environ.py +++ b/src/ansys/mechanical/core/embedding/logger/environ.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/embedding/logger/linux_api.py b/src/ansys/mechanical/core/embedding/logger/linux_api.py index e2db3484f..b28be441f 100644 --- a/src/ansys/mechanical/core/embedding/logger/linux_api.py +++ b/src/ansys/mechanical/core/embedding/logger/linux_api.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/embedding/logger/sinks.py b/src/ansys/mechanical/core/embedding/logger/sinks.py index ef317fe36..6b84f51de 100644 --- a/src/ansys/mechanical/core/embedding/logger/sinks.py +++ b/src/ansys/mechanical/core/embedding/logger/sinks.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/embedding/logger/windows_api.py b/src/ansys/mechanical/core/embedding/logger/windows_api.py index a301b42e0..c2de74dec 100644 --- a/src/ansys/mechanical/core/embedding/logger/windows_api.py +++ b/src/ansys/mechanical/core/embedding/logger/windows_api.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/embedding/poster.py b/src/ansys/mechanical/core/embedding/poster.py index 8dfe8a14e..93a994cc0 100644 --- a/src/ansys/mechanical/core/embedding/poster.py +++ b/src/ansys/mechanical/core/embedding/poster.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/embedding/resolver.py b/src/ansys/mechanical/core/embedding/resolver.py index 4046fe1cd..ebf8fe4da 100644 --- a/src/ansys/mechanical/core/embedding/resolver.py +++ b/src/ansys/mechanical/core/embedding/resolver.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/embedding/runtime.py b/src/ansys/mechanical/core/embedding/runtime.py index 69dc6ab11..f609808c7 100644 --- a/src/ansys/mechanical/core/embedding/runtime.py +++ b/src/ansys/mechanical/core/embedding/runtime.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/embedding/shims.py b/src/ansys/mechanical/core/embedding/shims.py index 49a55f362..285308e67 100644 --- a/src/ansys/mechanical/core/embedding/shims.py +++ b/src/ansys/mechanical/core/embedding/shims.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/embedding/ui.py b/src/ansys/mechanical/core/embedding/ui.py index 23c77a29b..af631b460 100644 --- a/src/ansys/mechanical/core/embedding/ui.py +++ b/src/ansys/mechanical/core/embedding/ui.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/embedding/utils.py b/src/ansys/mechanical/core/embedding/utils.py index f60c528b1..73c2b6c1f 100644 --- a/src/ansys/mechanical/core/embedding/utils.py +++ b/src/ansys/mechanical/core/embedding/utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/embedding/viz/__init__.py b/src/ansys/mechanical/core/embedding/viz/__init__.py index a4270eb41..5e8b43f5c 100644 --- a/src/ansys/mechanical/core/embedding/viz/__init__.py +++ b/src/ansys/mechanical/core/embedding/viz/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/embedding/viz/embedding_plotter.py b/src/ansys/mechanical/core/embedding/viz/embedding_plotter.py index b900b3cbc..ab03db7f1 100644 --- a/src/ansys/mechanical/core/embedding/viz/embedding_plotter.py +++ b/src/ansys/mechanical/core/embedding/viz/embedding_plotter.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/embedding/viz/usd_converter.py b/src/ansys/mechanical/core/embedding/viz/usd_converter.py index 46d8ed0e1..32e1ed931 100644 --- a/src/ansys/mechanical/core/embedding/viz/usd_converter.py +++ b/src/ansys/mechanical/core/embedding/viz/usd_converter.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/embedding/viz/utils.py b/src/ansys/mechanical/core/embedding/viz/utils.py index 93ed55171..38c96254c 100644 --- a/src/ansys/mechanical/core/embedding/viz/utils.py +++ b/src/ansys/mechanical/core/embedding/viz/utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/embedding/warnings.py b/src/ansys/mechanical/core/embedding/warnings.py index 6ae04d59b..0e5c7b42a 100644 --- a/src/ansys/mechanical/core/embedding/warnings.py +++ b/src/ansys/mechanical/core/embedding/warnings.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/errors.py b/src/ansys/mechanical/core/errors.py index 42345976f..b3d53a9d2 100644 --- a/src/ansys/mechanical/core/errors.py +++ b/src/ansys/mechanical/core/errors.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/examples/__init__.py b/src/ansys/mechanical/core/examples/__init__.py index 54c2935ba..bca01213e 100644 --- a/src/ansys/mechanical/core/examples/__init__.py +++ b/src/ansys/mechanical/core/examples/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/examples/downloads.py b/src/ansys/mechanical/core/examples/downloads.py index 188596394..09df66428 100644 --- a/src/ansys/mechanical/core/examples/downloads.py +++ b/src/ansys/mechanical/core/examples/downloads.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/feature_flags.py b/src/ansys/mechanical/core/feature_flags.py index 96c11838a..813911be9 100644 --- a/src/ansys/mechanical/core/feature_flags.py +++ b/src/ansys/mechanical/core/feature_flags.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/ide_config.py b/src/ansys/mechanical/core/ide_config.py index b317ad3bc..e7c1252e7 100644 --- a/src/ansys/mechanical/core/ide_config.py +++ b/src/ansys/mechanical/core/ide_config.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/launcher.py b/src/ansys/mechanical/core/launcher.py index 2c2848f0d..f1514adb7 100644 --- a/src/ansys/mechanical/core/launcher.py +++ b/src/ansys/mechanical/core/launcher.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/logging.py b/src/ansys/mechanical/core/logging.py index 528b532ab..c2a323157 100644 --- a/src/ansys/mechanical/core/logging.py +++ b/src/ansys/mechanical/core/logging.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/mechanical.py b/src/ansys/mechanical/core/mechanical.py index 18652eaac..1c4347671 100644 --- a/src/ansys/mechanical/core/mechanical.py +++ b/src/ansys/mechanical/core/mechanical.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/misc.py b/src/ansys/mechanical/core/misc.py index 0469b81e5..a388d11d9 100644 --- a/src/ansys/mechanical/core/misc.py +++ b/src/ansys/mechanical/core/misc.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/pool.py b/src/ansys/mechanical/core/pool.py index 5f58d8882..e4d480889 100644 --- a/src/ansys/mechanical/core/pool.py +++ b/src/ansys/mechanical/core/pool.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/src/ansys/mechanical/core/run.py b/src/ansys/mechanical/core/run.py index b29989b02..ced846726 100644 --- a/src/ansys/mechanical/core/run.py +++ b/src/ansys/mechanical/core/run.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/tests/conftest.py b/tests/conftest.py index bffde69ad..871fd18c2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/tests/embedding/__init__.py b/tests/embedding/__init__.py index b9b198b16..bb3af91ae 100644 --- a/tests/embedding/__init__.py +++ b/tests/embedding/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/tests/embedding/test_app.py b/tests/embedding/test_app.py index 7f85030b6..cb05cbbc2 100644 --- a/tests/embedding/test_app.py +++ b/tests/embedding/test_app.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/tests/embedding/test_app_libraries.py b/tests/embedding/test_app_libraries.py index eea82578b..842f4e6e3 100644 --- a/tests/embedding/test_app_libraries.py +++ b/tests/embedding/test_app_libraries.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/tests/embedding/test_appdata.py b/tests/embedding/test_appdata.py index 088eba919..0d08710a1 100644 --- a/tests/embedding/test_appdata.py +++ b/tests/embedding/test_appdata.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/tests/embedding/test_background.py b/tests/embedding/test_background.py index 4c0c3176c..962edf766 100644 --- a/tests/embedding/test_background.py +++ b/tests/embedding/test_background.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/tests/embedding/test_dyna.py b/tests/embedding/test_dyna.py index 7348b272a..5e8f44ae0 100644 --- a/tests/embedding/test_dyna.py +++ b/tests/embedding/test_dyna.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/tests/embedding/test_globals.py b/tests/embedding/test_globals.py index 5893c7e13..aa0f55ff9 100644 --- a/tests/embedding/test_globals.py +++ b/tests/embedding/test_globals.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/tests/embedding/test_graphics_export.py b/tests/embedding/test_graphics_export.py index 07dc0c214..fe32d5779 100644 --- a/tests/embedding/test_graphics_export.py +++ b/tests/embedding/test_graphics_export.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/tests/embedding/test_logger.py b/tests/embedding/test_logger.py index 239b5dcca..76242b671 100644 --- a/tests/embedding/test_logger.py +++ b/tests/embedding/test_logger.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/tests/embedding/test_qk_eng_wb2.py b/tests/embedding/test_qk_eng_wb2.py index 969094bdd..2a488003a 100644 --- a/tests/embedding/test_qk_eng_wb2.py +++ b/tests/embedding/test_qk_eng_wb2.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/tests/embedding/test_remote_solve.py b/tests/embedding/test_remote_solve.py index afc098661..d300482b0 100644 --- a/tests/embedding/test_remote_solve.py +++ b/tests/embedding/test_remote_solve.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/tests/scripts/api.py b/tests/scripts/api.py index 8e581a0c5..3b92ec665 100644 --- a/tests/scripts/api.py +++ b/tests/scripts/api.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/tests/scripts/background_app_test.py b/tests/scripts/background_app_test.py index d4e48bae0..e52f8632f 100644 --- a/tests/scripts/background_app_test.py +++ b/tests/scripts/background_app_test.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/tests/scripts/build_gallery_test.py b/tests/scripts/build_gallery_test.py index ead9afe63..fa66d78cd 100644 --- a/tests/scripts/build_gallery_test.py +++ b/tests/scripts/build_gallery_test.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/tests/scripts/embedding_log_test.py b/tests/scripts/embedding_log_test.py index e7c91a963..da3ed90cd 100644 --- a/tests/scripts/embedding_log_test.py +++ b/tests/scripts/embedding_log_test.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/tests/scripts/log_message.py b/tests/scripts/log_message.py index 3f1faa4bd..a8ea9484d 100644 --- a/tests/scripts/log_message.py +++ b/tests/scripts/log_message.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/tests/scripts/pythonnet_warning.py b/tests/scripts/pythonnet_warning.py index 207f5bc52..df96b3204 100644 --- a/tests/scripts/pythonnet_warning.py +++ b/tests/scripts/pythonnet_warning.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/tests/scripts/run_embedded_app.py b/tests/scripts/run_embedded_app.py index 43acf3ff4..cd67ac8ff 100644 --- a/tests/scripts/run_embedded_app.py +++ b/tests/scripts/run_embedded_app.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/tests/scripts/run_python_error.py b/tests/scripts/run_python_error.py index 1be74542b..96b52cfed 100644 --- a/tests/scripts/run_python_error.py +++ b/tests/scripts/run_python_error.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/tests/scripts/run_python_success.py b/tests/scripts/run_python_success.py index 8bc3d4a86..bdde16ac8 100644 --- a/tests/scripts/run_python_success.py +++ b/tests/scripts/run_python_success.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/tests/scripts/run_remote_session.py b/tests/scripts/run_remote_session.py index d8a4dfe35..68a0719ad 100644 --- a/tests/scripts/run_remote_session.py +++ b/tests/scripts/run_remote_session.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/tests/test_cli.py b/tests/test_cli.py index bcac054d2..9ce1f7000 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/tests/test_examples_downloads.py b/tests/test_examples_downloads.py index 0acd09ce4..bdd6db25b 100644 --- a/tests/test_examples_downloads.py +++ b/tests/test_examples_downloads.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/tests/test_launcher.py b/tests/test_launcher.py index c2dd78777..8331c4885 100644 --- a/tests/test_launcher.py +++ b/tests/test_launcher.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/tests/test_logging.py b/tests/test_logging.py index c03735086..20e88ccf8 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/tests/test_mechanical.py b/tests/test_mechanical.py index c07ac9e1d..5ef1fcc67 100644 --- a/tests/test_mechanical.py +++ b/tests/test_mechanical.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/tests/test_misc.py b/tests/test_misc.py index c6852f238..2372f1345 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # diff --git a/tests/test_pool.py b/tests/test_pool.py index fdba61535..1163ab99e 100644 --- a/tests/test_pool.py +++ b/tests/test_pool.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # From 218058a780a023438466d084943989c27fd9ece5 Mon Sep 17 00:00:00 2001 From: Kerry McAdams <58492561+klmcadams@users.noreply.github.com> Date: Thu, 2 Jan 2025 12:47:07 -0500 Subject: [PATCH 24/44] chore: Bump ``ansys-mechanical-stubs`` to 0.1.5 and add typehint to DataModel (#1015) Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/1015.maintenance.md | 1 + pyproject.toml | 2 +- src/ansys/mechanical/core/embedding/app.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 doc/changelog.d/1015.maintenance.md diff --git a/doc/changelog.d/1015.maintenance.md b/doc/changelog.d/1015.maintenance.md new file mode 100644 index 000000000..840c5de26 --- /dev/null +++ b/doc/changelog.d/1015.maintenance.md @@ -0,0 +1 @@ +Bump ``ansys-mechanical-stubs`` to 0.1.5 and add typehint to DataModel \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 2250ff4ea..576a79392 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ classifiers = [ dependencies = [ "ansys-api-mechanical==0.1.2", "ansys-mechanical-env==0.1.8", - "ansys-mechanical-stubs==0.1.4", + "ansys-mechanical-stubs==0.1.5", "ansys-platform-instancemanagement>=1.0.1", "ansys-pythonnet>=3.1.0rc2", "ansys-tools-path>=0.3.1", diff --git a/src/ansys/mechanical/core/embedding/app.py b/src/ansys/mechanical/core/embedding/app.py index e93864239..6a675fd85 100644 --- a/src/ansys/mechanical/core/embedding/app.py +++ b/src/ansys/mechanical/core/embedding/app.py @@ -376,7 +376,7 @@ def poster(self) -> Poster: return self._poster @property - def DataModel(self): + def DataModel(self) -> Ansys.Mechanical.DataModel.Interfaces.DataModelObject: """Return the DataModel.""" return GetterWrapper(self._app, lambda app: app.DataModel) From 1f7934762649c71c1a171a1ce7da58e1a1c5592c Mon Sep 17 00:00:00 2001 From: Kerry McAdams <58492561+klmcadams@users.noreply.github.com> Date: Thu, 2 Jan 2025 15:29:44 -0500 Subject: [PATCH 25/44] feat: Option to ignore lock file on open (#1007) Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/1007.added.md | 1 + src/ansys/mechanical/core/embedding/app.py | 25 ++++++++++++++++++++-- tests/embedding/test_app.py | 21 ++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 doc/changelog.d/1007.added.md diff --git a/doc/changelog.d/1007.added.md b/doc/changelog.d/1007.added.md new file mode 100644 index 000000000..8f9ce736e --- /dev/null +++ b/doc/changelog.d/1007.added.md @@ -0,0 +1 @@ +Option to ignore lock file on open \ No newline at end of file diff --git a/src/ansys/mechanical/core/embedding/app.py b/src/ansys/mechanical/core/embedding/app.py index 6a675fd85..acbf82406 100644 --- a/src/ansys/mechanical/core/embedding/app.py +++ b/src/ansys/mechanical/core/embedding/app.py @@ -25,6 +25,7 @@ import atexit import os +from pathlib import Path import shutil import typing import warnings @@ -217,8 +218,28 @@ def _dispose(self): self._app.Dispose() self._disposed = True - def open(self, db_file): - """Open the db file.""" + def open(self, db_file, remove_lock=False): + """Open the db file. + + Parameters + ---------- + db_file : str + Path to a Mechanical database file (.mechdat or .mechdb). + remove_lock : bool, optional + Whether or not to remove the lock file if it exists before opening the project file. + """ + if remove_lock: + lock_file = Path(self.DataModel.Project.ProjectDirectory) / ".mech_lock" + # Remove the lock file if it exists before opening the project file + if lock_file.exists(): + warnings.warn( + f"Removing the lock file, {lock_file}, before opening the project. \ +This may corrupt the project file.", + UserWarning, + stacklevel=2, + ) + lock_file.unlink() + self.DataModel.Project.Open(db_file) def save(self, path=None): diff --git a/tests/embedding/test_app.py b/tests/embedding/test_app.py index cb05cbbc2..60ea66c2e 100644 --- a/tests/embedding/test_app.py +++ b/tests/embedding/test_app.py @@ -22,6 +22,7 @@ """Miscellaneous embedding tests""" import os +from pathlib import Path import subprocess import sys from tempfile import NamedTemporaryFile @@ -433,3 +434,23 @@ def test_app_execute_script_from_file(embedded_app, rootdir, printer): succes_script_path = os.path.join(rootdir, "tests", "scripts", "run_python_success.py") result = embedded_app.execute_script_from_file(succes_script_path) assert result == "test" + + +@pytest.mark.embedding +def test_app_lock_file_open(embedded_app, tmp_path: pytest.TempPathFactory): + """Test the lock file is removed on open if remove_lock=True.""" + embedded_app.DataModel.Project.Name = "PROJECT 1" + project_file = os.path.join(tmp_path, f"{NamedTemporaryFile().name}.mechdat") + embedded_app.save_as(project_file) + with pytest.raises(Exception): + embedded_app.save_as(project_file) + embedded_app.save_as(project_file, overwrite=True) + + lock_file = Path(embedded_app.DataModel.Project.ProjectDirectory) / ".mech_lock" + + # Assert the lock file exists after saving it + assert lock_file.exists() + + # Assert a warning is emitted if the lock file is going to be removed + with pytest.warns(UserWarning): + embedded_app.open(project_file, remove_lock=True) From c288b5695097494f4fdc859f594a39bc50601b03 Mon Sep 17 00:00:00 2001 From: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> Date: Mon, 6 Jan 2025 14:44:43 +0100 Subject: [PATCH 26/44] CHORE: Follow pythonic standard for comparison to None. (#1016) Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/1016.maintenance.md | 1 + src/ansys/mechanical/core/embedding/app.py | 6 +++--- src/ansys/mechanical/core/embedding/background.py | 2 +- src/ansys/mechanical/core/embedding/logger/__init__.py | 4 ++-- src/ansys/mechanical/core/mechanical.py | 2 +- tests/conftest.py | 2 +- 6 files changed, 9 insertions(+), 8 deletions(-) create mode 100644 doc/changelog.d/1016.maintenance.md diff --git a/doc/changelog.d/1016.maintenance.md b/doc/changelog.d/1016.maintenance.md new file mode 100644 index 000000000..b0353c841 --- /dev/null +++ b/doc/changelog.d/1016.maintenance.md @@ -0,0 +1 @@ +Follow pythonic standard for comparison to None. \ No newline at end of file diff --git a/src/ansys/mechanical/core/embedding/app.py b/src/ansys/mechanical/core/embedding/app.py index acbf82406..43d16cd83 100644 --- a/src/ansys/mechanical/core/embedding/app.py +++ b/src/ansys/mechanical/core/embedding/app.py @@ -159,7 +159,7 @@ def __init__(self, db_file=None, private_appdata=False, **kwargs): if len(INSTANCES) != 0: instance: App = INSTANCES[0] instance._share(self) - if db_file != None: + if db_file is not None: self.open(db_file) return if len(INSTANCES) > 0: @@ -170,7 +170,7 @@ def __init__(self, db_file=None, private_appdata=False, **kwargs): version = int(version) except ValueError: raise ValueError( - f"The version must be an integer or that can be converted to an integer." + "The version must be an integer or of type that can be converted to an integer." ) self._version = initializer.initialize(version) configuration = kwargs.get("config", _get_default_addin_configuration()) @@ -392,7 +392,7 @@ def plot(self) -> None: @property def poster(self) -> Poster: """Returns an instance of Poster.""" - if self._poster == None: + if self._poster is None: self._poster = Poster() return self._poster diff --git a/src/ansys/mechanical/core/embedding/background.py b/src/ansys/mechanical/core/embedding/background.py index 0305d3344..d32f31636 100644 --- a/src/ansys/mechanical/core/embedding/background.py +++ b/src/ansys/mechanical/core/embedding/background.py @@ -49,7 +49,7 @@ class BackgroundApp: def __init__(self, **kwargs): """Construct an instance of BackgroundApp.""" - if BackgroundApp.__app_thread == None: + if BackgroundApp.__app_thread is None: BackgroundApp.__app_thread = threading.Thread( target=self._start_app, kwargs=kwargs, daemon=True ) diff --git a/src/ansys/mechanical/core/embedding/logger/__init__.py b/src/ansys/mechanical/core/embedding/logger/__init__.py index 5d136cdd6..20818e19d 100644 --- a/src/ansys/mechanical/core/embedding/logger/__init__.py +++ b/src/ansys/mechanical/core/embedding/logger/__init__.py @@ -78,7 +78,7 @@ def _get_backend() -> ( Actually logging a message or flushing the log only works after initializing. """ # TODO - use abc instead of a union type? - embedding_initialized = initializer.INITIALIZED_VERSION != None + embedding_initialized = initializer.INITIALIZED_VERSION is not None if not embedding_initialized: return environ.EnvironBackend() if os.name == "nt": @@ -115,7 +115,7 @@ def configure(cls, level=logging.WARNING, directory=None, base_directory=None, t # Set up the sink-specific log configuration and store to global state. cls._store_stdout_sink_enabled(to_stdout) - file_sink_enabled = directory != None or base_directory != None + file_sink_enabled = directory is not None or base_directory is not None cls._store_file_sink_enabled(file_sink_enabled) # Commit the sink-specific log configuration global state to the backend. diff --git a/src/ansys/mechanical/core/mechanical.py b/src/ansys/mechanical/core/mechanical.py index 1c4347671..58ac5c0f1 100644 --- a/src/ansys/mechanical/core/mechanical.py +++ b/src/ansys/mechanical/core/mechanical.py @@ -243,7 +243,7 @@ def check_valid_mechanical(): """ mechanical_path = atp.get_mechanical_path(False) - if mechanical_path == None: + if mechanical_path is None: return False mechanical_version = atp.version_from_path("mechanical", mechanical_path) return not (mechanical_version < 232 and os.name != "posix") diff --git a/tests/conftest.py b/tests/conftest.py index 871fd18c2..6b19895be 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -144,7 +144,7 @@ def start_embedding_app(version, pytestconfig) -> datetime.timedelta: ), "Can't run test cases, Mechanical is in readonly mode! Check license configuration." startup_time = (datetime.datetime.now() - start).total_seconds() num_cores = os.environ.get("NUM_CORES", None) - if num_cores != None: + if num_cores is not None: config = EMBEDDED_APP.ExtAPI.Application.SolveConfigurations["My Computer"] config.SolveProcessSettings.MaxNumberOfCores = int(num_cores) return startup_time From 143fc840248b8f66293f1780703ce91335590a1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 19:17:48 +0000 Subject: [PATCH 27/44] CHORE: Bump grpcio from 1.68.1 to 1.69.0 in the core group (#1020) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/1020.maintenance.md | 1 + pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 doc/changelog.d/1020.maintenance.md diff --git a/doc/changelog.d/1020.maintenance.md b/doc/changelog.d/1020.maintenance.md new file mode 100644 index 000000000..55e49ce55 --- /dev/null +++ b/doc/changelog.d/1020.maintenance.md @@ -0,0 +1 @@ +Bump grpcio from 1.68.1 to 1.69.0 in the core group \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 576a79392..24311bbdd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,7 @@ tests = [ doc = [ "sphinx==8.1.3", "ansys-sphinx-theme[autoapi]==1.2.4", - "grpcio==1.68.1", + "grpcio==1.69.0", "imageio-ffmpeg==0.5.1", "imageio==2.36.1", "jupyter_sphinx==0.5.3", From 009b81904e9ccd655a3007c175788468e5513827 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 14:51:02 -0600 Subject: [PATCH 28/44] CHORE: Bump sphinx-autodoc-typehints from 2.5.0 to 3.0.0 (#1021) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/1021.maintenance.md | 1 + pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 doc/changelog.d/1021.maintenance.md diff --git a/doc/changelog.d/1021.maintenance.md b/doc/changelog.d/1021.maintenance.md new file mode 100644 index 000000000..0dee149fd --- /dev/null +++ b/doc/changelog.d/1021.maintenance.md @@ -0,0 +1 @@ +Bump sphinx-autodoc-typehints from 2.5.0 to 3.0.0 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 24311bbdd..2fece645c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,7 +74,7 @@ doc = [ "pythreejs==2.4.2", "pyvista>=0.39.1", "sphinx-autobuild==2024.10.3", - "sphinx-autodoc-typehints==2.5.0", + "sphinx-autodoc-typehints==3.0.0", "sphinx-copybutton==0.5.2", "sphinx_design==0.6.1", "sphinx-gallery==0.18.0", From 924d47619b05644bd4369c664f308581784272e9 Mon Sep 17 00:00:00 2001 From: Mohamed Koubaa Date: Tue, 7 Jan 2025 07:59:46 -0600 Subject: [PATCH 29/44] fix: Process return code (#1029) Co-authored-by: Mohamed Koubaa Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/1029.fixed.md | 1 + tests/conftest.py | 11 +++++++---- tests/embedding/test_logger.py | 8 +++++--- 3 files changed, 13 insertions(+), 7 deletions(-) create mode 100644 doc/changelog.d/1029.fixed.md diff --git a/doc/changelog.d/1029.fixed.md b/doc/changelog.d/1029.fixed.md new file mode 100644 index 000000000..7996cc7e0 --- /dev/null +++ b/doc/changelog.d/1029.fixed.md @@ -0,0 +1 @@ +Process return code \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 6b19895be..cd4565b9a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -176,17 +176,20 @@ def mke_app_reset(request): EMBEDDED_APP.new() -_CHECK_PROCESS_RETURN_CODE = os.name == "nt" - # set to True if you want to see all the subprocess stdout/stderr _PRINT_SUBPROCESS_OUTPUT_TO_CONSOLE = False @pytest.fixture() -def run_subprocess(): +def run_subprocess(pytestconfig): + version = pytestconfig.getoption("ansys_version") + def func(args, env=None, check: bool = None): if check is None: - check = _CHECK_PROCESS_RETURN_CODE + check = True + if os.name != "nt": + if int(version) < 251: + check = False process, output = ansys.mechanical.core.run._run( args, env, check, _PRINT_SUBPROCESS_OUTPUT_TO_CONSOLE ) diff --git a/tests/embedding/test_logger.py b/tests/embedding/test_logger.py index 76242b671..c0ef1435f 100644 --- a/tests/embedding/test_logger.py +++ b/tests/embedding/test_logger.py @@ -57,14 +57,16 @@ def _run_embedding_log_test( embedded_py = os.path.join(rootdir, "tests", "scripts", "embedding_log_test.py") subprocess_pass_expected = pass_expected - if pass_expected == True and os.name != "nt" and int(version) < 251: - subprocess_pass_expected = False + if pass_expected == True: + if os.name != "nt" and int(version) < 251: + subprocess_pass_expected = False - process, stdout, stderr = run_subprocess( + _, stdout, stderr = run_subprocess( [sys.executable, embedded_py, version, testname], _get_env_without_logging_variables(), subprocess_pass_expected, ) + if not subprocess_pass_expected: stdout = stdout.decode() _assert_success(stdout, pass_expected) From ba8cc0d17bf74fb4d810f12a06a1b6262a3dce63 Mon Sep 17 00:00:00 2001 From: Dipin <26918585+dipinknair@users.noreply.github.com> Date: Tue, 7 Jan 2025 08:36:09 -0600 Subject: [PATCH 30/44] FEAT: Add project directory property (#1022) Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/1022.added.md | 1 + src/ansys/mechanical/core/embedding/app.py | 5 +++++ src/ansys/mechanical/core/embedding/ui.py | 2 +- tests/embedding/test_app.py | 8 ++++++-- 4 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 doc/changelog.d/1022.added.md diff --git a/doc/changelog.d/1022.added.md b/doc/changelog.d/1022.added.md new file mode 100644 index 000000000..fe71d4afa --- /dev/null +++ b/doc/changelog.d/1022.added.md @@ -0,0 +1 @@ +Add project directory property \ No newline at end of file diff --git a/src/ansys/mechanical/core/embedding/app.py b/src/ansys/mechanical/core/embedding/app.py index 43d16cd83..80e79524b 100644 --- a/src/ansys/mechanical/core/embedding/app.py +++ b/src/ansys/mechanical/core/embedding/app.py @@ -433,6 +433,11 @@ def version(self): """Returns the version of the app.""" return self._version + @property + def project_directory(self): + """Returns the current project directory.""" + return self.DataModel.Project.ProjectDirectory + def _share(self, other) -> None: """Shares the state of self with other. diff --git a/src/ansys/mechanical/core/embedding/ui.py b/src/ansys/mechanical/core/embedding/ui.py index af631b460..77418039a 100644 --- a/src/ansys/mechanical/core/embedding/ui.py +++ b/src/ansys/mechanical/core/embedding/ui.py @@ -59,7 +59,7 @@ def save_temp_copy( A Mechanical embedding application. """ # Identify the mechdb of the saved session from save_original() - project_directory = Path(app.DataModel.Project.ProjectDirectory) + project_directory = Path(app.project_directory) project_directory_parent = project_directory.parent mechdb_file = ( project_directory_parent / f"{project_directory.parts[-1].split('_')[0]}.mechdb" diff --git a/tests/embedding/test_app.py b/tests/embedding/test_app.py index 60ea66c2e..4d4f01a90 100644 --- a/tests/embedding/test_app.py +++ b/tests/embedding/test_app.py @@ -67,6 +67,10 @@ def test_app_save_open(embedded_app, tmp_path: pytest.TempPathFactory): embedded_app.DataModel.Project.Name = "PROJECT 1" project_file = os.path.join(tmp_path, f"{NamedTemporaryFile().name}.mechdat") embedded_app.save_as(project_file) + + project_file_directory = os.path.splitext(project_file)[0] + "_Mech_Files" + assert project_file_directory == os.path.normpath(embedded_app.project_directory) + with pytest.raises(Exception): embedded_app.save_as(project_file) embedded_app.save_as(project_file, overwrite=True) @@ -293,7 +297,7 @@ def test_rm_lockfile(embedded_app, tmp_path: pytest.TempPathFactory): embedded_app.save(mechdat_path) embedded_app.close() - lockfile_path = os.path.join(embedded_app.DataModel.Project.ProjectDirectory, ".mech_lock") + lockfile_path = os.path.join(embedded_app.project_directory, ".mech_lock") # Assert lock file path does not exist assert not os.path.exists(lockfile_path) @@ -446,7 +450,7 @@ def test_app_lock_file_open(embedded_app, tmp_path: pytest.TempPathFactory): embedded_app.save_as(project_file) embedded_app.save_as(project_file, overwrite=True) - lock_file = Path(embedded_app.DataModel.Project.ProjectDirectory) / ".mech_lock" + lock_file = Path(embedded_app.project_directory) / ".mech_lock" # Assert the lock file exists after saving it assert lock_file.exists() From c169bc7dd11b4903856828cf7e646a4fed2ef46e Mon Sep 17 00:00:00 2001 From: Dipin <26918585+dipinknair@users.noreply.github.com> Date: Tue, 7 Jan 2025 08:53:41 -0600 Subject: [PATCH 31/44] CI: Update ngihtly for pre-release version (#1023) Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Co-authored-by: Kerry McAdams <58492561+klmcadams@users.noreply.github.com> --- .github/workflows/ci_cd.yml | 44 +++++++++++++++-------------- doc/changelog.d/1023.maintenance.md | 1 + 2 files changed, 24 insertions(+), 21 deletions(-) create mode 100644 doc/changelog.d/1023.maintenance.md diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 584b1920b..54c0bb9c1 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -7,9 +7,11 @@ on: revn: type: choice options: + - '252' - '251' - '242' - '241' + - '232' description: 'The Mechanical revision number to run tests on.' default: '242' #stable version is 242, must match $stable_container schedule: @@ -35,8 +37,8 @@ env: DOCUMENTATION_CNAME: mechanical.docs.pyansys.com MAIN_PYTHON_VERSION: '3.10' # DEV_REVN & its Docker image are used in scheduled or registry package runs - DEV_REVN: '251' - DEV_DOCKER_IMAGE_VERSION: '25.1_candidate' + STABLE_REVN: '242' + DEV_REVN: '252' LICENSE_SERVER: ${{ secrets.LICENSE_SERVER }} ANSYSLMD_LICENSE_FILE: 1055@${{ secrets.LICENSE_SERVER }} @@ -124,39 +126,41 @@ jobs: name: Save variations of revn runs-on: ubuntu-latest outputs: - # ghcr.io/ansys/mechanical:24.2.0 stable_container: ${{ steps.save-versions.outputs.stable_container }} - # '242' or '251' test_revn: '${{ steps.save-versions.outputs.test_revn }}' - # ghcr.io/ansys/mechanical:24.2.0 or ghcr.io/ansys/mechanical:25.1.0 test_container: ${{ steps.save-versions.outputs.test_container }} - # '24.2.0' or '25.1.0' test_docker_image_version: '${{ steps.save-versions.outputs.test_docker_image_version }}' steps: - id: save-versions run: | if ${{ github.event_name == 'schedule' }}; then - # 251 echo "test_revn=${{ env.DEV_REVN}}" >> $GITHUB_OUTPUT - # ghcr.io/ansys/mechanical:24.2_candidate - echo "test_container=${{ env.DOCKER_PACKAGE }}:${{ env.DEV_DOCKER_IMAGE_VERSION }}" >> $GITHUB_OUTPUT - # 25.1_candidate - echo "test_docker_image_version=${{ env.DEV_DOCKER_IMAGE_VERSION }}" >> $GITHUB_OUTPUT + test_mech_revn=${{ env.DEV_REVN}} + test_mech_image_version=${test_mech_revn:0:2}.${test_mech_revn:2}_candidate + echo "test_container=${{ env.DOCKER_PACKAGE }}:$test_mech_image_version" >> $GITHUB_OUTPUT + echo "test_docker_image_version=$test_mech_image_version" >> $GITHUB_OUTPUT else if [[ -z "${{inputs.revn}}" ]]; then - export mech_revn=242 + mech_revn=${{ env.STABLE_REVN }} else - export mech_revn=${{inputs.revn}} + mech_revn=${{inputs.revn}} fi export mech_image_version=${mech_revn:0:2}.${mech_revn:2}.0 echo "test_revn=$mech_revn" >> $GITHUB_OUTPUT - # ghcr.io/ansys/mechanical:24.2.0 echo "test_container=${{ env.DOCKER_PACKAGE }}:$mech_image_version" >> $GITHUB_OUTPUT - # 24.2.0 echo "test_docker_image_version=$mech_image_version" >> $GITHUB_OUTPUT fi - echo "stable_container=${{ env.DOCKER_PACKAGE }}:24.2.0" >> $GITHUB_OUTPUT + stable_mech_revn=${{ env.STABLE_REVN }} + stable_mech_image_version=${mech_revn:0:2}.${mech_revn:2}.0 + echo "stable_container=${{ env.DOCKER_PACKAGE }}:$stable_mech_image_version" >> $GITHUB_OUTPUT + + echo $GITHUB_OUTPUT + + # --- Help ---- + # schedule nightly uses DEV_REVN candidate + # PRs and merges use STABLE_REVN + # Workflow dispatch can use any revision number config-matrix: runs-on: ubuntu-latest @@ -166,9 +170,7 @@ jobs: steps: - id: set-matrix run: | - # Run all stable mechanical versions release tags - # For nightly scheduled runs use pre-release (25.1_candidate) - # For pull requests and merges use latest stable version (242) + # if a tag(release) is pushed, test all versions if ${{ github.event_name == 'push' }} && ${{ contains(github.ref, 'refs/tags') }}; then echo "matrix={\"mechanical-version\":['23.2.0', '24.1.0', '24.2.0'],\"experimental\":[false]}" >> $GITHUB_OUTPUT else @@ -184,8 +186,8 @@ jobs: run: | sudo apt update sudo apt install bc -y - CONTAINER_VERSION=$(echo "${{ needs.revn-variations.outputs.test_docker_image_version }}" | grep -o -E '[0-9]+(\.[0-9]+)?' | head -n 1) - if (( $(echo "$CONTAINER_VERSION > 24.2" | bc -l) )); then + container_version=$(echo "${{ needs.revn-variations.outputs.test_docker_image_version }}" | grep -o -E '[0-9]+(\.[0-9]+)?' | head -n 1) + if (( $(echo "$container_version > 24.2" | bc -l) )); then echo "container_stable_exit=true" >> $GITHUB_OUTPUT else echo "container_stable_exit=false" >> $GITHUB_OUTPUT diff --git a/doc/changelog.d/1023.maintenance.md b/doc/changelog.d/1023.maintenance.md new file mode 100644 index 000000000..398f0d3a9 --- /dev/null +++ b/doc/changelog.d/1023.maintenance.md @@ -0,0 +1 @@ +Update ngihtly for pre-release version \ No newline at end of file From b81bef25f9f87110a53b117e09664bd3f1ae9b82 Mon Sep 17 00:00:00 2001 From: Mohamed Koubaa Date: Wed, 8 Jan 2025 07:02:27 -0600 Subject: [PATCH 32/44] FIX: Background App initialization (#1030) Co-authored-by: Mohamed Koubaa Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- doc/changelog.d/1026.fixed.md | 1 + doc/changelog.d/1030.fixed.md | 1 + .../mechanical/core/embedding/background.py | 6 +++--- .../mechanical/core/embedding/initializer.py | 19 +++++++++---------- tests/embedding/test_background.py | 2 +- 5 files changed, 15 insertions(+), 14 deletions(-) create mode 100644 doc/changelog.d/1026.fixed.md create mode 100644 doc/changelog.d/1030.fixed.md diff --git a/doc/changelog.d/1026.fixed.md b/doc/changelog.d/1026.fixed.md new file mode 100644 index 000000000..7996cc7e0 --- /dev/null +++ b/doc/changelog.d/1026.fixed.md @@ -0,0 +1 @@ +Process return code \ No newline at end of file diff --git a/doc/changelog.d/1030.fixed.md b/doc/changelog.d/1030.fixed.md new file mode 100644 index 000000000..5000d5c90 --- /dev/null +++ b/doc/changelog.d/1030.fixed.md @@ -0,0 +1 @@ +Background App initialization \ No newline at end of file diff --git a/src/ansys/mechanical/core/embedding/background.py b/src/ansys/mechanical/core/embedding/background.py index d32f31636..9c2fd571f 100644 --- a/src/ansys/mechanical/core/embedding/background.py +++ b/src/ansys/mechanical/core/embedding/background.py @@ -28,6 +28,7 @@ import typing import ansys.mechanical.core as mech +from ansys.mechanical.core.embedding import initializer from ansys.mechanical.core.embedding.poster import Poster import ansys.mechanical.core.embedding.utils as utils @@ -35,7 +36,6 @@ def _exit(background_app: "BackgroundApp"): """Stop the thread serving the Background App.""" background_app.stop() - atexit.unregister(_exit) class BackgroundApp: @@ -50,6 +50,7 @@ class BackgroundApp: def __init__(self, **kwargs): """Construct an instance of BackgroundApp.""" if BackgroundApp.__app_thread is None: + initializer.initialize(kwargs.get("version")) BackgroundApp.__app_thread = threading.Thread( target=self._start_app, kwargs=kwargs, daemon=True ) @@ -67,8 +68,6 @@ def new(): self.post(new) - atexit.register(_exit, self) - @property def app(self) -> mech.App: """Get the App instance of the background thread. @@ -96,6 +95,7 @@ def stop(self) -> None: def _start_app(self, **kwargs) -> None: BackgroundApp.__app = mech.App(**kwargs) BackgroundApp.__poster = BackgroundApp.__app.poster + atexit.register(_exit, self) while True: if BackgroundApp.__stop_signaled: break diff --git a/src/ansys/mechanical/core/embedding/initializer.py b/src/ansys/mechanical/core/embedding/initializer.py index fdf4d930f..3bb3d200d 100644 --- a/src/ansys/mechanical/core/embedding/initializer.py +++ b/src/ansys/mechanical/core/embedding/initializer.py @@ -162,24 +162,22 @@ def __check_loaded_libs(version: int = None): # pragma: no cover def initialize(version: int = None): """Initialize Mechanical embedding.""" - __check_python_interpreter_architecture() # blocks 32 bit python - __check_for_mechanical_env() # checks for mechanical-env in linux embedding - global INITIALIZED_VERSION + if version is None: + version = _get_latest_default_version() + + version = __check_for_supported_version(version=version) + if INITIALIZED_VERSION is not None: if INITIALIZED_VERSION != version: raise ValueError( f"Initialized version {INITIALIZED_VERSION} " f"does not match the expected version {version}." ) - return + return INITIALIZED_VERSION - if version is None: - version = _get_latest_default_version() - - version = __check_for_supported_version(version=version) - - INITIALIZED_VERSION = version + __check_python_interpreter_architecture() # blocks 32 bit python + __check_for_mechanical_env() # checks for mechanical-env in linux embedding __set_environment(version) @@ -212,4 +210,5 @@ def initialize(version: int = None): # attach the resolver resolve(version) + INITIALIZED_VERSION = version return version diff --git a/tests/embedding/test_background.py b/tests/embedding/test_background.py index 962edf766..7ad1d7de1 100644 --- a/tests/embedding/test_background.py +++ b/tests/embedding/test_background.py @@ -40,7 +40,7 @@ def _run_background_app_test( subprocess_pass_expected = pass_expected if pass_expected and os.name != "nt": - if int(version) < 251 or testname == "multiple_instances": + if int(version) < 251: subprocess_pass_expected = False process, stdout, stderr = run_subprocess( From 9ab4288dd16d69c7d8cb8f0a4bc844b3b195ad42 Mon Sep 17 00:00:00 2001 From: Dipin <26918585+dipinknair@users.noreply.github.com> Date: Wed, 8 Jan 2025 09:49:36 -0600 Subject: [PATCH 33/44] CHORE: Support python 3.13 (#997) Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- .github/workflows/ci_cd.yml | 10 +++++----- doc/changelog.d/997.maintenance.md | 1 + doc/source/getting_started/installation.rst | 4 ++-- pyproject.toml | 3 +++ 4 files changed, 11 insertions(+), 7 deletions(-) create mode 100644 doc/changelog.d/997.maintenance.md diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 54c0bb9c1..735a64271 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -35,7 +35,7 @@ env: DOCKER_MECH_CONTAINER_NAME: mechanical PACKAGE_NAME: ansys-mechanical-core DOCUMENTATION_CNAME: mechanical.docs.pyansys.com - MAIN_PYTHON_VERSION: '3.10' + MAIN_PYTHON_VERSION: '3.13' # DEV_REVN & its Docker image are used in scheduled or registry package runs STABLE_REVN: '242' DEV_REVN: '252' @@ -108,7 +108,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ['3.10', '3.11', '3.12'] + python-version: ['3.10', '3.11', '3.12', '3.13'] should-release: - ${{ github.event_name == 'push' && contains(github.ref, 'refs/tags') }} exclude: @@ -294,7 +294,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.10', '3.11', '3.12'] + python-version: ['3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 @@ -367,7 +367,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.10', '3.11', '3.12'] + python-version: ['3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 @@ -437,7 +437,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.10', '3.11', '3.12'] + python-version: ['3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 diff --git a/doc/changelog.d/997.maintenance.md b/doc/changelog.d/997.maintenance.md new file mode 100644 index 000000000..a4053eacd --- /dev/null +++ b/doc/changelog.d/997.maintenance.md @@ -0,0 +1 @@ +Support python 3.13 \ No newline at end of file diff --git a/doc/source/getting_started/installation.rst b/doc/source/getting_started/installation.rst index f7f9d2c57..c437d73aa 100644 --- a/doc/source/getting_started/installation.rst +++ b/doc/source/getting_started/installation.rst @@ -13,7 +13,7 @@ Install the package ------------------- The latest ``ansys.mechanical.core`` package supports Python 3.10 through -Python 3.12 on Windows, Linux, and Mac. +Python 3.13 on Windows, Linux, and Mac. You should consider installing PyMechanical in a virtual environment. For more information, see Python's @@ -35,7 +35,7 @@ machine architecture from the `Releases page Date: Wed, 8 Jan 2025 17:21:03 +0100 Subject: [PATCH 34/44] chore: update CHANGELOG for v0.11.11 (#1031) --- doc/changelog.d/1000.documentation.md | 1 - doc/changelog.d/1001.added.md | 1 - doc/changelog.d/1003.maintenance.md | 1 - doc/changelog.d/1004.maintenance.md | 1 - doc/changelog.d/1005.added.md | 1 - doc/changelog.d/1007.added.md | 1 - doc/changelog.d/1008.maintenance.md | 1 - doc/changelog.d/1009.maintenance.md | 1 - doc/changelog.d/1011.miscellaneous.md | 1 - doc/changelog.d/1014.maintenance.md | 1 - doc/changelog.d/1015.maintenance.md | 1 - doc/changelog.d/1016.maintenance.md | 1 - doc/changelog.d/1020.maintenance.md | 1 - doc/changelog.d/1021.maintenance.md | 1 - doc/changelog.d/1022.added.md | 1 - doc/changelog.d/1023.maintenance.md | 1 - doc/changelog.d/1026.fixed.md | 1 - doc/changelog.d/1029.fixed.md | 1 - doc/changelog.d/1030.fixed.md | 1 - doc/changelog.d/1031.maintenance.md | 1 + doc/changelog.d/983.maintenance.md | 1 - doc/changelog.d/984.maintenance.md | 1 - doc/changelog.d/985.added.md | 1 - doc/changelog.d/986.added.md | 1 - doc/changelog.d/988.maintenance.md | 1 - doc/changelog.d/990.maintenance.md | 1 - doc/changelog.d/991.maintenance.md | 1 - doc/changelog.d/992.maintenance.md | 1 - doc/changelog.d/993.maintenance.md | 1 - doc/changelog.d/997.maintenance.md | 1 - doc/changelog.d/999.maintenance.md | 1 - doc/source/changelog.rst | 55 +++++++++++++++++++++++++++ 32 files changed, 56 insertions(+), 30 deletions(-) delete mode 100644 doc/changelog.d/1000.documentation.md delete mode 100644 doc/changelog.d/1001.added.md delete mode 100644 doc/changelog.d/1003.maintenance.md delete mode 100644 doc/changelog.d/1004.maintenance.md delete mode 100644 doc/changelog.d/1005.added.md delete mode 100644 doc/changelog.d/1007.added.md delete mode 100644 doc/changelog.d/1008.maintenance.md delete mode 100644 doc/changelog.d/1009.maintenance.md delete mode 100644 doc/changelog.d/1011.miscellaneous.md delete mode 100644 doc/changelog.d/1014.maintenance.md delete mode 100644 doc/changelog.d/1015.maintenance.md delete mode 100644 doc/changelog.d/1016.maintenance.md delete mode 100644 doc/changelog.d/1020.maintenance.md delete mode 100644 doc/changelog.d/1021.maintenance.md delete mode 100644 doc/changelog.d/1022.added.md delete mode 100644 doc/changelog.d/1023.maintenance.md delete mode 100644 doc/changelog.d/1026.fixed.md delete mode 100644 doc/changelog.d/1029.fixed.md delete mode 100644 doc/changelog.d/1030.fixed.md create mode 100644 doc/changelog.d/1031.maintenance.md delete mode 100644 doc/changelog.d/983.maintenance.md delete mode 100644 doc/changelog.d/984.maintenance.md delete mode 100644 doc/changelog.d/985.added.md delete mode 100644 doc/changelog.d/986.added.md delete mode 100644 doc/changelog.d/988.maintenance.md delete mode 100644 doc/changelog.d/990.maintenance.md delete mode 100644 doc/changelog.d/991.maintenance.md delete mode 100644 doc/changelog.d/992.maintenance.md delete mode 100644 doc/changelog.d/993.maintenance.md delete mode 100644 doc/changelog.d/997.maintenance.md delete mode 100644 doc/changelog.d/999.maintenance.md diff --git a/doc/changelog.d/1000.documentation.md b/doc/changelog.d/1000.documentation.md deleted file mode 100644 index c8dc88271..000000000 --- a/doc/changelog.d/1000.documentation.md +++ /dev/null @@ -1 +0,0 @@ -Update docs with new api \ No newline at end of file diff --git a/doc/changelog.d/1001.added.md b/doc/changelog.d/1001.added.md deleted file mode 100644 index b592af031..000000000 --- a/doc/changelog.d/1001.added.md +++ /dev/null @@ -1 +0,0 @@ -Update docstring and ``App.save_as()`` \ No newline at end of file diff --git a/doc/changelog.d/1003.maintenance.md b/doc/changelog.d/1003.maintenance.md deleted file mode 100644 index da9752730..000000000 --- a/doc/changelog.d/1003.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -Bump clr-loader from 0.2.6 to 0.2.7.post0 in the core group \ No newline at end of file diff --git a/doc/changelog.d/1004.maintenance.md b/doc/changelog.d/1004.maintenance.md deleted file mode 100644 index dbd883510..000000000 --- a/doc/changelog.d/1004.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -Bump matplotlib from 3.9.3 to 3.10.0 in the doc group \ No newline at end of file diff --git a/doc/changelog.d/1005.added.md b/doc/changelog.d/1005.added.md deleted file mode 100644 index 142ae433d..000000000 --- a/doc/changelog.d/1005.added.md +++ /dev/null @@ -1 +0,0 @@ -Update object state for `print_tree()` \ No newline at end of file diff --git a/doc/changelog.d/1007.added.md b/doc/changelog.d/1007.added.md deleted file mode 100644 index 8f9ce736e..000000000 --- a/doc/changelog.d/1007.added.md +++ /dev/null @@ -1 +0,0 @@ -Option to ignore lock file on open \ No newline at end of file diff --git a/doc/changelog.d/1008.maintenance.md b/doc/changelog.d/1008.maintenance.md deleted file mode 100644 index 416b898e2..000000000 --- a/doc/changelog.d/1008.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -Bump the doc group with 3 updates \ No newline at end of file diff --git a/doc/changelog.d/1009.maintenance.md b/doc/changelog.d/1009.maintenance.md deleted file mode 100644 index 688c442a5..000000000 --- a/doc/changelog.d/1009.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -Bump psutil from 6.1.0 to 6.1.1 \ No newline at end of file diff --git a/doc/changelog.d/1011.miscellaneous.md b/doc/changelog.d/1011.miscellaneous.md deleted file mode 100644 index 975814ed1..000000000 --- a/doc/changelog.d/1011.miscellaneous.md +++ /dev/null @@ -1 +0,0 @@ -Remove f-string without placeholders and specify exception type. \ No newline at end of file diff --git a/doc/changelog.d/1014.maintenance.md b/doc/changelog.d/1014.maintenance.md deleted file mode 100644 index 99aeeef5f..000000000 --- a/doc/changelog.d/1014.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -Update license headers for 2025 \ No newline at end of file diff --git a/doc/changelog.d/1015.maintenance.md b/doc/changelog.d/1015.maintenance.md deleted file mode 100644 index 840c5de26..000000000 --- a/doc/changelog.d/1015.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -Bump ``ansys-mechanical-stubs`` to 0.1.5 and add typehint to DataModel \ No newline at end of file diff --git a/doc/changelog.d/1016.maintenance.md b/doc/changelog.d/1016.maintenance.md deleted file mode 100644 index b0353c841..000000000 --- a/doc/changelog.d/1016.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -Follow pythonic standard for comparison to None. \ No newline at end of file diff --git a/doc/changelog.d/1020.maintenance.md b/doc/changelog.d/1020.maintenance.md deleted file mode 100644 index 55e49ce55..000000000 --- a/doc/changelog.d/1020.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -Bump grpcio from 1.68.1 to 1.69.0 in the core group \ No newline at end of file diff --git a/doc/changelog.d/1021.maintenance.md b/doc/changelog.d/1021.maintenance.md deleted file mode 100644 index 0dee149fd..000000000 --- a/doc/changelog.d/1021.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -Bump sphinx-autodoc-typehints from 2.5.0 to 3.0.0 \ No newline at end of file diff --git a/doc/changelog.d/1022.added.md b/doc/changelog.d/1022.added.md deleted file mode 100644 index fe71d4afa..000000000 --- a/doc/changelog.d/1022.added.md +++ /dev/null @@ -1 +0,0 @@ -Add project directory property \ No newline at end of file diff --git a/doc/changelog.d/1023.maintenance.md b/doc/changelog.d/1023.maintenance.md deleted file mode 100644 index 398f0d3a9..000000000 --- a/doc/changelog.d/1023.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -Update ngihtly for pre-release version \ No newline at end of file diff --git a/doc/changelog.d/1026.fixed.md b/doc/changelog.d/1026.fixed.md deleted file mode 100644 index 7996cc7e0..000000000 --- a/doc/changelog.d/1026.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Process return code \ No newline at end of file diff --git a/doc/changelog.d/1029.fixed.md b/doc/changelog.d/1029.fixed.md deleted file mode 100644 index 7996cc7e0..000000000 --- a/doc/changelog.d/1029.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Process return code \ No newline at end of file diff --git a/doc/changelog.d/1030.fixed.md b/doc/changelog.d/1030.fixed.md deleted file mode 100644 index 5000d5c90..000000000 --- a/doc/changelog.d/1030.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Background App initialization \ No newline at end of file diff --git a/doc/changelog.d/1031.maintenance.md b/doc/changelog.d/1031.maintenance.md new file mode 100644 index 000000000..822e820a0 --- /dev/null +++ b/doc/changelog.d/1031.maintenance.md @@ -0,0 +1 @@ +update CHANGELOG for v0.11.11 \ No newline at end of file diff --git a/doc/changelog.d/983.maintenance.md b/doc/changelog.d/983.maintenance.md deleted file mode 100644 index 17893824c..000000000 --- a/doc/changelog.d/983.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -Bump codecov/codecov-action from 4 to 5 \ No newline at end of file diff --git a/doc/changelog.d/984.maintenance.md b/doc/changelog.d/984.maintenance.md deleted file mode 100644 index 1972c6ecb..000000000 --- a/doc/changelog.d/984.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -update CHANGELOG for v0.11.10 \ No newline at end of file diff --git a/doc/changelog.d/985.added.md b/doc/changelog.d/985.added.md deleted file mode 100644 index 5806c43fe..000000000 --- a/doc/changelog.d/985.added.md +++ /dev/null @@ -1 +0,0 @@ -Add tests for transaction \ No newline at end of file diff --git a/doc/changelog.d/986.added.md b/doc/changelog.d/986.added.md deleted file mode 100644 index c7236f515..000000000 --- a/doc/changelog.d/986.added.md +++ /dev/null @@ -1 +0,0 @@ -Update private app data creation and add tests \ No newline at end of file diff --git a/doc/changelog.d/988.maintenance.md b/doc/changelog.d/988.maintenance.md deleted file mode 100644 index 83a8797ce..000000000 --- a/doc/changelog.d/988.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -Bump ansys-sphinx-theme[autoapi] from 1.2.1 to 1.2.2 in the doc group \ No newline at end of file diff --git a/doc/changelog.d/990.maintenance.md b/doc/changelog.d/990.maintenance.md deleted file mode 100644 index cb92f6c00..000000000 --- a/doc/changelog.d/990.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -Bump grpcio from 1.68.0 to 1.68.1 in the core group \ No newline at end of file diff --git a/doc/changelog.d/991.maintenance.md b/doc/changelog.d/991.maintenance.md deleted file mode 100644 index 0798d65b6..000000000 --- a/doc/changelog.d/991.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -Bump pytest from 8.3.3 to 8.3.4 in the tests group \ No newline at end of file diff --git a/doc/changelog.d/992.maintenance.md b/doc/changelog.d/992.maintenance.md deleted file mode 100644 index 46cb383cb..000000000 --- a/doc/changelog.d/992.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -Bump the doc group with 2 updates \ No newline at end of file diff --git a/doc/changelog.d/993.maintenance.md b/doc/changelog.d/993.maintenance.md deleted file mode 100644 index c655a0905..000000000 --- a/doc/changelog.d/993.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -pre-commit automatic update \ No newline at end of file diff --git a/doc/changelog.d/997.maintenance.md b/doc/changelog.d/997.maintenance.md deleted file mode 100644 index a4053eacd..000000000 --- a/doc/changelog.d/997.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -Support python 3.13 \ No newline at end of file diff --git a/doc/changelog.d/999.maintenance.md b/doc/changelog.d/999.maintenance.md deleted file mode 100644 index 46cb383cb..000000000 --- a/doc/changelog.d/999.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -Bump the doc group with 2 updates \ No newline at end of file diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index e9cd696b9..4db155c06 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -9,6 +9,61 @@ This document contains the release notes for the project. .. towncrier release notes start +`0.11.11 `_ - 2025-01-08 +===================================================================================== + +Added +^^^^^ + +- Add tests for transaction `#985 `_ +- Update private app data creation and add tests `#986 `_ +- Update docstring and ``App.save_as()`` `#1001 `_ +- Update object state for `print_tree()` `#1005 `_ +- Option to ignore lock file on open `#1007 `_ +- Add project directory property `#1022 `_ + + +Fixed +^^^^^ + +- Process return code `#1026 `_, `#1029 `_ +- Background App initialization `#1030 `_ + + +Miscellaneous +^^^^^^^^^^^^^ + +- Remove f-string without placeholders and specify exception type. `#1011 `_ + + +Documentation +^^^^^^^^^^^^^ + +- Update docs with new api `#1000 `_ + + +Maintenance +^^^^^^^^^^^ + +- Bump codecov/codecov-action from 4 to 5 `#983 `_ +- update CHANGELOG for v0.11.10 `#984 `_ +- Bump ansys-sphinx-theme[autoapi] from 1.2.1 to 1.2.2 in the doc group `#988 `_ +- Bump grpcio from 1.68.0 to 1.68.1 in the core group `#990 `_ +- Bump pytest from 8.3.3 to 8.3.4 in the tests group `#991 `_ +- Bump the doc group with 2 updates `#992 `_, `#999 `_ +- pre-commit automatic update `#993 `_ +- Support python 3.13 `#997 `_ +- Bump clr-loader from 0.2.6 to 0.2.7.post0 in the core group `#1003 `_ +- Bump matplotlib from 3.9.3 to 3.10.0 in the doc group `#1004 `_ +- Bump the doc group with 3 updates `#1008 `_ +- Bump psutil from 6.1.0 to 6.1.1 `#1009 `_ +- Update license headers for 2025 `#1014 `_ +- Bump ``ansys-mechanical-stubs`` to 0.1.5 and add typehint to DataModel `#1015 `_ +- Follow pythonic standard for comparison to None. `#1016 `_ +- Bump grpcio from 1.68.1 to 1.69.0 in the core group `#1020 `_ +- Bump sphinx-autodoc-typehints from 2.5.0 to 3.0.0 `#1021 `_ +- Update ngihtly for pre-release version `#1023 `_ + `0.11.10 `_ - 2024-11-18 ===================================================================================== From 205f86c70a5d813489cf69919ff07a3eedcf400d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 19:27:19 +0000 Subject: [PATCH 35/44] CHORE: Bump the doc group with 2 updates (#1036) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/1036.maintenance.md | 1 + pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 doc/changelog.d/1036.maintenance.md diff --git a/doc/changelog.d/1036.maintenance.md b/doc/changelog.d/1036.maintenance.md new file mode 100644 index 000000000..46cb383cb --- /dev/null +++ b/doc/changelog.d/1036.maintenance.md @@ -0,0 +1 @@ +Bump the doc group with 2 updates \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index c9025b39e..007bdebb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,7 @@ tests = [ ] doc = [ "sphinx==8.1.3", - "ansys-sphinx-theme[autoapi]==1.2.4", + "ansys-sphinx-theme[autoapi]==1.2.6", "grpcio==1.69.0", "imageio-ffmpeg==0.5.1", "imageio==2.36.1", @@ -70,7 +70,7 @@ doc = [ "pandas==2.2.3", "panel==1.5.5", "plotly==5.24.1", - "pypandoc==1.14", + "pypandoc==1.15", "pytest-sphinx==0.6.3", "pythreejs==2.4.2", "pyvista>=0.39.1", From 1a439b582456a6f55b4739ade9bbe4f9c39de46b Mon Sep 17 00:00:00 2001 From: Mohamed Koubaa Date: Mon, 13 Jan 2025 15:17:55 -0600 Subject: [PATCH 36/44] feat: add poster method that raises an exception (#1038) Co-authored-by: Mohamed Koubaa Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/1038.added.md | 1 + src/ansys/mechanical/core/embedding/poster.py | 34 ++++++++++++++++++- tests/embedding/test_app.py | 15 +++++++- 3 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 doc/changelog.d/1038.added.md diff --git a/doc/changelog.d/1038.added.md b/doc/changelog.d/1038.added.md new file mode 100644 index 000000000..093e54d4f --- /dev/null +++ b/doc/changelog.d/1038.added.md @@ -0,0 +1 @@ +add poster method that raises an exception \ No newline at end of file diff --git a/src/ansys/mechanical/core/embedding/poster.py b/src/ansys/mechanical/core/embedding/poster.py index 93a994cc0..c9ccd1ae8 100644 --- a/src/ansys/mechanical/core/embedding/poster.py +++ b/src/ansys/mechanical/core/embedding/poster.py @@ -25,6 +25,19 @@ import typing +class PosterError(Exception): + """Class which holds errors from the background thread posting system.""" + + def __init__(self, error: Exception): + """Create an instance to hold the given error.""" + self._error = error + + @property + def error(self) -> Exception: + """Get the underlying exception.""" + return self._error + + class Poster: """Class which can post a python callable function to Mechanical's main thread.""" @@ -37,7 +50,26 @@ def __init__(self): self._poster = Ans.Common.WB1ManagedUtils.TaskPoster - def post(self, callable: typing.Callable): + def try_post(self, callable: typing.Callable) -> typing.Any: + """Post the callable to Mechanical's main thread. + + This does the same thing as `post` but if `callable` + raises an exception, try_post will raise the same + exception to the caller of `try_post`. + """ + + def wrapped(): + try: + return callable() + except Exception as e: + return PosterError(e) + + result = self.post(wrapped) + if isinstance(result, PosterError): + raise result.error + return result + + def post(self, callable: typing.Callable) -> typing.Any: """Post the callable to Mechanical's main thread. The main thread needs to be receiving posted messages diff --git a/tests/embedding/test_app.py b/tests/embedding/test_app.py index 4d4f01a90..910d83c8d 100644 --- a/tests/embedding/test_app.py +++ b/tests/embedding/test_app.py @@ -169,6 +169,7 @@ def test_app_poster(embedded_app): poster = embedded_app.poster name = [] + error = [] def change_name_async(poster): """Change_name_async will run a background thread @@ -182,9 +183,19 @@ def get_name(): def change_name(): embedded_app.DataModel.Project.Name = "foo" + def raise_ex(): + raise Exception("Exception") + name.append(poster.post(get_name)) poster.post(change_name) + try: + poster.try_post() + except Exception as e: + error.append(e) + + name.append(poster.try_post(get_name)) + import threading change_name_thread = threading.Thread(target=change_name_async, args=(poster,)) @@ -196,9 +207,11 @@ def change_name(): # thread, e.g. `change_name` that was posted by the poster. utils.sleep(400) change_name_thread.join() - assert len(name) == 1 + assert len(name) == 2 assert name[0] == "Project" + assert name[1] == "foo" assert embedded_app.DataModel.Project.Name == "foo" + assert len(error) == 1 @pytest.mark.embedding From 2ee94116bbe031257f9def24f5272ca02a1f9f9b Mon Sep 17 00:00:00 2001 From: Dipin <26918585+dipinknair@users.noreply.github.com> Date: Mon, 13 Jan 2025 16:33:02 -0600 Subject: [PATCH 37/44] FEAT: Update enum and globals (#1037) Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/1037.added.md | 1 + src/ansys/mechanical/core/embedding/enum_importer.py | 2 +- src/ansys/mechanical/core/embedding/imports.py | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 doc/changelog.d/1037.added.md diff --git a/doc/changelog.d/1037.added.md b/doc/changelog.d/1037.added.md new file mode 100644 index 000000000..741cac2d8 --- /dev/null +++ b/doc/changelog.d/1037.added.md @@ -0,0 +1 @@ +Update enum and globals \ No newline at end of file diff --git a/src/ansys/mechanical/core/embedding/enum_importer.py b/src/ansys/mechanical/core/embedding/enum_importer.py index 059a294cc..61eca5605 100644 --- a/src/ansys/mechanical/core/embedding/enum_importer.py +++ b/src/ansys/mechanical/core/embedding/enum_importer.py @@ -33,5 +33,5 @@ from Ansys.ACT.Interfaces.Common import * # noqa isort: skip from Ansys.Mechanical.DataModel.Enums import * # noqa isort: skip - +from Ansys.ACT.Interfaces.Analysis import * # noqa isort: skip import Ansys # noqa isort: skip diff --git a/src/ansys/mechanical/core/embedding/imports.py b/src/ansys/mechanical/core/embedding/imports.py index 0e0f42879..60ec3351f 100644 --- a/src/ansys/mechanical/core/embedding/imports.py +++ b/src/ansys/mechanical/core/embedding/imports.py @@ -55,6 +55,7 @@ def global_variables(app: "ansys.mechanical.core.App", enums: bool = False) -> t # When ansys-pythonnet issue #14 is fixed, uncomment above from Ansys.ACT.Core.Math import Point2D, Point3D from Ansys.ACT.Math import Vector3D + from Ansys.ACT.Mechanical.Fields import VariableDefinitionType from Ansys.Core.Units import Quantity from Ansys.Mechanical.DataModel import MechanicalEnums from Ansys.Mechanical.Graphics import Point, SectionPlane @@ -74,6 +75,7 @@ def global_variables(app: "ansys.mechanical.core.App", enums: bool = False) -> t vars["Point2D"] = Point2D vars["Point3D"] = Point3D vars["Vector3D"] = Vector3D + vars["VariableDefinitionType"] = VariableDefinitionType if enums: vars.update(get_all_enums()) From 62c55a622380c72a612b036d55f92748e987aff2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 14 Jan 2025 17:38:27 +0100 Subject: [PATCH 38/44] chore: pre-commit automatic update (#1039) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Co-authored-by: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Co-authored-by: Kerry McAdams <58492561+klmcadams@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- doc/changelog.d/1039.maintenance.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 doc/changelog.d/1039.maintenance.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a45dce36e..d3867dab0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -76,6 +76,6 @@ repos: - id: check-added-large-files - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.30.0 + rev: 0.31.0 hooks: - id: check-github-workflows \ No newline at end of file diff --git a/doc/changelog.d/1039.maintenance.md b/doc/changelog.d/1039.maintenance.md new file mode 100644 index 000000000..c655a0905 --- /dev/null +++ b/doc/changelog.d/1039.maintenance.md @@ -0,0 +1 @@ +pre-commit automatic update \ No newline at end of file From 9364aafd4ea894a1a4514528a908c9c034aaa5af Mon Sep 17 00:00:00 2001 From: Kerry McAdams <58492561+klmcadams@users.noreply.github.com> Date: Tue, 14 Jan 2025 19:17:07 -0500 Subject: [PATCH 39/44] chore: Bump `ansys-mechanical-stubs` from 0.1.5 to 0.1.6 (#1044) Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/1044.maintenance.md | 1 + pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 doc/changelog.d/1044.maintenance.md diff --git a/doc/changelog.d/1044.maintenance.md b/doc/changelog.d/1044.maintenance.md new file mode 100644 index 000000000..6567a4e14 --- /dev/null +++ b/doc/changelog.d/1044.maintenance.md @@ -0,0 +1 @@ +Bump `ansys-mechanical-stubs` from 0.1.5 to 0.1.6 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 007bdebb5..ffe8cfad8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ classifiers = [ dependencies = [ "ansys-api-mechanical==0.1.2", "ansys-mechanical-env==0.1.8", - "ansys-mechanical-stubs==0.1.5", + "ansys-mechanical-stubs==0.1.6", "ansys-platform-instancemanagement>=1.0.1", "ansys-pythonnet>=3.1.0rc2", "ansys-tools-path>=0.3.1", From e3faf2b7bef411ae3b05965fdd351e9d3553b1f8 Mon Sep 17 00:00:00 2001 From: Dipin <26918585+dipinknair@users.noreply.github.com> Date: Wed, 15 Jan 2025 12:20:19 -0600 Subject: [PATCH 40/44] CHORE: Bump `ansys-mechanical-env` version from `0.1.8` to `0.1.9` (#1048) Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/1048.maintenance.md | 1 + pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 doc/changelog.d/1048.maintenance.md diff --git a/doc/changelog.d/1048.maintenance.md b/doc/changelog.d/1048.maintenance.md new file mode 100644 index 000000000..bd87b206c --- /dev/null +++ b/doc/changelog.d/1048.maintenance.md @@ -0,0 +1 @@ +Bump `ansys-mechanical-env` version from `0.1.8` to `0.1.9` \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index ffe8cfad8..4f67dadd6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ classifiers = [ ] dependencies = [ "ansys-api-mechanical==0.1.2", - "ansys-mechanical-env==0.1.8", + "ansys-mechanical-env==0.1.9", "ansys-mechanical-stubs==0.1.6", "ansys-platform-instancemanagement>=1.0.1", "ansys-pythonnet>=3.1.0rc2", From c94c2b57b7c1a2b6e52c3c0164624e354c32ea8d Mon Sep 17 00:00:00 2001 From: Dipin <26918585+dipinknair@users.noreply.github.com> Date: Wed, 15 Jan 2025 15:30:12 -0600 Subject: [PATCH 41/44] FEAT: docker and ci/cd change for 25R1 (#1042) Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Co-authored-by: Kerry McAdams <58492561+klmcadams@users.noreply.github.com> --- .github/workflows/ci_cd.yml | 6 +- doc/changelog.d/1042.added.md | 1 + doc/source/getting_started/docker.rst | 2 +- docker/251/Dockerfile | 111 ++++++++ docker/251/dockerignore | 370 ++++++++++++++++++++++++++ docker/docker-compose.yml | 116 ++++---- docker/make_container.rst | 276 +++++++++---------- 7 files changed, 683 insertions(+), 199 deletions(-) create mode 100644 doc/changelog.d/1042.added.md create mode 100644 docker/251/Dockerfile create mode 100644 docker/251/dockerignore diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 735a64271..ecf7afd3a 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -13,7 +13,7 @@ on: - '241' - '232' description: 'The Mechanical revision number to run tests on.' - default: '242' #stable version is 242, must match $stable_container + default: '251' #stable version is 251, must match $stable_container schedule: - cron: '00 22 * * *' # UTC time, may start 5-15 mins later than scheduled time # registry_package: @@ -37,7 +37,7 @@ env: DOCUMENTATION_CNAME: mechanical.docs.pyansys.com MAIN_PYTHON_VERSION: '3.13' # DEV_REVN & its Docker image are used in scheduled or registry package runs - STABLE_REVN: '242' + STABLE_REVN: '251' DEV_REVN: '252' LICENSE_SERVER: ${{ secrets.LICENSE_SERVER }} ANSYSLMD_LICENSE_FILE: 1055@${{ secrets.LICENSE_SERVER }} @@ -172,7 +172,7 @@ jobs: run: | # if a tag(release) is pushed, test all versions if ${{ github.event_name == 'push' }} && ${{ contains(github.ref, 'refs/tags') }}; then - echo "matrix={\"mechanical-version\":['23.2.0', '24.1.0', '24.2.0'],\"experimental\":[false]}" >> $GITHUB_OUTPUT + echo "matrix={\"mechanical-version\":['23.2.0', '24.1.0', '24.2.0', '25.1.0'],\"experimental\":[false]}" >> $GITHUB_OUTPUT else echo "matrix={\"mechanical-version\":['${{ needs.revn-variations.outputs.test_docker_image_version }}'],\"experimental\":[false]}" >> $GITHUB_OUTPUT fi diff --git a/doc/changelog.d/1042.added.md b/doc/changelog.d/1042.added.md new file mode 100644 index 000000000..191f1e201 --- /dev/null +++ b/doc/changelog.d/1042.added.md @@ -0,0 +1 @@ +docker and ci/cd change for 25R1 \ No newline at end of file diff --git a/doc/source/getting_started/docker.rst b/doc/source/getting_started/docker.rst index cf696fbbd..659106915 100644 --- a/doc/source/getting_started/docker.rst +++ b/doc/source/getting_started/docker.rst @@ -53,7 +53,7 @@ Launch Mechanical with this code: .. code:: LICENSE_SERVER=1055@XXX.XXX.XXX.XXX - VERSION=v24.2.0 + VERSION=v25.1.0 IMAGE=ghcr.io/ansys/pymechanical/mechanical:$VERSION docker run -e ANSYSLMD_LICENSE_FILE=$LICENSE_SERVER -p ip:10000:10000 $IMAGE diff --git a/docker/251/Dockerfile b/docker/251/Dockerfile new file mode 100644 index 000000000..95efe8fc4 --- /dev/null +++ b/docker/251/Dockerfile @@ -0,0 +1,111 @@ +FROM ubuntu:20.04 + +ARG VERSION + +# LABELS +LABEL description="Mechanical on Ubuntu" +LABEL email="pyansys.core@ansys.com" + +# OCI LABELS +LABEL org.opencontainers.image.documentation="https://mechanical.docs.pyansys.com" + +RUN apt-get update && apt-get install -y \ + libxt6 \ + libsm6 \ + libice6 \ + libx11-6 \ + libx11-xcb1 \ + libxext6 \ + openssl \ + openssh-client \ + libcairo2 \ + elfutils \ + expat \ + fontconfig \ + libglib2.0-0 \ + libc-bin \ + libc6 \ + jbigkit-bin \ + libxau6 \ + libxcursor1 \ + libxdamage1 \ + libxext6 \ + libxfixes3 \ + libxrender1 \ + libxxf86vm1 \ + libxi6 \ + libdrm-common \ + libdrm2 \ + libjpeg-turbo8 \ + libselinux1 \ + libtiff5 \ + libxcb1 \ + libxshmfence1 \ + libglx-mesa0 \ + libosmesa6 \ + libgl1 \ + libgl1-mesa-dri \ + libgl1-mesa-glx \ + libglu1 \ + libglu1-mesa \ + libegl1-mesa \ + libglapi-mesa \ + libpcre2-8-0 \ + libpixman-1-0 \ + libmotif-common \ + libxm4 \ + libgfortran4 \ + libgfortran5 \ + xvfb \ + tini \ + ca-certificates \ + libgomp1 \ + libtirpc3 \ + locales \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* \ + && locale-gen en_US.UTF-8 \ + && locale-gen fr_FR.utf8 \ + && locale-gen de_DE.utf8 \ + && locale-gen ja_JP.utf8 \ + && locale-gen zh_CN.utf8 + +# Copying files +WORKDIR /install/ + +COPY . . + +# License server +# (Optional) +# ENV LICENSE_SERVER=111.222.333.444 +# ENV ANSYSLMD_LICENSE_FILE=1055@$LICENSE_SERVER + +ENV ANSYS_VERSION=${VERSION} \ + ANSYS${VERSION}_DIR=/install/ansys_inc/v${VERSION}/ansys \ + AWP_ROOT${VERSION}=/install/ansys_inc/v${VERSION} \ + CADOE_LIBDIR${VERSION}=/install/ansys_inc/v${VERSION}/commonfiles/Language/en-us \ + ANSYS_WORKBENCH_LOGGING=1 \ + ANSYS_WORKBENCH_LOGGING_FILTER_LEVEL=0 \ + ANSYS_WORKBENCH_LOGGING_AUTO_FLUSH=1 \ + ANSYS_WORKBENCH_LOGGING_CONSOLE=1 \ + ANSYS_WORKBENCH_LOGGING_DIRECTORY=/tmp \ + LANG=POSIX \ + # Ans.EDServices.MaterialCOM.dll dependencies + LD_LIBRARY_PATH=/install/ansys_inc/v${VERSION}/tp/IntelCompiler/2023.1.0/linx64:${LD_LIBRARY_PATH} \ + LD_LIBRARY_PATH=/install/ansys_inc/v${VERSION}/tp/qt/5.15.16//Linux64:${LD_LIBRARY_PATH} \ + #umpe dependencies for Ans.Post.UmpeCOM.dll + LD_LIBRARY_PATH=/install/ansys_inc/v${VERSION}/aisol/umpe/common/bin/linx64:${LD_LIBRARY_PATH} \ + LD_LIBRARY_PATH=/install/ansys_inc/v${VERSION}/tp/hdf5/1_12_2/linx64:${LD_LIBRARY_PATH} \ + # need to use EGL with this image's version of Mesa (version might be fine but I think the package was compiled with some flags set differently than what is required, causing offscreen renderings to only show the background blue-white gradient; need to use EGL to overcome it) + ANS_WB_FORCE_EGL=1 \ + MECHANICAL_ON_DOCKER=TRUE \ + OMPI_ALLOW_RUN_AS_ROOT=1 \ + OMPI_ALLOW_RUN_AS_ROOT_CONFIRM=1 + +# expose port for grpc +EXPOSE 10000 + +# Set working directory +WORKDIR /install/ansys_inc/v${VERSION}/aisol/ + +ENTRYPOINT ["tini", "--", "xvfb-run", "./.workbench", "-dsapplet", "-AppModeMech", "-b", "-grpc", "10000"] \ No newline at end of file diff --git a/docker/251/dockerignore b/docker/251/dockerignore new file mode 100644 index 000000000..e497fab6e --- /dev/null +++ b/docker/251/dockerignore @@ -0,0 +1,370 @@ +# combine files needed for mechanical and mapdl + +# ignore all .a files ( static libraries ) +**/*.a + +# ignore the .txt files +*.txt + +# ignore the log files +ansys_inc/*.log + +# ignore the license file +ansys_inc/shared_files/licensing/ansyslmd.ini + +ansys_inc/v251/ACP + +ansys_inc/v251/Addins +!ansys_inc/v251/Addins/ACT +!ansys_inc/v251/Addins/EngineeringData +!ansys_inc/v251/Addins/JobManager +!ansys_inc/v251/Addins/JobMgrAdapter +!ansys_inc/v251/Addins/Units + +ansys_inc/v251/aisol/BladeModeler +ansys_inc/v251/aisol/FEModeler + +ansys_inc/v251/aisol/dll/linx64/libans.blademodeler.ndfutilitiescom.so +ansys_inc/v251/aisol/dll/linx64/libans.blademodeler.bg41objectscom.so +ansys_inc/v251/aisol/dll/linx64/ans.blademodeler.bg41objectscom.rsb +ansys_inc/v251/aisol/dll/linx64/libans.blademodeler.tgproxycom.so +ansys_inc/v251/aisol/dll/linx64/ans.blademodeler.tgproxycom.rsb +ansys_inc/v251/aisol/dll/linx64/libans.blademodeler.objectscom.so +ansys_inc/v251/aisol/dll/linx64/ans.blademodeler.objectscom.rsb + +ansys_inc/v251/aisol/dll/linx64/libans.addins.parameshkernelcom.so +ansys_inc/v251/aisol/dll/linx64/ans.addins.parameshkernelcom.rsb +ansys_inc/v251/aisol/dll/linx64/libans.addins.parameshobjectscom.so +ansys_inc/v251/aisol/dll/linx64/ans.addins.parameshobjectscom.rsb + +ansys_inc/v251/Additive + +ansys_inc/v251/commonfiles/CAD/TechSoft3D +!ansys_inc/v251/commonfiles/CAD/TechSoft3D/HoopsExchange2024.6.0 + +ansys_inc/v251/commonfiles/CAD/Spatial +!ansys_inc/v251/commonfiles/CAD/Spatial/iop/linx64/code/bin/libtbbmalloc.so.2 + +ansys_inc/v251/commonfiles/CPython +!ansys_inc/v251/commonfiles/CPython/3_10 + +ansys_inc/v251/commonfiles/SystemCoupling + +ansys_inc/v251/commonfiles/Tcl +!ansys_inc/v251/commonfiles/Tcl/lib/linx64/libjpeg.so +!ansys_inc/v251/commonfiles/Tcl/lib/linx64/libpng.so +!ansys_inc/v251/commonfiles/Tcl/lib/linx64/libtiff.so +!ansys_inc/v251/commonfiles/Tcl/lib/linx64/libz.so + +ansys_inc/v251/commonfiles/CFX +!ansys_inc/v251/commonfiles/CFX/lib/linux-amd64/libans.cue.di.so +!ansys_inc/v251/commonfiles/CFX/lib/linux-amd64/libgfortran.so.5 +!ansys_inc/v251/commonfiles/CFX/lib/linux-amd64/libquadmath.so.0 +!ansys_inc/v251/commonfiles/CFX/lib/linux-amd64/libnsl.so.2 +!ansys_inc/v251/commonfiles/CFX/lib/linux-amd64/libnsl.so.2.0.0 +!ansys_inc/v251/commonfiles/CFX/tools/fluentio/linx64/lib/libfluentio.so +!ansys_inc/v251/commonfiles/CFX/tools/perl-5.34.3-dynamic/lib/5.34.3/x86_64-linux/CORE/libperl.so +!ansys_inc/v251/commonfiles/CFX/tools/poco-1.12.4/linx64/lib/libPocoFoundation.so +!ansys_inc/v251/commonfiles/CFX/tools/poco-1.12.4/linx64/lib/libPocoFoundation.so.94 + +ansys_inc/v251/commonfiles/Textures +ansys_inc/v251/commonfiles/tools +ansys_inc/v251/dcs +!ansys_inc/v251/dcs/nginx/auth_web/assets/favicon.ico + +ansys_inc/v251/Framework +!ansys_inc/v251/Framework/bin + +ansys_inc/v251/Images + +ansys_inc/v251/meshing + +ansys_inc/v251/RSM + +ansys_inc/v251/SystemCoupling + +ansys_inc/v251/ProductConfig.sh +ansys_inc/v251/ans_uninstall251 + +ansys_inc/v251/ansys +!ansys_inc/v251/ansys/syslib/ubuntu/libXp.so +!ansys_inc/v251/ansys/syslib/ubuntu/libXp.so.6 +!ansys_inc/v251/ansys/syslib/ubuntu/libXp.so.6.2.0 +!ansys_inc/v251/ansys/syslib/daal/libonedal_core.so +!ansys_inc/v251/ansys/syslib/daal/libonedal_core.so.1 +!ansys_inc/v251/ansys/syslib/daal/libonedal_core.so.1.1 +!ansys_inc/v251/ansys/syslib/daal/libonedal_thread.so +!ansys_inc/v251/ansys/syslib/daal/libonedal_thread.so.1 +!ansys_inc/v251/ansys/syslib/daal/libonedal_thread.so.1.1 +!ansys_inc/v251/ansys/syslib/daal/libtbb.so +!ansys_inc/v251/ansys/syslib/daal/libtbb.so.12 +!ansys_inc/v251/ansys/syslib/daal/libtbb.so.12.9 +!ansys_inc/v251/ansys/syslib/daal/libtbbmalloc.so +!ansys_inc/v251/ansys/syslib/daal/libtbbmalloc.so.2 +!ansys_inc/v251/ansys/syslib/daal/libtbbmalloc.so.2.9 +!ansys_inc/v251/ansys/syslib/openssl/libcrypto.so +!ansys_inc/v251/ansys/syslib/openssl/libcrypto-anss.so +!ansys_inc/v251/ansys/syslib/openssl/libcrypto-anss.so.3 +!ansys_inc/v251/ansys/syslib/openssl/libssl.so +!ansys_inc/v251/ansys/syslib/openssl/libssl-anss.so +!ansys_inc/v251/ansys/syslib/openssl/libssl-anss.so.3 + +!ansys_inc/v251/ansys/bin/mpitest +!ansys_inc/v251/ansys/bin/mpitest251 + +!ansys_inc/v251/ansys/lib/linx64/blas + +!ansys_inc/v251/aisol/lib/linx64/libgcc_s.so.1 +!ansys_inc/v251/aisol/lib/linx64/libaddress_sorting.so +!ansys_inc/v251/aisol/lib/linx64/libgpr.so +!ansys_inc/v251/aisol/lib/linx64/libgrpc.so +!ansys_inc/v251/ansys/apdl +!ansys_inc/v251/ansys/apdl/start.ans +!ansys_inc/v251/ansys/bin/ansset.ini +!ansys_inc/v251/ansys/bin/anssh.ini +!ansys_inc/v251/ansys/bin/ansys251 +!ansys_inc/v251/ansys/bin/ansupf +!ansys_inc/v251/ansys/bin/ansupf251 +!ansys_inc/v251/ansys/bin/ansysdis +!ansys_inc/v251/ansys/bin/ansysdis251 +!ansys_inc/v251/ansys/bin/linx64/ansys.e + +!ansys_inc/v251/ansys/bin/linx64/AnsMechSolverMesh.e +!ansys_inc/v251/ansys/bin/linx64/AnsMechSolverMesh_f.e +!ansys_inc/v251/ansys/bin/linx64/RunQMorph.e +!ansys_inc/v251/ansys/bin/linx64/RunQMorph_f.e + +!ansys_inc/v251/ansys/bin/linx64/lsdyna*.e + +!ansys_inc/v251/ansys/bin/mapdl +!ansys_inc/v251/ansys/docu/dynprompt251.ans +!ansys_inc/v251/ansys/gui/en-us/UIDL/MECHTOOL.AUI +!ansys_inc/v251/ansys/gui/en-us/UIDL/menulist.ans +!ansys_inc/v251/ansys/gui/en-us/UIDL/UIFUNC1.GRN +!ansys_inc/v251/ansys/gui/en-us/UIDL/UIFUNC2.GRN +!ansys_inc/v251/ansys/gui/en-us/UIDL/UIMENU.GRN + +!ansys_inc/v251/ansys/lib/analytics/model_031323/sparse_A_min_max.txt +!ansys_inc/v251/ansys/lib/analytics/model_031323/sparse_A_param_a.txt +!ansys_inc/v251/ansys/lib/analytics/model_031323/sparse_A_param_b.txt +!ansys_inc/v251/ansys/lib/analytics/model_031323/sparse_memory_keras2cpp_model_0.model +!ansys_inc/v251/ansys/lib/analytics/model_031323/sparse_memory_keras2cpp_model_1.model +!ansys_inc/v251/ansys/lib/analytics/model_031323/sparse_memory_keras2cpp_model_2.model +!ansys_inc/v251/ansys/lib/analytics/model_031323/sparse_memory_keras2cpp_model_3.model +!ansys_inc/v251/ansys/lib/analytics/model_031323/sparse_memory_keras2cpp_model_4.model +!ansys_inc/v251/ansys/lib/analytics/model_031323/sparse_memory_mean_0.txt +!ansys_inc/v251/ansys/lib/analytics/model_031323/sparse_memory_mean_1.txt +!ansys_inc/v251/ansys/lib/analytics/model_031323/sparse_memory_mean_2.txt +!ansys_inc/v251/ansys/lib/analytics/model_031323/sparse_memory_mean_3.txt +!ansys_inc/v251/ansys/lib/analytics/model_031323/sparse_memory_mean_4.txt +!ansys_inc/v251/ansys/lib/analytics/model_031323/sparse_memory_scale_0.txt +!ansys_inc/v251/ansys/lib/analytics/model_031323/sparse_memory_scale_1.txt +!ansys_inc/v251/ansys/lib/analytics/model_031323/sparse_memory_scale_2.txt +!ansys_inc/v251/ansys/lib/analytics/model_031323/sparse_memory_scale_3.txt +!ansys_inc/v251/ansys/lib/analytics/model_031323/sparse_memory_scale_4.txt + +!ansys_inc/v251/ansys/lib/linx64/libansUtils.so +!ansys_inc/v251/ansys/lib/linx64/libgeom_api.so +!ansys_inc/v251/ansys/lib/linx64/libami.so +!ansys_inc/v251/ansys/lib/linx64/libansexb.so +!ansys_inc/v251/ansys/lib/linx64/libansgil.so +!ansys_inc/v251/ansys/lib/linx64/libansGPU.so +!ansys_inc/v251/ansys/lib/linx64/libansHDF.so +!ansys_inc/v251/ansys/lib/linx64/libansMathUtils.so +!ansys_inc/v251/ansys/lib/linx64/libansMemManager.so +!ansys_inc/v251/ansys/lib/linx64/libansMETIS.so +!ansys_inc/v251/ansys/lib/linx64/libansMPI.so +!ansys_inc/v251/ansys/lib/linx64/libansMPIinterface.so +!ansys_inc/v251/ansys/lib/linx64/libansOpenMP.so +!ansys_inc/v251/ansys/lib/linx64/libansOpenSSL.so +!ansys_inc/v251/ansys/lib/linx64/libansParMETIS.so +!ansys_inc/v251/ansys/lib/linx64/libansPrintf.so +!ansys_inc/v251/ansys/lib/linx64/libansResourcePredict.so +!ansys_inc/v251/ansys/lib/linx64/libansScaLAPACK.so +!ansys_inc/v251/ansys/lib/linx64/libansuser.so +!ansys_inc/v251/ansys/lib/linx64/libansys.so +!ansys_inc/v251/ansys/lib/linx64/libansysb.so +!ansys_inc/v251/ansys/lib/linx64/libansysx.so +!ansys_inc/v251/ansys/lib/linx64/libboe.so +!ansys_inc/v251/ansys/lib/linx64/libApipWrapper.so +!ansys_inc/v251/ansys/lib/linx64/libcadoe_algorithms.so +!ansys_inc/v251/ansys/lib/linx64/libcdbtransformer.so +!ansys_inc/v251/ansys/lib/linx64/libcgns.so.4.3 +!ansys_inc/v251/ansys/lib/linx64/libchap.so +!ansys_inc/v251/ansys/lib/linx64/libcif.so +!ansys_inc/v251/ansys/lib/linx64/libCInterpolation.so +!ansys_inc/v251/ansys/lib/linx64/libCKernel.so +!ansys_inc/v251/ansys/lib/linx64/libCLegacy.so +!ansys_inc/v251/ansys/lib/linx64/libCMath.so +!ansys_inc/v251/ansys/lib/linx64/libCReaders.so +!ansys_inc/v251/ansys/lib/linx64/libCReadersExt.so +!ansys_inc/v251/ansys/lib/linx64/libdmumps.so +!ansys_inc/v251/ansys/lib/linx64/libdsp.so +!ansys_inc/v251/ansys/lib/linx64/libmnf.so +!ansys_inc/v251/ansys/lib/linx64/libmumps_common.so +!ansys_inc/v251/ansys/lib/linx64/liboctree-mesh.so +!ansys_inc/v251/ansys/lib/linx64/libPrimeMesh.so +!ansys_inc/v251/ansys/lib/linx64/libansBiolib.so +!ansys_inc/v251/ansys/lib/linx64/libqhull.so +!ansys_inc/v251/ansys/lib/linx64/libspooles.so +!ansys_inc/v251/ansys/lib/linx64/libtg.so +!ansys_inc/v251/ansys/lib/linx64/libvtk.so +!ansys_inc/v251/ansys/lib/linx64/libxox.so +!ansys_inc/v251/ansys/lib/linx64/libzmumps.so + +!ansys_inc/v251/ansys/lib/linx64/lapack/intel/libansLAPACK.so +!ansys_inc/v251/ansys/lib/linx64/lapack/amd/libansLAPACK.so + +!ansys_inc/v251/ansys/lib/linx64/mpi/intelmpi/libansMPI.so +!ansys_inc/v251/ansys/lib/linx64/mpi/intelmpi/libansScaLAPACK.so +!ansys_inc/v251/ansys/lib/linx64/mpi/openmpi/libansScaLAPACK.so +!ansys_inc/v251/ansys/lib/linx64/mpi/openmpi/libansMPI.so + +!ansys_inc/v251/ansys/site + +!ansys_inc/v251/ansys/syslib/AnsGil/libgmp.so +!ansys_inc/v251/ansys/syslib/AnsGil/libgmp.so.10 +!ansys_inc/v251/ansys/syslib/AnsGil/libgmp.so.10.3.2 +!ansys_inc/v251/ansys/syslib/AnsMechSolverMesh/libComponentSystem.so +!ansys_inc/v251/ansys/syslib/ansGRPC +!ansys_inc/v251/ansys/syslib/boost/libboost_atomic.so +!ansys_inc/v251/ansys/syslib/boost/libboost_atomic.so.1 +!ansys_inc/v251/ansys/syslib/boost/libboost_atomic.so.1.71 +!ansys_inc/v251/ansys/syslib/boost/libboost_atomic.so.1.71.0 +!ansys_inc/v251/ansys/syslib/boost/libboost_chrono.so +!ansys_inc/v251/ansys/syslib/boost/libboost_chrono.so.1 +!ansys_inc/v251/ansys/syslib/boost/libboost_chrono.so.1.71 +!ansys_inc/v251/ansys/syslib/boost/libboost_chrono.so.1.71.0 +!ansys_inc/v251/ansys/syslib/boost/libboost_date_time.so +!ansys_inc/v251/ansys/syslib/boost/libboost_date_time.so.1 +!ansys_inc/v251/ansys/syslib/boost/libboost_date_time.so.1.71 +!ansys_inc/v251/ansys/syslib/boost/libboost_date_time.so.1.71.0 +!ansys_inc/v251/ansys/syslib/boost/libboost_filesystem.so +!ansys_inc/v251/ansys/syslib/boost/libboost_filesystem.so.1 +!ansys_inc/v251/ansys/syslib/boost/libboost_filesystem.so.1.71 +!ansys_inc/v251/ansys/syslib/boost/libboost_filesystem.so.1.71.0 +!ansys_inc/v251/ansys/syslib/boost/libboost_log.so +!ansys_inc/v251/ansys/syslib/boost/libboost_log.so.1 +!ansys_inc/v251/ansys/syslib/boost/libboost_log.so.1.71 +!ansys_inc/v251/ansys/syslib/boost/libboost_log.so.1.71.0 +!ansys_inc/v251/ansys/syslib/boost/libboost_log_setup.so +!ansys_inc/v251/ansys/syslib/boost/libboost_log_setup.so.1 +!ansys_inc/v251/ansys/syslib/boost/libboost_log_setup.so.1.71 +!ansys_inc/v251/ansys/syslib/boost/libboost_log_setup.so.1.71.0 +!ansys_inc/v251/ansys/syslib/boost/libboost_program_options.so.1.71.0 +!ansys_inc/v251/ansys/syslib/boost/libboost_regex.so +!ansys_inc/v251/ansys/syslib/boost/libboost_regex.so.1 +!ansys_inc/v251/ansys/syslib/boost/libboost_regex.so.1.71 +!ansys_inc/v251/ansys/syslib/boost/libboost_regex.so.1.71.0 +!ansys_inc/v251/ansys/syslib/boost/libboost_system.so +!ansys_inc/v251/ansys/syslib/boost/libboost_system.so.1 +!ansys_inc/v251/ansys/syslib/boost/libboost_system.so.1.71 +!ansys_inc/v251/ansys/syslib/boost/libboost_system.so.1.71.0 +!ansys_inc/v251/ansys/syslib/boost/libboost_thread.so +!ansys_inc/v251/ansys/syslib/boost/libboost_thread.so.1 +!ansys_inc/v251/ansys/syslib/boost/libboost_thread.so.1.71 +!ansys_inc/v251/ansys/syslib/boost/libboost_thread.so.1.71.0 + +!ansys_inc/v251/ansys/syslib/daal/libdaal_core.so +!ansys_inc/v251/ansys/syslib/daal/libdaal_sequential.so +!ansys_inc/v251/ansys/syslib/daal/libdaal_thread.so +!ansys_inc/v251/ansys/syslib/daal/libtbb.so +!ansys_inc/v251/ansys/syslib/daal/libtbb.so.2 +!ansys_inc/v251/ansys/syslib/daal/libtbbmalloc.so +!ansys_inc/v251/ansys/syslib/daal/libtbbmalloc.so.2 + +!ansys_inc/v251/ansys/syslib/openssl/libcrypto.so +!ansys_inc/v251/ansys/syslib/openssl/libcrypto-anss.so +!ansys_inc/v251/ansys/syslib/openssl/libcrypto-anss.so.1.1 +!ansys_inc/v251/ansys/syslib/openssl/libssl.so +!ansys_inc/v251/ansys/syslib/openssl/libssl-anss.so +!ansys_inc/v251/ansys/syslib/openssl/libssl-anss.so.1.1 + +!ansys_inc/v251/ansys/syslib/zlib/libz.so +!ansys_inc/v251/ansys/syslib/zlib/libz.so.1 +!ansys_inc/v251/ansys/syslib/zlib/libz.so.1.2.13 + +ansys_inc/v251/commonfiles/MPI +!ansys_inc/v251/commonfiles/MPI/Intel/2021.11.0/linx64/bin/hydra_bstrap_proxy +!ansys_inc/v251/commonfiles/MPI/Intel/2021.11.0/linx64/bin/hydra_pmi_proxy +!ansys_inc/v251/commonfiles/MPI/Intel/2021.11.0/linx64/bin/mpiexec +!ansys_inc/v251/commonfiles/MPI/Intel/2021.11.0/linx64/bin/mpiexec.hydra +!ansys_inc/v251/commonfiles/MPI/Intel/2021.11.0/linx64/bin/mpirun +!ansys_inc/v251/commonfiles/MPI/Intel/2021.11.0/linx64/lib/release/libmpi.so +!ansys_inc/v251/commonfiles/MPI/Intel/2021.11.0/linx64/lib/release/libmpi.so.12 +!ansys_inc/v251/commonfiles/MPI/Intel/2021.11.0/linx64/lib/release/libmpi.so.12.0 +!ansys_inc/v251/commonfiles/MPI/Intel/2021.11.0/linx64/lib/release/libmpi.so.12.0.0 +!ansys_inc/v251/commonfiles/MPI/Intel/2021.11.0/linx64/libfabric/lib/libfabric.so +!ansys_inc/v251/commonfiles/MPI/Intel/2021.11.0/linx64/libfabric/lib/libfabric.so.1 +!ansys_inc/v251/commonfiles/MPI/Intel/2021.11.0/linx64/libfabric/lib/prov/libefa-fi.so +!ansys_inc/v251/commonfiles/MPI/Intel/2021.11.0/linx64/libfabric/lib/prov/libmlx-fi.so +!ansys_inc/v251/commonfiles/MPI/Intel/2021.11.0/linx64/libfabric/lib/prov/libpsm3-fi.so +!ansys_inc/v251/commonfiles/MPI/Intel/2021.11.0/linx64/libfabric/lib/prov/libpsmx2-fi.so +!ansys_inc/v251/commonfiles/MPI/Intel/2021.11.0/linx64/libfabric/lib/prov/librxm-fi.so +!ansys_inc/v251/commonfiles/MPI/Intel/2021.11.0/linx64/libfabric/lib/prov/libshm-fi.so +!ansys_inc/v251/commonfiles/MPI/Intel/2021.11.0/linx64/libfabric/lib/prov/libtcp-fi.so +!ansys_inc/v251/commonfiles/MPI/Intel/2021.11.0/linx64/libfabric/lib/prov/libverbs-1.1-fi.so +!ansys_inc/v251/commonfiles/MPI/Intel/2021.11.0/linx64/libfabric/lib/prov/libverbs-1.12-fi.so + +ansys_inc/v251/commonfiles/Tcl +!ansys_inc/v251/commonfiles/Tcl/lib/linx64/libjpeg.so +!ansys_inc/v251/commonfiles/Tcl/lib/linx64/libpng.so +!ansys_inc/v251/commonfiles/Tcl/lib/linx64/libtiff.so + +ansys_inc/v251/tp/hdf5/1_12_2 +!ansys_inc/v251/tp/hdf5/1_12_2/linx64/lib/libhdf5_cpp-serial.so.200 +!ansys_inc/v251/tp/hdf5/1_12_2/linx64/lib/libhdf5_hl_cpp-serial.so.200 +!ansys_inc/v251/tp/hdf5/1_12_2/linx64/lib/libhdf5_hl-serial.so.200 +!ansys_inc/v251/tp/hdf5/1_12_2/linx64/lib/libhdf5-serial.so.200 + + +ansys_inc/v251/tp/IntelCompiler/2023.1.0 +!ansys_inc/v251/tp/IntelCompiler/2023.1.0/linx64/lib/intel64/libcilkrts.so +!ansys_inc/v251/tp/IntelCompiler/2023.1.0/linx64/lib/intel64/libcilkrts.so.5 +!ansys_inc/v251/tp/IntelCompiler/2023.1.0/linx64/lib/intel64/libifcore.so +!ansys_inc/v251/tp/IntelCompiler/2023.1.0/linx64/lib/intel64/libifcore.so.5 + +!ansys_inc/v251/tp/IntelCompiler/2023.1.0/linx64/lib/intel64/libifcoremt.so +!ansys_inc/v251/tp/IntelCompiler/2023.1.0/linx64/lib/intel64/libifcoremt.so.5 +!ansys_inc/v251/tp/IntelCompiler/2023.1.0/linx64/lib/intel64/libifport.so +!ansys_inc/v251/tp/IntelCompiler/2023.1.0/linx64/lib/intel64/libifport.so.5 +!ansys_inc/v251/tp/IntelCompiler/2023.1.0/linx64/lib/intel64/libimf.so +!ansys_inc/v251/tp/IntelCompiler/2023.1.0/linx64/lib/intel64/libintlc.so +!ansys_inc/v251/tp/IntelCompiler/2023.1.0/linx64/lib/intel64/libintlc.so.5 +!ansys_inc/v251/tp/IntelCompiler/2023.1.0/linx64/lib/intel64/libiomp5.so +!ansys_inc/v251/tp/IntelCompiler/2023.1.0/linx64/lib/intel64/libirc.so +!ansys_inc/v251/tp/IntelCompiler/2023.1.0/linx64/lib/intel64/libirng.so +!ansys_inc/v251/tp/IntelCompiler/2023.1.0/linx64/lib/intel64/libsvml.so + +ansys_inc/v251/tp/IntelMKL +ansys_inc/v251/tp/IntelMKL/2023.1.0 +!ansys_inc/v251/tp/IntelMKL/2023.1.0/linx64/lib/intel64/libmkl_avx512.so.2 +!ansys_inc/v251/tp/IntelMKL/2023.1.0/linx64/lib/intel64/libmkl_blacs_intelmpi_lp64.so +!ansys_inc/v251/tp/IntelMKL/2023.1.0/linx64/lib/intel64/libmkl_blacs_intelmpi_lp64.so.2 +!ansys_inc/v251/tp/IntelMKL/2023.1.0/linx64/lib/intel64/libmkl_core.so +!ansys_inc/v251/tp/IntelMKL/2023.1.0/linx64/lib/intel64/libmkl_core.so.2 +!ansys_inc/v251/tp/IntelMKL/2023.1.0/linx64/lib/intel64/libmkl_def.so +!ansys_inc/v251/tp/IntelMKL/2023.1.0/linx64/lib/intel64/libmkl_def.so.2 +!ansys_inc/v251/tp/IntelMKL/2023.1.0/linx64/lib/intel64/libmkl_gnu_thread.so +!ansys_inc/v251/tp/IntelMKL/2023.1.0/linx64/lib/intel64/libmkl_gnu_thread.so.2 +!ansys_inc/v251/tp/IntelMKL/2023.1.0/linx64/lib/intel64/libmkl_intel_lp64.so +!ansys_inc/v251/tp/IntelMKL/2023.1.0/linx64/lib/intel64/libmkl_intel_lp64.so.2 +!ansys_inc/v251/tp/IntelMKL/2023.1.0/linx64/lib/intel64/libmkl_intel_thread.so +!ansys_inc/v251/tp/IntelMKL/2023.1.0/linx64/lib/intel64/libmkl_intel_thread.so.2 +!ansys_inc/v251/tp/IntelMKL/2023.1.0/linx64/lib/intel64/libmkl_scalapack_lp64.so +!ansys_inc/v251/tp/IntelMKL/2023.1.0/linx64/lib/intel64/libmkl_scalapack_lp64.so.2 +!ansys_inc/v251/tp/IntelMKL/2023.1.0/linx64/lib/intel64/libmkl_tbb_thread.so +!ansys_inc/v251/tp/IntelMKL/2023.1.0/linx64/lib/intel64/libmkl_tbb_thread.so.2 +!ansys_inc/v251/tp/IntelMKL/2023.1.0/linx64/lib/intel64/libmkl_vml_avx512.so.2 +!ansys_inc/v251/tp/IntelMKL/2023.1.0/linx64/lib/intel64/libmkl_avx2.so.2 +!ansys_inc/v251/tp/IntelMKL/2023.1.0/linx64/lib/intel64/libmkl_vml_avx2.so.2 +!ansys_inc/v251/tp/IntelMKL/2023.1.0/linx64/lib/intel64/libmkl_vml_def.so.2 +!ansys_inc/v251/tp/IntelMKL/2023.1.0/linx64/lib/intel64/libmkl_blacs_openmpi_lp64.so +!ansys_inc/v251/tp/IntelMKL/2023.1.0/linx64/lib/intel64/libmkl_blacs_openmpi_lp64.so.2 + +ansys_inc/v251/tp/qt +!ansys_inc/v251/tp/qt/5.15.17 + +ansys_inc/install.err +.dockerignore +Dockerfile diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 29f60309b..375c49152 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,58 +1,58 @@ -version: '3.3' - -# This is a docker-compose file that launches a remote Mechanical instance in a Dockerized environment. -# You can connect to this instance through port 10000. -# -# For the license, you need to provide the hostname of the license server. -# -# REQUIREMENTS: -# ============= -# -# - docker -# - docker images from ghcr.io (you might need to do 'docker login ghcr.io' first) -# -# USAGE: -# ====== -# -# 1. You need an environment variable named ANSYSLMD_LICENSE_FILE that points to the license server. For example: -# -# export ANSYSLMD_LICENSE_FILE=1055@mylicensehost -# -# 2. Run the following command: -# -# docker-compose up -# -# Optionally you can specify the '-d' flag for detaching mode. (The container will run in the background.) -# -# -# NOTES: -# ====== -# -# - The docker image is based on the Mechanical Ubuntu docker image from Ansys. -# - The entrypoint is defined in the Docker image. -# - If you need to run a license server, look at the 'docker-compose.license_server.yml' file. -# In that case, the command would be: -# -# docker-compose -f docker-compose.yml -f docker-compose.license_server.yml up -# -# -# - If you want to mount the local directory, add the 'volumes' section to the 'mechanical' service. -# -# volumes: -# - ./:/local -# - -services: - mechanical: - restart: always - shm_size: '8gb' - container_name: mechanical - mem_reservation: 8g - environment: - - ANSYSLMD_LICENSE_FILE=${ANSYSLMD_LICENSE_FILE} - ports: - - '10000:10000' - image: 'ghcr.io/ansys/mechanical:24.2.0' - user: "0:0" - # entrypoint: "/bin/bash tini -- xvfb-run ./.workbench -dsapplet -AppModeMech -b -grpc 10000" - +version: '3.3' + +# This is a docker-compose file that launches a remote Mechanical instance in a Dockerized environment. +# You can connect to this instance through port 10000. +# +# For the license, you need to provide the hostname of the license server. +# +# REQUIREMENTS: +# ============= +# +# - docker +# - docker images from ghcr.io (you might need to do 'docker login ghcr.io' first) +# +# USAGE: +# ====== +# +# 1. You need an environment variable named ANSYSLMD_LICENSE_FILE that points to the license server. For example: +# +# export ANSYSLMD_LICENSE_FILE=1055@mylicensehost +# +# 2. Run the following command: +# +# docker-compose up +# +# Optionally you can specify the '-d' flag for detaching mode. (The container will run in the background.) +# +# +# NOTES: +# ====== +# +# - The docker image is based on the Mechanical Ubuntu docker image from Ansys. +# - The entrypoint is defined in the Docker image. +# - If you need to run a license server, look at the 'docker-compose.license_server.yml' file. +# In that case, the command would be: +# +# docker-compose -f docker-compose.yml -f docker-compose.license_server.yml up +# +# +# - If you want to mount the local directory, add the 'volumes' section to the 'mechanical' service. +# +# volumes: +# - ./:/local +# + +services: + mechanical: + restart: always + shm_size: '8gb' + container_name: mechanical + mem_reservation: 8g + environment: + - ANSYSLMD_LICENSE_FILE=${ANSYSLMD_LICENSE_FILE} + ports: + - '10000:10000' + image: 'ghcr.io/ansys/mechanical:25.1.0' + user: "0:0" + # entrypoint: "/bin/bash tini -- xvfb-run ./.workbench -dsapplet -AppModeMech -b -grpc 10000" + diff --git a/docker/make_container.rst b/docker/make_container.rst index 0bb6a2dcc..5b80b04df 100644 --- a/docker/make_container.rst +++ b/docker/make_container.rst @@ -1,137 +1,139 @@ - -Create your own Mechanical Docker container -=========================================== - -.. warning:: You need a valid Ansys license and an Ansys account to - complete the steps in this section. - -You can create your own Mechanical Docker container following -the steps on this page. -These steps use a local Ubuntu machine to generate the needed -files for the Mechanical container by installing Ansys products first -and then copying the generated files to the container. - - -Requirements -============ - -* A Linux machine, preferable with Ubuntu 18.04 or later. - CentOS Linux distribution is no longer supported. - This machine needs to have `Docker `_ installed. - -* A valid Ansys account. Your Ansys reseller should have - provided you with an account. - -* These files are provided: - - * `Dockerfile `_ - * `.dockerignore `_ - - -Procedure -========= - -Download Mechanical installation files --------------------------------------------- - -Download the latest Mechanical version from the Ansys Customer Portal -(`Current Release `_). -You need to have a valid Ansys account with access to -the products to download. - -If you do not Ansys account information, contact your -IT manager. - - -Install Mechanical product --------------------------------- - -To install Mechanical product on an Ubuntu machine you can follow -`Install Mechanical `_ -if you are using the graphical user interface -or `Install Ansys products in WSL `_ -for the command line interface. The later approach can be reused with small changes in a -continuous integration workflow. - -To reduce the size of the final image, you might want to -install the minimal files by using: - -.. code:: bash - - sh /path-to-mechanical-installer \ - -silent -overwrite_preview -mechapdl -lsdyna \ - -install_dir /path-to-install-mechanical/ - - # example - # sh /home/username/download/linx/INSTALL \ - # -silent -overwrite_preview -mechapdl -lsdyna \ - # -install_dir /install/ansys_inc/ - - -Use ``sudo`` if you do not have write permissions in the installation directory. -The ``-mechapdl`` command installs Mechanical. - -Take note of where you are installing Ansys because the -directory path is need in the following section. - -Build Docker image ------------------- - -To build the Docker image, you must create a directory and copy -all the files you need in the image into this directory. - -The steps to copy these files and build the image are provided in the following script, -which you should modify to adapt it to your needs. - -.. code:: bash - - # Create env vars for the Dockerfile - export ANS_MAJOR_VERSION=23 - export ANS_MINOR_VERSION=1 - export ANS_VERSION=${ANS_MAJOR_VERSION}${ANS_MINOR_VERSION} - - export TAG=mechanical:${ANS_MAJOR_VERSION}.${ANS_MINOR_VERSION} - # example: if Mechanical v242 is installed under /install/ansys_inc/v242 - - # use /install for path_to_mechanical_installation - export MECHANICAL_INSTALL_LOCATION=/path_to_mechanical_installation/ - - # example: if pymechanical is cloned under /some_location/pymechanical - # use /some_location for path-to-pymechanical - export PYMECHANICAL_LOCATION=/path-to-pymechanical - - # Create working directory - cd ${MECHANICAL_INSTALL_LOCATION} - - # Copy the Docker files - cp ${PYMECHANICAL_LOCATION}/pymechanical/docker/${ANS_VERSION}/Dockerfile . - cp ${PYMECHANICAL_LOCATION}/pymechanical/docker/${ANS_VERSION}/.dockerignore . - - # Build Docker image - sudo docker build -t $TAG --build-arg VERSION=$ANS_VERSION . - -Take note of the these paths: - -* ``path-to-pymechanical`` is the path where PyMechanical repository is located. -* ``path_to_mechanical_installation`` is the path to where you have locally installed Mechanical. - -Not all installation files are copied. In fact, the files ignored during the copying -are described in the `.dockerignore file `_. - -The Docker container configuration needed to build the container is described in the -`Dockerfile `_. - - -Summary -======= - - -* **Step 1:** Download the latest Mechanical version from the Ansys Customer Portal - (`Current Release `_). - -* **Step 2:** Install Mechanical in a known folder. You can reuse your local - installation if it is updated and the machine is running the same Ubuntu - version as the target Ubuntu Docker version. - -* **Step 3:** Build the Docker image with the provided Docker configuration files - and script. + +Create your own Mechanical Docker container +=========================================== + +.. warning:: You need a valid Ansys license and an Ansys account to + complete the steps in this section. + +You can create your own Mechanical Docker container following +the steps on this page. +These steps use a local Ubuntu machine to generate the needed +files for the Mechanical container by installing Ansys products first +and then copying the generated files to the container. + + +Requirements +============ + +* A Linux machine, preferable with Ubuntu 20.04 or later. + CentOS Linux distribution is no longer supported. + This machine needs to have `Docker `_ installed. + +* A valid Ansys account. Your Ansys reseller should have + provided you with an account. + +* These files are provided: + + * `Dockerfile `_ + + * `.dockerignore `_ + + +Procedure +========= + +Download Mechanical installation files +-------------------------------------------- + +Download the latest Mechanical version from the Ansys Customer Portal +(`Current Release `_). +You need to have a valid Ansys account with access to +the products to download. + +If you do not Ansys account information, contact your +IT manager. + + +Install Mechanical product +-------------------------------- + +To install Mechanical product on an Ubuntu machine you can follow +`Install Mechanical `_ +if you are using the graphical user interface +or `Install Ansys products in WSL `_ +for the command line interface. The later approach can be reused with small changes in a +continuous integration workflow. + +To reduce the size of the final image, you might want to +install the minimal files by using: + +.. code:: bash + + sh /path-to-mechanical-installer \ + -silent -overwrite_preview -mechapdl -lsdyna \ + -install_dir /path-to-install-mechanical/ + + # example + # sh /home/username/download/linx/INSTALL \ + # -silent -overwrite_preview -mechapdl -lsdyna \ + # -install_dir /install/ansys_inc/ + + +Use ``sudo`` if you do not have write permissions in the installation directory. +The ``-mechapdl`` command installs Mechanical. + +Take note of where you are installing Ansys because the +directory path is need in the following section. + +Build Docker image +------------------ + +To build the Docker image, you must create a directory and copy +all the files you need in the image into this directory. + +The steps to copy these files and build the image are provided in the following script, +which you should modify to adapt it to your needs. + +.. code:: bash + + # Create env vars for the Dockerfile + export ANS_MAJOR_VERSION=25 + + export ANS_MINOR_VERSION=1 + export ANS_VERSION=${ANS_MAJOR_VERSION}${ANS_MINOR_VERSION} + + export TAG=mechanical:${ANS_MAJOR_VERSION}.${ANS_MINOR_VERSION} + # example: if Mechanical v251 is installed under /install/ansys_inc/v251 + + # use /install for path_to_mechanical_installation + export MECHANICAL_INSTALL_LOCATION=/path_to_mechanical_installation/ + + # example: if pymechanical is cloned under /some_location/pymechanical + # use /some_location for path-to-pymechanical + export PYMECHANICAL_LOCATION=/path-to-pymechanical + + # Create working directory + cd ${MECHANICAL_INSTALL_LOCATION} + + # Copy the Docker files + cp ${PYMECHANICAL_LOCATION}/pymechanical/docker/${ANS_VERSION}/Dockerfile . + cp ${PYMECHANICAL_LOCATION}/pymechanical/docker/${ANS_VERSION}/.dockerignore . + + # Build Docker image + sudo docker build -t $TAG --build-arg VERSION=$ANS_VERSION . + +Take note of the these paths: + +* ``path-to-pymechanical`` is the path where PyMechanical repository is located. +* ``path_to_mechanical_installation`` is the path to where you have locally installed Mechanical. + +Not all installation files are copied. In fact, the files ignored during the copying +are described in the `.dockerignore file `_. + +The Docker container configuration needed to build the container is described in the +`Dockerfile `_. + + +Summary +======= + + +* **Step 1:** Download the latest Mechanical version from the Ansys Customer Portal + (`Current Release `_). + +* **Step 2:** Install Mechanical in a known folder. You can reuse your local + installation if it is updated and the machine is running the same Ubuntu + version as the target Ubuntu Docker version. + +* **Step 3:** Build the Docker image with the provided Docker configuration files + and script. From 6200fcb20b7aa4b949ef5d5f35774480b95853dd Mon Sep 17 00:00:00 2001 From: Dipin <26918585+dipinknair@users.noreply.github.com> Date: Wed, 15 Jan 2025 21:21:39 -0600 Subject: [PATCH 42/44] CHORE: Update default product version to 25R1 (#1045) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Co-authored-by: klmcadams <58492561+klmcadams@users.noreply.github.com> --- README.rst | 305 +- doc/changelog.d/1045.maintenance.md | 1 + doc/source/cheatsheet/cheat_sheet.qmd | 428 +- doc/source/faq.rst | 10 +- doc/source/getting_started/docker.rst | 4 +- doc/source/getting_started/installation.rst | 20 +- .../getting_started/running_mechanical.rst | 566 +- doc/source/getting_started/wsl.rst | 2 +- doc/source/index.rst | 2 +- doc/source/links.rst | 53 +- doc/source/user_guide_embedding/index.rst | 240 +- doc/source/user_guide_embedding/libraries.rst | 70 +- doc/source/user_guide_session/index.rst | 170 +- doc/source/user_guide_session/pool.rst | 166 +- .../user_guide_session/server-launcher.rst | 12 +- docker/make_container.rst | 277 +- src/ansys/mechanical/core/_version.py | 95 +- src/ansys/mechanical/core/embedding/app.py | 1221 +++-- .../mechanical/core/embedding/initializer.py | 7 +- .../core/embedding/logger/__init__.py | 438 +- .../mechanical/core/embedding/resolver.py | 82 +- src/ansys/mechanical/core/ide_config.py | 424 +- src/ansys/mechanical/core/mechanical.py | 4648 ++++++++--------- src/ansys/mechanical/core/misc.py | 352 +- src/ansys/mechanical/core/pool.py | 1424 ++--- src/ansys/mechanical/core/run.py | 642 +-- tests/conftest.py | 4 +- tests/test_cli.py | 861 +-- tests/test_mechanical.py | 1116 ++-- 29 files changed, 6833 insertions(+), 6807 deletions(-) create mode 100644 doc/changelog.d/1045.maintenance.md diff --git a/README.rst b/README.rst index d2c43abe8..e30aebcab 100644 --- a/README.rst +++ b/README.rst @@ -1,153 +1,154 @@ -.. image:: https://raw.githubusercontent.com/ansys/pymechanical/main/doc/source/_static/logo/pymechanical-logo.png - :alt: PyMechanical logo - :width: 580px - - -|pyansys| |pypi| |python| |GH-CI| |codecov| |MIT| |black| - -.. |pyansys| image:: https://img.shields.io/badge/Py-Ansys-ffc107.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABDklEQVQ4jWNgoDfg5mD8vE7q/3bpVyskbW0sMRUwofHD7Dh5OBkZGBgW7/3W2tZpa2tLQEOyOzeEsfumlK2tbVpaGj4N6jIs1lpsDAwMJ278sveMY2BgCA0NFRISwqkhyQ1q/Nyd3zg4OBgYGNjZ2ePi4rB5loGBhZnhxTLJ/9ulv26Q4uVk1NXV/f///////69du4Zdg78lx//t0v+3S88rFISInD59GqIH2esIJ8G9O2/XVwhjzpw5EAam1xkkBJn/bJX+v1365hxxuCAfH9+3b9/+////48cPuNehNsS7cDEzMTAwMMzb+Q2u4dOnT2vWrMHu9ZtzxP9vl/69RVpCkBlZ3N7enoDXBwEAAA+YYitOilMVAAAAAElFTkSuQmCC - :target: https://docs.pyansys.com/ - :alt: PyAnsys - -.. |pypi| image:: https://img.shields.io/pypi/v/ansys-mechanical-core.svg?logo=python&logoColor=white - :target: https://pypi.org/project/ansys-mechanical-core - :alt: PyPI - -.. |python| image:: https://img.shields.io/pypi/pyversions/ansys-mechanical-core?logo=pypi - :target: https://pypi.org/project/ansys-mechanical-core - :alt: Python - -.. |codecov| image:: https://codecov.io/gh/ansys/pymechanical/branch/main/graph/badge.svg - :target: https://app.codecov.io/gh/ansys/pymechanical - :alt: Codecov - -.. |GH-CI| image:: https://github.com/ansys/pymechanical/actions/workflows/ci_cd.yml/badge.svg - :target: https://github.com/ansys/pymechanical/actions/workflows/ci_cd.yml - :alt: GH-CI - -.. |MIT| image:: https://img.shields.io/badge/License-MIT-yellow.svg - :target: https://opensource.org/licenses/MIT - :alt: MIT - -.. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg?style=flat - :target: https://github.com/psf/black - :alt: Black - -.. |pre-commit| image:: https://results.pre-commit.ci/badge/github/ansys/pymechanical/main.svg?style=flat - :target: https://results.pre-commit.ci/latest/github/ansys/pymechanical/main - :alt: pre-commit - -Overview --------- - -PyMechanical brings Ansys Mechanical to Python. It enables your Python programs to use -Mechanical within Python's ecosystem. It includes the ability to: - -- Connect to a remote Mechanical session -- Embed an instance of Mechanical directly as a Python object - - -Install the package -------------------- - -Install PyMechanical using ``pip`` with:: - - pip install ansys-mechanical-core - -For more information, see `Install the package `_ -in the PyMechanical documentation. - - -Dependencies ------------- - -You must have a licensed copy of `Ansys Mechanical `_ -installed. When using an embedded instance, that installation must be runnable from the -same computer as your Python program. When using a remote session, a connection to that -session must be reachable from your Python program. - -Getting started ---------------- - -PyMechanical uses the built-in scripting capabilities of Mechanical. For information on the -scripting APIs available, see the `Scripting in Mechanical Guide -`_ in the -Ansys Help. - -Configuring the mechanical installation -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -On a Windows system, the environment variable ``AWP_ROOT`` is configured when Mechanical is -installed, where ```` is the Mechanical release number, such as ``242`` for release 2024 R2. -PyMechanical automatically uses this environment variable (or variables if there are multiple -installations of different versions) to locate the latest Mechanical installation. On a Linux -system, you must configure the ``AWP_ROOT`` environment variable to point to the -absolute path of a Mechanical installation. - -Starting a remote session -^^^^^^^^^^^^^^^^^^^^^^^^^ - -To start a remote session of Mechanical on your computer from Python, use the ``launch_mechanical()`` -method. This methods returns an object representing the connection to the session: - -.. code:: python - - import ansys.mechanical.core as pymechanical - - mechanical = pymechanical.launch_mechanical() - -Running commands on the remote session -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Given a connection to a remote session, you can send an IronPython script. This uses the built-in -scripting capabilities of Mechanical. Here is an example: - -.. code:: python - - result = mechanical.run_python_script("2+3") - result = mechanical.run_python_script("ExtAPI.DataModel.Project.ProjectDirectory") - - -Using an embedded instance of Mechanical as a Python object -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -PyMechanical also supports directly embedding an instance of Mechanical as a Python object. -In this mode, there is no externally running instance of Mechanical. This feature is supported -on Windows and Linux for version 2023 R2 and later. Here is an example: - -.. code:: python - - import ansys.mechanical.core as pymechanical - - app = pymechanical.App() - app.update_globals(globals()) - project_dir = DataModel.Project.ProjectDirectory - -Documentation and issues ------------------------- - -Documentation for the latest stable release of PyMechanical is hosted at `PyMechanical 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. - -You can also `view `_ or -`download `_ the -PyMechanical cheat sheet. This one-page reference provides syntax rules and commands -for using PyMechanical. - -On the `PyMechanical Issues `_ page, -you can create issues to report bugs and request new features. On the `PyMechanical Discussions -`_ page or the `Discussions `_ -page on the Ansys Developer portal, you can post questions, share ideas, and get community feedback. - -To reach the project support team, email `pyansys.core@ansys.com `_. - -Testing and development ------------------------ - -If you would like to test or contribute to the development of PyMechanical, see -`Contribute `_ in +.. image:: https://raw.githubusercontent.com/ansys/pymechanical/main/doc/source/_static/logo/pymechanical-logo.png + :alt: PyMechanical logo + :width: 580px + + +|pyansys| |pypi| |python| |GH-CI| |codecov| |MIT| |black| + +.. |pyansys| image:: https://img.shields.io/badge/Py-Ansys-ffc107.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABDklEQVQ4jWNgoDfg5mD8vE7q/3bpVyskbW0sMRUwofHD7Dh5OBkZGBgW7/3W2tZpa2tLQEOyOzeEsfumlK2tbVpaGj4N6jIs1lpsDAwMJ278sveMY2BgCA0NFRISwqkhyQ1q/Nyd3zg4OBgYGNjZ2ePi4rB5loGBhZnhxTLJ/9ulv26Q4uVk1NXV/f///////69du4Zdg78lx//t0v+3S88rFISInD59GqIH2esIJ8G9O2/XVwhjzpw5EAam1xkkBJn/bJX+v1365hxxuCAfH9+3b9/+////48cPuNehNsS7cDEzMTAwMMzb+Q2u4dOnT2vWrMHu9ZtzxP9vl/69RVpCkBlZ3N7enoDXBwEAAA+YYitOilMVAAAAAElFTkSuQmCC + :target: https://docs.pyansys.com/ + :alt: PyAnsys + +.. |pypi| image:: https://img.shields.io/pypi/v/ansys-mechanical-core.svg?logo=python&logoColor=white + :target: https://pypi.org/project/ansys-mechanical-core + :alt: PyPI + +.. |python| image:: https://img.shields.io/pypi/pyversions/ansys-mechanical-core?logo=pypi + :target: https://pypi.org/project/ansys-mechanical-core + :alt: Python + +.. |codecov| image:: https://codecov.io/gh/ansys/pymechanical/branch/main/graph/badge.svg + :target: https://app.codecov.io/gh/ansys/pymechanical + :alt: Codecov + +.. |GH-CI| image:: https://github.com/ansys/pymechanical/actions/workflows/ci_cd.yml/badge.svg + :target: https://github.com/ansys/pymechanical/actions/workflows/ci_cd.yml + :alt: GH-CI + +.. |MIT| image:: https://img.shields.io/badge/License-MIT-yellow.svg + :target: https://opensource.org/licenses/MIT + :alt: MIT + +.. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg?style=flat + :target: https://github.com/psf/black + :alt: Black + +.. |pre-commit| image:: https://results.pre-commit.ci/badge/github/ansys/pymechanical/main.svg?style=flat + :target: https://results.pre-commit.ci/latest/github/ansys/pymechanical/main + :alt: pre-commit + +Overview +-------- + +PyMechanical brings Ansys Mechanical to Python. It enables your Python programs to use +Mechanical within Python's ecosystem. It includes the ability to: + +- Connect to a remote Mechanical session +- Embed an instance of Mechanical directly as a Python object + + +Install the package +------------------- + +Install PyMechanical using ``pip`` with:: + + pip install ansys-mechanical-core + +For more information, see `Install the package `_ +in the PyMechanical documentation. + + +Dependencies +------------ + +You must have a licensed copy of `Ansys Mechanical `_ +installed. When using an embedded instance, that installation must be runnable from the +same computer as your Python program. When using a remote session, a connection to that +session must be reachable from your Python program. + +Getting started +--------------- + +.. _scripting_guide: https://ansyshelp.ansys.com/Views/Secured/corp/v251/en/act_script/act_script.html + +PyMechanical uses the built-in scripting capabilities of Mechanical. For information on the +scripting APIs available, see the `Scripting in Mechanical Guide <_scripting_guide>`_ in the +Ansys Help. + +Configuring the mechanical installation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +On a Windows system, the environment variable ``AWP_ROOT`` is configured when Mechanical is +installed, where ```` is the Mechanical release number, such as ``251`` for release 2025 R1. +PyMechanical automatically uses this environment variable (or variables if there are multiple +installations of different versions) to locate the latest Mechanical installation. On a Linux +system, you must configure the ``AWP_ROOT`` environment variable to point to the +absolute path of a Mechanical installation. + +Starting a remote session +^^^^^^^^^^^^^^^^^^^^^^^^^ + +To start a remote session of Mechanical on your computer from Python, use the ``launch_mechanical()`` +method. This methods returns an object representing the connection to the session: + +.. code:: python + + import ansys.mechanical.core as pymechanical + + mechanical = pymechanical.launch_mechanical() + +Running commands on the remote session +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Given a connection to a remote session, you can send an IronPython script. This uses the built-in +scripting capabilities of Mechanical. Here is an example: + +.. code:: python + + result = mechanical.run_python_script("2+3") + result = mechanical.run_python_script("ExtAPI.DataModel.Project.ProjectDirectory") + + +Using an embedded instance of Mechanical as a Python object +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +PyMechanical also supports directly embedding an instance of Mechanical as a Python object. +In this mode, there is no externally running instance of Mechanical. This feature is supported +on Windows and Linux for version 2023 R2 and later. Here is an example: + +.. code:: python + + import ansys.mechanical.core as pymechanical + + app = pymechanical.App() + app.update_globals(globals()) + project_dir = DataModel.Project.ProjectDirectory + +Documentation and issues +------------------------ + +Documentation for the latest stable release of PyMechanical is hosted at `PyMechanical 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. + +You can also `view `_ or +`download `_ the +PyMechanical cheat sheet. This one-page reference provides syntax rules and commands +for using PyMechanical. + +On the `PyMechanical Issues `_ page, +you can create issues to report bugs and request new features. On the `PyMechanical Discussions +`_ page or the `Discussions `_ +page on the Ansys Developer portal, you can post questions, share ideas, and get community feedback. + +To reach the project support team, email `pyansys.core@ansys.com `_. + +Testing and development +----------------------- + +If you would like to test or contribute to the development of PyMechanical, see +`Contribute `_ in the PyMechanical documentation. \ No newline at end of file diff --git a/doc/changelog.d/1045.maintenance.md b/doc/changelog.d/1045.maintenance.md new file mode 100644 index 000000000..10865a473 --- /dev/null +++ b/doc/changelog.d/1045.maintenance.md @@ -0,0 +1 @@ +Update default product version to 25R1 \ No newline at end of file diff --git a/doc/source/cheatsheet/cheat_sheet.qmd b/doc/source/cheatsheet/cheat_sheet.qmd index a500813c2..e00ded098 100644 --- a/doc/source/cheatsheet/cheat_sheet.qmd +++ b/doc/source/cheatsheet/cheat_sheet.qmd @@ -1,215 +1,215 @@ ---- -title: PyMechanical cheat sheet -format: cheat_sheet-pdf -params: - version: main -footer: PyMechanical -footerlinks: - - urls: 'https://mechanical.docs.pyansys.com/version/stable/' - text: Documentation - - urls: 'https://mechanical.docs.pyansys.com/version/stable/getting_started/index.html' - text: Getting started - - urls: 'https://mechanical.docs.pyansys.com/version/stable/examples/index.html' - text: Examples - - urls: 'https://mechanical.docs.pyansys.com/version/stable/user_guide_scripting/index.html' - text: Mechanical scripting - - urls: 'https://mechanical.docs.pyansys.com/version/stable/api/index.html' - text: API reference - - urls: 'https://mechanical.docs.pyansys.com/version/stable/kil/index.html' - text: Known issues and limitations -execute: - # output: false - eval: false - -latex-clean: true -jupyter: - jupytext: - text_representation: - extension: .qmd - format_name: quarto - format_version: '1.0' - jupytext_version: 1.16.1 - kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -# Connect to Mechanical Remote session - -## Launch and connect to session -```{python} -#| eval: false -# Launch an instance -from ansys.mechanical.core import launch_mechanical -mechanical = launch_mechanical() -``` -## Launch Mechanical UI -```{python} -#| eval: false -mechanical = launch_mechanical(batch=False) -``` -## Launch by version -Verify the license and version of Mechanical that is used: -```{python} -#| eval: false -print(mechanical) -``` -Launch a specific version of Mechanical: -```{python} -#| eval: false -from ansys.mechanical.core import find_mechanical -wb_exe = find_mechanical(242)[0] -# 'Ansys Inc\\....\\winx64\\AnsysWBU.exe' -mechanical = launch_mechanical( - exec_file=wb_exe, verbose_mechanical=True, - batch=True - ) -print(mechanical) -``` -## Launch Mechanical using the CLI -```{python} -#| eval: false -ansys-mechanical -r 242 --port 10000 -g -``` -## Manually connect to the Mechanical session -```{python} -#| eval: false -from ansys.mechanical.core import connect_to_mechanical -# Connect locally -mechanical = connect_to_mechanical(port=10000) - # Or -# Connect remotely, to the IP address or hostname -mechanical = connect_to_mechanical( - "192.168.0.1", port=10000 - ) -``` -## Send commands to Mechanical -Run a single command: -```{python} -#| eval: false -result1 = mechanical.run_python_script("2+3") -result2 = mechanical.run_python_script( - "DataModel.Project.ProjectDirectory" -) -mechanical.run_python_script( - "Model.AddStaticStructuralAnalysis()" -) -``` -Evaluate a block of commands: -```{python} -#| eval: false -# Import a material -commands = """ -cu_mat_file_path = ( - r'D:\Workdir\copper.xml'.replace("\\", "\\\\") -) -materials = Model.Materials -materials.Import(cu_mat__file_path) -""" -mechanical.run_python_script(commands) -``` -## Execute a Python script: -```{python} -#| eval: false -mechanical.run_python_script_from_file(file_path) -``` -## Import a Mechanical file and print the count of bodies: -```{python} -#| eval: false -file = r"D:\\Workdir\\bracket.mechdb" -command = f'DataModel.Project.Open("{file}")' -mechanical.run_python_script(command) -mechanical.run_python_script(""" -allbodies = Model.GetChildren( - DataModelObjectCategory.Body, True) -""") -mechanical.run_python_script("allbodies.Count") -``` -## Perform project-specific operations: -```{python} -#| eval: false - -# Get the project directory -mechanical.project_directory -# List the files in the working directory. -mechanical.list_files() -# Save -mechanical.run_python_script( -"ExtAPI.DataModel.Project.Save(r'D:\\Workdir')") -# Log in two ways: -mechanical._log.info("This is a useful message.") -mechanical.log_message("INFO", "info message") -# Exit -mechanical.exit(force=True) -``` - -# Load a Mechanical embedded instance - - -## Start an instance of App -```{python} -#| eval: false -from ansys.mechanical.core import App -app = App(version=242) -print(app) -``` -Extract and merge global API entry points: -```{python} -#| eval: false - -# Extract global API entry points from Mechanical -# Merge them into your Python global variables -app.update_globals(globals()) -``` -Access entry points from Python: -```{python} -#| eval: false - -ExtAPI # Application.ExtAPI -DataModel # Application.DataModel -Model # Application.DataModel.Project.Model -Tree # Application.DataModel.Tree -Graphics # Application.ExtAPI.Graphics -``` -Import a file and print the count of bodies -```{python} -#| eval: false - -file = r"D:\\Workdir\\bracket.mechdb" -app.open(file) -app.update_globals(globals()) -allbodies = Model.GetChildren( - DataModelObjectCategory.Body, True) -print(allbodies.Count) -``` -## Turn on warning logging: -```{python} -#| eval: false - -import logging -from ansys.mechanical.core import App -from ansys.mechanical.core.embedding.logger -import Configuration,Logger -Configuration.configure(level=logging.WARNING, -to_stdout=True) -app = App(version=242) -Logger.error("Test Error Message") -``` -## Visualize geometry in 3D: -```{python} -#| eval: false -# requires Mechanical version >= 24R2 -app.plot() -``` -## Print project structure as tree: -```{python} -#| eval: false -app.print_tree() -# print only 20 lines -app.print_tree(max_lines=20) -``` - -```{python} -#| eval: false +--- +title: PyMechanical cheat sheet +format: cheat_sheet-pdf +params: + version: main +footer: PyMechanical +footerlinks: + - urls: 'https://mechanical.docs.pyansys.com/version/stable/' + text: Documentation + - urls: 'https://mechanical.docs.pyansys.com/version/stable/getting_started/index.html' + text: Getting started + - urls: 'https://mechanical.docs.pyansys.com/version/stable/examples/index.html' + text: Examples + - urls: 'https://mechanical.docs.pyansys.com/version/stable/user_guide_scripting/index.html' + text: Mechanical scripting + - urls: 'https://mechanical.docs.pyansys.com/version/stable/api/index.html' + text: API reference + - urls: 'https://mechanical.docs.pyansys.com/version/stable/kil/index.html' + text: Known issues and limitations +execute: + # output: false + eval: false + +latex-clean: true +jupyter: + jupytext: + text_representation: + extension: .qmd + format_name: quarto + format_version: '1.0' + jupytext_version: 1.16.1 + kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + +# Connect to Mechanical Remote session + +## Launch and connect to session +```{python} +#| eval: false +# Launch an instance +from ansys.mechanical.core import launch_mechanical +mechanical = launch_mechanical() +``` +## Launch Mechanical UI +```{python} +#| eval: false +mechanical = launch_mechanical(batch=False) +``` +## Launch by version +Verify the license and version of Mechanical that is used: +```{python} +#| eval: false +print(mechanical) +``` +Launch a specific version of Mechanical: +```{python} +#| eval: false +from ansys.mechanical.core import find_mechanical +wb_exe = find_mechanical(251)[0] +# 'Ansys Inc\\....\\winx64\\AnsysWBU.exe' +mechanical = launch_mechanical( + exec_file=wb_exe, verbose_mechanical=True, + batch=True + ) +print(mechanical) +``` +## Launch Mechanical using the CLI +```{python} +#| eval: false +ansys-mechanical -r 251 --port 10000 -g +``` +## Manually connect to the Mechanical session +```{python} +#| eval: false +from ansys.mechanical.core import connect_to_mechanical +# Connect locally +mechanical = connect_to_mechanical(port=10000) + # Or +# Connect remotely, to the IP address or hostname +mechanical = connect_to_mechanical( + "192.168.0.1", port=10000 + ) +``` +## Send commands to Mechanical +Run a single command: +```{python} +#| eval: false +result1 = mechanical.run_python_script("2+3") +result2 = mechanical.run_python_script( + "DataModel.Project.ProjectDirectory" +) +mechanical.run_python_script( + "Model.AddStaticStructuralAnalysis()" +) +``` +Evaluate a block of commands: +```{python} +#| eval: false +# Import a material +commands = """ +cu_mat_file_path = ( + r'D:\Workdir\copper.xml'.replace("\\", "\\\\") +) +materials = Model.Materials +materials.Import(cu_mat__file_path) +""" +mechanical.run_python_script(commands) +``` +## Execute a Python script: +```{python} +#| eval: false +mechanical.run_python_script_from_file(file_path) +``` +## Import a Mechanical file and print the count of bodies: +```{python} +#| eval: false +file = r"D:\\Workdir\\bracket.mechdb" +command = f'DataModel.Project.Open("{file}")' +mechanical.run_python_script(command) +mechanical.run_python_script(""" +allbodies = Model.GetChildren( + DataModelObjectCategory.Body, True) +""") +mechanical.run_python_script("allbodies.Count") +``` +## Perform project-specific operations: +```{python} +#| eval: false + +# Get the project directory +mechanical.project_directory +# List the files in the working directory. +mechanical.list_files() +# Save +mechanical.run_python_script( +"ExtAPI.DataModel.Project.Save(r'D:\\Workdir')") +# Log in two ways: +mechanical._log.info("This is a useful message.") +mechanical.log_message("INFO", "info message") +# Exit +mechanical.exit(force=True) +``` + +# Load a Mechanical embedded instance + + +## Start an instance of App +```{python} +#| eval: false +from ansys.mechanical.core import App +app = App(version=251) +print(app) +``` +Extract and merge global API entry points: +```{python} +#| eval: false + +# Extract global API entry points from Mechanical +# Merge them into your Python global variables +app.update_globals(globals()) +``` +Access entry points from Python: +```{python} +#| eval: false + +ExtAPI # Application.ExtAPI +DataModel # Application.DataModel +Model # Application.DataModel.Project.Model +Tree # Application.DataModel.Tree +Graphics # Application.ExtAPI.Graphics +``` +Import a file and print the count of bodies +```{python} +#| eval: false + +file = r"D:\\Workdir\\bracket.mechdb" +app.open(file) +app.update_globals(globals()) +allbodies = Model.GetChildren( + DataModelObjectCategory.Body, True) +print(allbodies.Count) +``` +## Turn on warning logging: +```{python} +#| eval: false + +import logging +from ansys.mechanical.core import App +from ansys.mechanical.core.embedding.logger +import Configuration,Logger +Configuration.configure(level=logging.WARNING, +to_stdout=True) +app = App(version=251) +Logger.error("Test Error Message") +``` +## Visualize geometry in 3D: +```{python} +#| eval: false +# requires Mechanical version >= 24R2 +app.plot() +``` +## Print project structure as tree: +```{python} +#| eval: false +app.print_tree() +# print only 20 lines +app.print_tree(max_lines=20) +``` + +```{python} +#| eval: false ``` \ No newline at end of file diff --git a/doc/source/faq.rst b/doc/source/faq.rst index 8c5313012..5b4587ca8 100644 --- a/doc/source/faq.rst +++ b/doc/source/faq.rst @@ -131,16 +131,16 @@ This section provides answers to frequently asked questions. .. code-block:: shell Ansys Mechanical [Ansys Mechanical Enterprise] - Product Version:242 - Software build date: 06/03/2024 09:35:09 + Product Version:251 + Software build date: 11/27/2024 09:34:44 .. tab-item:: Without License .. code-block:: shell Ansys Mechanical [] - Product Version:242 - Software build date: 06/03/2024 09:35:09 + Product Version:251 + Software build date: 11/27/2024 09:34:44 Alternatively, once the ``app`` is created ``readonly`` method can be used to see if license is active. If license is not checked out then it is in read only mode. @@ -156,7 +156,7 @@ This section provides answers to frequently asked questions. $ mechanical-env python >>> import ansys.mechanical.core as mech - >>> app=mech.App(version=42) + >>> app=mech.App(version=251) or diff --git a/doc/source/getting_started/docker.rst b/doc/source/getting_started/docker.rst index 659106915..2c91ea905 100644 --- a/doc/source/getting_started/docker.rst +++ b/doc/source/getting_started/docker.rst @@ -93,8 +93,8 @@ Verify your connection with this code: >>> mechanical Ansys Mechanical [Ansys Mechanical Enterprise] - Product Version:242 - Software build date: 06/03/2024 14:47:58 + Product Version:251 + Software build date: 11/27/2024 09:34:44 Additional considerations ------------------------- diff --git a/doc/source/getting_started/installation.rst b/doc/source/getting_started/installation.rst index c437d73aa..9b17131b7 100644 --- a/doc/source/getting_started/installation.rst +++ b/doc/source/getting_started/installation.rst @@ -67,10 +67,10 @@ This package is required to use PyMechanical. or - >>> find_mechanical(version=242) # for specific version + >>> find_mechanical(version=251) # for specific version - ('C:/Program Files/ANSYS Inc/v242/aisol/bin/winx64/AnsysWBU.exe', 24.2) # windows - ('/usr/ansys_inc/v242/aisol/.workbench', 24.2) # Linux + ('C:/Program Files/ANSYS Inc/v251/aisol/bin/winx64/AnsysWBU.exe', 25.1) # windows + ('/usr/ansys_inc/v251/aisol/.workbench', 25.1) # Linux If you install Ansys in a directory other than the default or typical location, you can save this directory path using the @@ -82,15 +82,15 @@ and ``version_from_path`` functions to verify the path and version. .. code:: pycon >>> from ansys.tools.path import save_mechanical_path, find_mechanical - >>> save_mechanical_path("home/username/ansys_inc/v242/aisol/.workbench") + >>> save_mechanical_path("home/username/ansys_inc/v251/aisol/.workbench") >>> path = get_mechanical_path() >>> print(path) - /home/username/ansys_inc/v242/aisol/.workbench + /home/username/ansys_inc/v251/aisol/.workbench >>> version = version_from_path("mechanical", path) - 242 + 251 Verify a remote session ^^^^^^^^^^^^^^^^^^^^^^^ @@ -104,8 +104,8 @@ Verify your installation by starting a remote session of Mechanical from Python: >>> mechanical Ansys Mechanical [Ansys Mechanical Enterprise] - Product Version:242 - Software build date: 06/03/2024 14:47:58 + Product Version:251 + Software build date: 11/27/2024 09:34:44 If you see a response from the server, you can begin using Mechanical as a service. @@ -135,8 +135,8 @@ Inside of Python, use the following commands to load an embedded instance: >>> app = App() >>> print(app) Ansys Mechanical [Ansys Mechanical Enterprise] - Product Version:242 - Software build date: 06/03/2024 14:47:58 + Product Version:251 + Software build date: 11/27/2024 09:34:44 .. LINKS AND REFERENCES .. _ansys_tools_path_api: https://github.com/psf/black diff --git a/doc/source/getting_started/running_mechanical.rst b/doc/source/getting_started/running_mechanical.rst index ac8c2773e..9abf6ca47 100644 --- a/doc/source/getting_started/running_mechanical.rst +++ b/doc/source/getting_started/running_mechanical.rst @@ -1,283 +1,283 @@ -.. _using_standard_install: - -Launching PyMechanical -====================== - -The ``ansys-mechanical-core`` package requires either a local or -remote instance of Mechanical to communicate with. This page describes -how Mechanical is installed from the Ansys standard installer and -describes how you launch and interface with Mechanical from Python. - -Install Mechanical ------------------- - -Mechanical is installed by default from the Ansys standard installer. -When you run the standard installer, look under the **Structural Mechanics** -heading to verify that the **Mechanical Products** checkbox is selected. -Although options in the standard installer might change, this image provides -a reference: - -.. figure:: ../images/unified_install_2023R1.jpg - :width: 400pt - -Launch a remote Mechanical session ----------------------------------- - -You can use PyMechanical to launch a Mechanical session on the local machine -Python is running on. Alternatively, you can run Mechanical's command line -directly on any machine to start Mechanical in server mode and then use its -IP address to manually connect to it from Python. - -Launch Mechanical on the local machine using Python -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When Mechanical is installed locally on your machine, you can use the -`launch_mechanical() <../api/ansys/mechanical/core/mechanical/index.html#mechanical.launch_mechanical>`_ -method to launch and automatically connect to Mechanical. While this method provides the -easiest and fastest way to launch Mechanical, it only works with a local Mechanical installation. - -Launch Mechanical locally with this code: - -.. code:: pycon - - >>> from ansys.mechanical.core import launch_mechanical - >>> mechanical = launch_mechanical() - >>> mechanical - - Ansys Mechanical [Ansys Mechanical Enterprise] - Product Version:242 - Software build date: 06/03/2024 14:47:58 - -Launch Mechanical from the command line -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The `ansys-mechanical` utility is installed automatically with PyMechanical, -and can be used to run Mechanical from the command line. To obtain help on -usage, type the following command: - -.. code:: console - - $ ansys-mechanical --help - - Usage: ansys-mechanical [OPTIONS] - - CLI tool to run mechanical. - - USAGE: - - The following example demonstrates the main use of this tool: - - $ ansys-mechanical -r 242 -g - - Starting Ansys Mechanical version 2024R1 in graphical mode... - - Options: - -h, --help Show this message and exit. - -p, --project-file TEXT Opens Mechanical project file (.mechdb). Cannot - be mixed with -i - --private-appdata Make the appdata folder private. This enables you - to run parallel instances of Mechanical. - --port INTEGER Start mechanical in server mode with the given - port number - -i, --input-script TEXT Name of the input Python script. Cannot be mixed - with -p - --features TEXT Beta feature flags to set, as a semicolon - delimited list. Options: ['MultistageHarmonic', - 'ThermalShells'] - --exit Exit the application after running an input - script. You can only use this command with - --input-script argument (-i). The command - defaults to true you are not running the - application in graphical mode. The ``exit`` - command is only supported in version 2024 R1 or - later. - -s, --show-welcome-screen Show the welcome screen. You use this screen to - open a file. This argument only affects the - application when in graphical mode. - --debug Show a debug dialog window at the start of the - process. - -r, --revision INTEGER Ansys Revision number, e.g. "242" or "241". If - none is specified, uses the default from ansys- - tools-path - -g, --graphical Graphical mode - - ... - -You can launch Mechanical in server mode from the command line and then -manually connect to the server. Use the `port` argument to select the port. - -.. code:: - - ansys-mechanical --port 10000 - -Connect to a Mechanical session -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can connect to a Mechanical session from the same host or from an external host. - -Assuming that Mechanical is running locally at the default IP address (``127.0.0.1``) on the -default port (``10000``), you would use this code to connect to it with this code: - -.. code:: python - - from ansys.mechanical.core import Mechanical - - mechanical = Mechanical() - -Alternatively, you can use the -`connect_to_mechanical() <../api/ansys/mechanical/core/mechanical/index.html#mechanical.connect_to_mechanical>`_ -for same functionality. - -.. code:: python - - from ansys.mechanical.core import connect_to_mechanical - - mechanical = connect_to_mechanical() - - -Now assume that a remote instance of Mechanical has been started in server mode. To connect to -the computer on your local area network that is running Mechanical, you can use either -an IP address and port or a hostname and port. - -**IP address and port** - -Assume that Mechanical is running remotely at IP address ``192.168.0.1`` on port ``10000``. - -You would connect to it with this code: - -.. code:: python - - mechanical = Mechanical("192.168.0.1", port=10000) - -or - -.. code:: python - - mechanical = connect_to_mechanical("192.168.0.1", port=10000) - -**Hostname and port** - -Assume that Mechanical is running remotely at hostname ``myremotemachine`` on port ``10000``. - -You would connect to it with this code: - -.. code:: python - - mechanical = Mechanical("myremotemachine", port=10000) - -or - -.. code:: python - - mechanical = connect_to_mechanical("myremotemachine", port=10000) - -Launching issues ----------------- - -For any number of reasons, launching Mechanical can fail. Some approaches -follow for debugging launch failures. - -Manually set the location of the executable file -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you have a non-standard installation of Mechanical, PyMechanical might -not be able to find your installation. In this case, you should manually -set the location of your Mechanical executable file as the first parameter -for the `launch_mechanical()`_ method. - -**On Windows** - -.. code:: python - - from ansys.mechanical.core import launch_mechanical - - exec_loc = "C:/Program Files/ANSYS Inc/v242/aisol/bin/winx64/AnsysWBU.exe" - mechanical = launch_mechanical(exec_loc) - -**On Linux** - -.. code:: python - - from ansys.mechanical.core import launch_mechanical - - exec_loc = "/usr/ansys_inc/v242/aisol/.workbench" - mechanical = launch_mechanical(exec_loc) - -If, when using the `launch_mechanical()`_ -method, Mechanical still fails to launch or hangs while launching, pass the -``verbose_mechanical=True`` parameter. This prints the output of Mechanical in the Python console. -You can then use this output to debug why Mechanical isn't launching. - -.. Note:: - - On Windows, output is limited because of the way Mechanical launches. - -Debug from the command line -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You may need to run the ``launch`` command from the command line to debug why Mechanical is not launching. -running the launch command from the command line. - -Open a terminal and run the following command: - -.. code:: console - - ansys-mechanical -g --port 10000 - -If the preceding command for your operating system doesn't launch Mechanical, you might have -a variety of issues, including: - -- License server setup -- Running behind a VPN -- Missing dependencies - -Embed a Mechanical instance ---------------------------- - -The instructions for embedding a Mechanical instance are different on -Windows and Linux. While the Python code is the same in both cases, -Linux requires some additional environment variables. - -Python code -~~~~~~~~~~~ - -.. code:: pycon - - >>> from ansys.mechanical.core import App - >>> mechanical = App() - >>> mechanical - Ansys Mechanical [Ansys Mechanical Enterprise] - Product Version:242 - Software build date: 06/03/2024 14:47:58 - -Additional information for Linux -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Starting with 2023 R2, it is possible to embed an instance of Mechanical on Linux. -However, because of differences in how Mechanical works on Linux, you cannot simply -run Python as usual. On Linux, certain environment variables must be set for the Python -process before it starts. You can set up these environment variables using the ``mechanical-env`` -script which is part of PyMechanical - -.. code:: shell - - $ mechanical-env python - -Licensing issues ----------------- - -`PADT `_ has an `Ansys `_ -product section. Posts about licensing are common. - -If you are responsible for maintaining an Ansys license or have a personal installation -of Ansys, you likely can access the -`Licensing `_ -section of the Ansys Help, where you can view or download the *Ansys, Inc. Licensing Guide* for -comprehensive licensing information. - -VPN issues ----------- - -Sometimes, Mechanical has issues starting when VPN software is running. For more information, -access the `Mechanical Users Guide`_ -in the Ansys Help. +.. _using_standard_install: + +Launching PyMechanical +====================== + +The ``ansys-mechanical-core`` package requires either a local or +remote instance of Mechanical to communicate with. This page describes +how Mechanical is installed from the Ansys standard installer and +describes how you launch and interface with Mechanical from Python. + +Install Mechanical +------------------ + +Mechanical is installed by default from the Ansys standard installer. +When you run the standard installer, look under the **Structural Mechanics** +heading to verify that the **Mechanical Products** checkbox is selected. +Although options in the standard installer might change, this image provides +a reference: + +.. figure:: ../images/unified_install_2023R1.jpg + :width: 400pt + +Launch a remote Mechanical session +---------------------------------- + +You can use PyMechanical to launch a Mechanical session on the local machine +Python is running on. Alternatively, you can run Mechanical's command line +directly on any machine to start Mechanical in server mode and then use its +IP address to manually connect to it from Python. + +Launch Mechanical on the local machine using Python +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When Mechanical is installed locally on your machine, you can use the +`launch_mechanical() <../api/ansys/mechanical/core/mechanical/index.html#mechanical.launch_mechanical>`_ +method to launch and automatically connect to Mechanical. While this method provides the +easiest and fastest way to launch Mechanical, it only works with a local Mechanical installation. + +Launch Mechanical locally with this code: + +.. code:: pycon + + >>> from ansys.mechanical.core import launch_mechanical + >>> mechanical = launch_mechanical() + >>> mechanical + + Ansys Mechanical [Ansys Mechanical Enterprise] + Product Version:251 + Software build date: 11/27/2024 09:34:44 + +Launch Mechanical from the command line +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The `ansys-mechanical` utility is installed automatically with PyMechanical, +and can be used to run Mechanical from the command line. To obtain help on +usage, type the following command: + +.. code:: console + + $ ansys-mechanical --help + + Usage: ansys-mechanical [OPTIONS] + + CLI tool to run mechanical. + + USAGE: + + The following example demonstrates the main use of this tool: + + $ ansys-mechanical -r 251 -g + + Starting Ansys Mechanical version 2025R1 in graphical mode... + + Options: + -h, --help Show this message and exit. + -p, --project-file TEXT Opens Mechanical project file (.mechdb). Cannot + be mixed with -i + --private-appdata Make the appdata folder private. This enables you + to run parallel instances of Mechanical. + --port INTEGER Start mechanical in server mode with the given + port number + -i, --input-script TEXT Name of the input Python script. Cannot be mixed + with -p + --features TEXT Beta feature flags to set, as a semicolon + delimited list. Options: ['MultistageHarmonic', + 'ThermalShells'] + --exit Exit the application after running an input + script. You can only use this command with + --input-script argument (-i). The command + defaults to true you are not running the + application in graphical mode. The ``exit`` + command is only supported in version 2024 R1 or + later. + -s, --show-welcome-screen Show the welcome screen. You use this screen to + open a file. This argument only affects the + application when in graphical mode. + --debug Show a debug dialog window at the start of the + process. + -r, --revision INTEGER Ansys Revision number, e.g. "232", "241", "242" or "251". + If none is specified, uses the default from ansys- + tools-path + -g, --graphical Graphical mode + + ... + +You can launch Mechanical in server mode from the command line and then +manually connect to the server. Use the `port` argument to select the port. + +.. code:: + + ansys-mechanical --port 10000 + +Connect to a Mechanical session +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can connect to a Mechanical session from the same host or from an external host. + +Assuming that Mechanical is running locally at the default IP address (``127.0.0.1``) on the +default port (``10000``), you would use this code to connect to it with this code: + +.. code:: python + + from ansys.mechanical.core import Mechanical + + mechanical = Mechanical() + +Alternatively, you can use the +`connect_to_mechanical() <../api/ansys/mechanical/core/mechanical/index.html#mechanical.connect_to_mechanical>`_ +for same functionality. + +.. code:: python + + from ansys.mechanical.core import connect_to_mechanical + + mechanical = connect_to_mechanical() + + +Now assume that a remote instance of Mechanical has been started in server mode. To connect to +the computer on your local area network that is running Mechanical, you can use either +an IP address and port or a hostname and port. + +**IP address and port** + +Assume that Mechanical is running remotely at IP address ``192.168.0.1`` on port ``10000``. + +You would connect to it with this code: + +.. code:: python + + mechanical = Mechanical("192.168.0.1", port=10000) + +or + +.. code:: python + + mechanical = connect_to_mechanical("192.168.0.1", port=10000) + +**Hostname and port** + +Assume that Mechanical is running remotely at hostname ``myremotemachine`` on port ``10000``. + +You would connect to it with this code: + +.. code:: python + + mechanical = Mechanical("myremotemachine", port=10000) + +or + +.. code:: python + + mechanical = connect_to_mechanical("myremotemachine", port=10000) + +Launching issues +---------------- + +For any number of reasons, launching Mechanical can fail. Some approaches +follow for debugging launch failures. + +Manually set the location of the executable file +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you have a non-standard installation of Mechanical, PyMechanical might +not be able to find your installation. In this case, you should manually +set the location of your Mechanical executable file as the first parameter +for the `launch_mechanical()`_ method. + +**On Windows** + +.. code:: python + + from ansys.mechanical.core import launch_mechanical + + exec_loc = "C:/Program Files/ANSYS Inc/v251/aisol/bin/winx64/AnsysWBU.exe" + mechanical = launch_mechanical(exec_loc) + +**On Linux** + +.. code:: python + + from ansys.mechanical.core import launch_mechanical + + exec_loc = "/usr/ansys_inc/v251/aisol/.workbench" + mechanical = launch_mechanical(exec_loc) + +If, when using the `launch_mechanical()`_ +method, Mechanical still fails to launch or hangs while launching, pass the +``verbose_mechanical=True`` parameter. This prints the output of Mechanical in the Python console. +You can then use this output to debug why Mechanical isn't launching. + +.. Note:: + + On Windows, output is limited because of the way Mechanical launches. + +Debug from the command line +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You may need to run the ``launch`` command from the command line to debug why Mechanical is not launching. +running the launch command from the command line. + +Open a terminal and run the following command: + +.. code:: console + + ansys-mechanical -g --port 10000 + +If the preceding command for your operating system doesn't launch Mechanical, you might have +a variety of issues, including: + +- License server setup +- Running behind a VPN +- Missing dependencies + +Embed a Mechanical instance +--------------------------- + +The instructions for embedding a Mechanical instance are different on +Windows and Linux. While the Python code is the same in both cases, +Linux requires some additional environment variables. + +Python code +~~~~~~~~~~~ + +.. code:: pycon + + >>> from ansys.mechanical.core import App + >>> mechanical = App() + >>> mechanical + Ansys Mechanical [Ansys Mechanical Enterprise] + Product Version:251 + Software build date: 11/27/2024 09:34:44 + +Additional information for Linux +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Starting with 2023 R2, it is possible to embed an instance of Mechanical on Linux. +However, because of differences in how Mechanical works on Linux, you cannot simply +run Python as usual. On Linux, certain environment variables must be set for the Python +process before it starts. You can set up these environment variables using the ``mechanical-env`` +script which is part of PyMechanical + +.. code:: shell + + $ mechanical-env python + +Licensing issues +---------------- + +`PADT `_ has an `Ansys `_ +product section. Posts about licensing are common. + +If you are responsible for maintaining an Ansys license or have a personal installation +of Ansys, you likely can access the +`Licensing `_ +section of the Ansys Help, where you can view or download the *Ansys, Inc. Licensing Guide* for +comprehensive licensing information. + +VPN issues +---------- + +Sometimes, Mechanical has issues starting when VPN software is running. For more information, +access the `Mechanical Users Guide`_ +in the Ansys Help. diff --git a/doc/source/getting_started/wsl.rst b/doc/source/getting_started/wsl.rst index e3ec110af..964f3a4c2 100644 --- a/doc/source/getting_started/wsl.rst +++ b/doc/source/getting_started/wsl.rst @@ -70,7 +70,7 @@ To install Ansys products in WSL, perform these steps: .. code:: bash - tar xvzf STRUCTURES_2022R2_LINX64.tgz + tar xvzf STRUCTURES_2025R1_LINX64.tgz 3. To install Mechanical, go into the folder where the files have been extracted diff --git a/doc/source/index.rst b/doc/source/index.rst index 73478d69e..f425395e2 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -92,7 +92,7 @@ Python API to interact with `Ansys Mechanical`_ (FEA software for structural eng Issues and limitations on both PyMechanical and Mechanical. - :bdg-primary-line:`23R2` :bdg-primary-line:`24R1` :bdg-primary-line:`24R2` + :bdg-primary-line:`23R2` :bdg-primary-line:`24R1` :bdg-primary-line:`24R2` :bdg-primary-line:`25R1` .. grid-item-card:: Contribute :fa:`people-group` :padding: 2 2 2 2 diff --git a/doc/source/links.rst b/doc/source/links.rst index 40bf1f4ba..2da92c612 100644 --- a/doc/source/links.rst +++ b/doc/source/links.rst @@ -1,27 +1,28 @@ -.. # Pyansys -.. _pyansys: https://docs.pyansys.com/ -.. _pyansys_support: pyansys.support@ansys.com - -.. # PyMechanical related -.. _pymechanical_issue: https://github.com/ansys/pymechanical/issues -.. # PyMechanical Embedding Examples related -.. _pymechanical_embedding_ex_doc: https://embedding.examples.mechanical.docs.pyansys.com/ -.. _pymechanical_embedding_ex_repo: https://github.com/ansys/pymechanical-embedding-examples -.. _pymechanical_embedding_ex_basic: https://embedding.examples.mechanical.docs.pyansys.com/examples/index.html - -.. # PyMechanical Remote Sessions Examples related -.. _pymechanical_remote_ex_doc: https://examples.mechanical.docs.pyansys.com/ -.. _pymechanical_remote_ex_repo: https://github.com/ansys/pymechanical-examples -.. _pymechanical_remote_ex_all: https://examples.mechanical.docs.pyansys.com/examples/index.html -.. _Scripting in Mechanical Guide: https://ansyshelp.ansys.com/Views/Secured/corp/%%VERSION%%/en/act_script/act_script.html -.. _Mechanical Users Guide: https://ansyshelp.ansys.com/account/secured?returnurl=/Views/Secured/corp/%%VERSION%%/en/wb_sim/ds_Home.html -.. _Chapter 6: https://ansyshelp.ansys.com/account/secured?returnurl=/Views/Secured/corp/%%VERSION%%/en/installation/unix_silent.html -.. _Chapter 7: https://ansyshelp.ansys.com/account/secured?returnurl=/Views/Secured/corp/%%VERSION%%/en/installation/win_silent.html - -.. # Mechanical related -.. _Mechanical API Documentation: https://scripting.mechanical.docs.pyansys.com/version/dev/api/index.html -.. _Mechanical scripting interface APIs: https://developer.ansys.com/docs/mechanical-scripting-interface/api/index.md -.. _ACT API Reference Guide: https://ansyshelp.ansys.com/account/secured?returnurl=/Views/Secured/corp/v242/en/act_ref/act_ref.html -.. _Mechanical API known issues and limitations: https://ansyshelp.ansys.com/account/secured?returnurl=/Views/Secured/corp/%%VERSION%%/en/act_script/mech_apis_KIL.html?q=known%20issues -.. _ACT known issues and limitations: https://ansyshelp.ansys.com/account/secured?returnurl=/Views/Secured/corp/%%VERSION%%/en/act_dev/act_dev_knownissues.html +.. # Pyansys +.. _pyansys: https://docs.pyansys.com/ +.. _pyansys_support: pyansys.support@ansys.com + +.. # PyMechanical related +.. _pymechanical_issue: https://github.com/ansys/pymechanical/issues +.. # PyMechanical Embedding Examples related +.. _pymechanical_embedding_ex_doc: https://embedding.examples.mechanical.docs.pyansys.com/ +.. _pymechanical_embedding_ex_repo: https://github.com/ansys/pymechanical-embedding-examples +.. _pymechanical_embedding_ex_basic: https://embedding.examples.mechanical.docs.pyansys.com/examples/index.html + +.. # PyMechanical Remote Sessions Examples related +.. _pymechanical_remote_ex_doc: https://examples.mechanical.docs.pyansys.com/ +.. _pymechanical_remote_ex_repo: https://github.com/ansys/pymechanical-examples +.. _pymechanical_remote_ex_all: https://examples.mechanical.docs.pyansys.com/examples/index.html +.. _Scripting in Mechanical Guide: https://ansyshelp.ansys.com/Views/Secured/corp/%%VERSION%%/en/act_script/act_script.html +.. _Mechanical Users Guide: https://ansyshelp.ansys.com/account/secured?returnurl=/Views/Secured/corp/%%VERSION%%/en/wb_sim/ds_Home.html +.. _Chapter 6: https://ansyshelp.ansys.com/account/secured?returnurl=/Views/Secured/corp/%%VERSION%%/en/installation/unix_silent.html +.. _Chapter 7: https://ansyshelp.ansys.com/account/secured?returnurl=/Views/Secured/corp/%%VERSION%%/en/installation/win_silent.html + +.. # Mechanical related +.. _Mechanical API Documentation: https://scripting.mechanical.docs.pyansys.com/version/dev/api/index.html +.. _Mechanical scripting interface APIs: https://developer.ansys.com/docs/mechanical-scripting-interface/api/index.md +.. _ACT API Reference Guide: https://ansyshelp.ansys.com/account/secured?returnurl=/Views/Secured/corp/%%VERSION%%/en/act_ref/act_ref.html + +.. _Mechanical API known issues and limitations: https://ansyshelp.ansys.com/account/secured?returnurl=/Views/Secured/corp/%%VERSION%%/en/act_script/mech_apis_KIL.html?q=known%20issues +.. _ACT known issues and limitations: https://ansyshelp.ansys.com/account/secured?returnurl=/Views/Secured/corp/%%VERSION%%/en/act_dev/act_dev_knownissues.html .. _Ansys Mechanical: https://www.ansys.com/products/structures/ansys-mechanical \ No newline at end of file diff --git a/doc/source/user_guide_embedding/index.rst b/doc/source/user_guide_embedding/index.rst index 31e57079a..322454950 100644 --- a/doc/source/user_guide_embedding/index.rst +++ b/doc/source/user_guide_embedding/index.rst @@ -1,120 +1,120 @@ -.. _ref_user_guide_embedding: - -Embedded instance -================= - -This section provides an overview of how you use PyMechanical to embed -an instance of Mechanical in Python. - -.. - This toctree must be a top-level index to get it to show up in - pydata_sphinx_theme. - -.. toctree:: - :maxdepth: 1 - :hidden: - - self - configuration - globals - logging - libraries - -Overview --------- - -The `App <../api/ansys/mechanical/core/embedding/app/App.html>`_ class provides -a Mechanical instance: - -.. code:: python - - from ansys.mechanical.core import App - - app = App() - ns = app.DataModel.Project.Model.AddNamedSelection() - -The `App`_ class has access to the global scripting entry points that are -available from built-in Mechanical scripting: - -* ExtAPI: ``Application.ExtAPI`` -* DataModel: ``Application.DataModel`` -* Model: ``Application.DataModel.Project.Model`` -* Tree: ``Application.DataModel.Tree`` -* Graphics: ``Application.ExtAPI.Graphics`` - -Besides scripting entry points, many other types and objects are available from -built-in Mechanical scripting. To learn how to import scripting entry points, -namespaces, and types, see :ref:`ref_embedding_user_guide_globals`. - -Additional configuration ------------------------- - -By default, an instance of the `App`_ class -uses the same Addin configuration as standalone Mechanical. To customize Addins, see -:ref:`ref_embedding_user_guide_addin_configuration`. - -Diagnosing problems with embedding ----------------------------------- - -In some cases, debugging the embedded Mechanical instance may require additional logging. -For information on how to configure logging, see :ref:`ref_embedding_user_guide_logging`. - -Running PyMechanical embedding scripts inside Mechanical with IronPython ------------------------------------------------------------------------- - -If your PyMechanical embedding script does not use any other third-party Python package, such as `NumPy`, -it is possible to adapt it so that it can run inside of Mechanical with IronPython. -The scripting occurs inside Mechanical's command line interface. For instance, consider the following PyMechanical code: - -.. code:: python - - from ansys.mechanical.core import App - - app = App() - app.update_globals(globals()) - ns = DataModel.Project.Model.AddNamedSelection() - ns.Name = "Jarvis" - -The above code can be written as a Python file, such as ``file.py`` with only the following content: - -.. code:: python - - ns = DataModel.Project.Model.AddNamedSelection() - ns.Name = "Jarvis" - -Because the file does not contain the PyMechanical import statements, you can run -``file.py`` using the command line inside Mechanical. - -**Using command line interface (CLI)** - -This can be achieved on both the Windows and Linux platforms using -``ansys-mechanical`` cli from the virtual environment where ``ansys-mechanical-core`` -has been installed. Activate the virtual environment and then use CLI to run the scripts. -If multiple Mechanical versions are installed in the same system, -versions can be specified using ``-r`` flag. Use ``-h`` for more information. - -.. code:: - - ansys-mechanical -i file.py - -.. note:: - - Alternately user can use the following commands in the command prompt of Windows and the terminal - for Linux systems. - - **On Windows** - - .. code:: - - "C:/Program Files/ANSYS Inc/v242/aisol/bin/winx64/AnsysWBU.exe -DSApplet -AppModeMech -script file.py" - - PowerShell users can run the preceding command without including the opening and - closing quotation marks. - - **On Linux** - - .. code:: - - /usr/ansys_inc/v242/aisol/.workbench -DSApplet -AppModeMech -script file.py - - On either Windows or Linux, add the command line argument ``-b`` to run the script in batch mode. +.. _ref_user_guide_embedding: + +Embedded instance +================= + +This section provides an overview of how you use PyMechanical to embed +an instance of Mechanical in Python. + +.. + This toctree must be a top-level index to get it to show up in + pydata_sphinx_theme. + +.. toctree:: + :maxdepth: 1 + :hidden: + + self + configuration + globals + logging + libraries + +Overview +-------- + +The `App <../api/ansys/mechanical/core/embedding/app/App.html>`_ class provides +a Mechanical instance: + +.. code:: python + + from ansys.mechanical.core import App + + app = App() + ns = app.DataModel.Project.Model.AddNamedSelection() + +The `App`_ class has access to the global scripting entry points that are +available from built-in Mechanical scripting: + +* ExtAPI: ``Application.ExtAPI`` +* DataModel: ``Application.DataModel`` +* Model: ``Application.DataModel.Project.Model`` +* Tree: ``Application.DataModel.Tree`` +* Graphics: ``Application.ExtAPI.Graphics`` + +Besides scripting entry points, many other types and objects are available from +built-in Mechanical scripting. To learn how to import scripting entry points, +namespaces, and types, see :ref:`ref_embedding_user_guide_globals`. + +Additional configuration +------------------------ + +By default, an instance of the `App`_ class +uses the same Addin configuration as standalone Mechanical. To customize Addins, see +:ref:`ref_embedding_user_guide_addin_configuration`. + +Diagnosing problems with embedding +---------------------------------- + +In some cases, debugging the embedded Mechanical instance may require additional logging. +For information on how to configure logging, see :ref:`ref_embedding_user_guide_logging`. + +Running PyMechanical embedding scripts inside Mechanical with IronPython +------------------------------------------------------------------------ + +If your PyMechanical embedding script does not use any other third-party Python package, such as `NumPy`, +it is possible to adapt it so that it can run inside of Mechanical with IronPython. +The scripting occurs inside Mechanical's command line interface. For instance, consider the following PyMechanical code: + +.. code:: python + + from ansys.mechanical.core import App + + app = App() + app.update_globals(globals()) + ns = DataModel.Project.Model.AddNamedSelection() + ns.Name = "Jarvis" + +The above code can be written as a Python file, such as ``file.py`` with only the following content: + +.. code:: python + + ns = DataModel.Project.Model.AddNamedSelection() + ns.Name = "Jarvis" + +Because the file does not contain the PyMechanical import statements, you can run +``file.py`` using the command line inside Mechanical. + +**Using command line interface (CLI)** + +This can be achieved on both the Windows and Linux platforms using +``ansys-mechanical`` cli from the virtual environment where ``ansys-mechanical-core`` +has been installed. Activate the virtual environment and then use CLI to run the scripts. +If multiple Mechanical versions are installed in the same system, +versions can be specified using ``-r`` flag. Use ``-h`` for more information. + +.. code:: + + ansys-mechanical -i file.py + +.. note:: + + Alternately user can use the following commands in the command prompt of Windows and the terminal + for Linux systems. + + **On Windows** + + .. code:: + + "C:/Program Files/ANSYS Inc/v251/aisol/bin/winx64/AnsysWBU.exe -DSApplet -AppModeMech -script file.py" + + PowerShell users can run the preceding command without including the opening and + closing quotation marks. + + **On Linux** + + .. code:: + + /usr/ansys_inc/v251/aisol/.workbench -DSApplet -AppModeMech -script file.py + + On either Windows or Linux, add the command line argument ``-b`` to run the script in batch mode. diff --git a/doc/source/user_guide_embedding/libraries.rst b/doc/source/user_guide_embedding/libraries.rst index 592216e7c..2abf49853 100644 --- a/doc/source/user_guide_embedding/libraries.rst +++ b/doc/source/user_guide_embedding/libraries.rst @@ -1,36 +1,36 @@ -.. _ref_embedding_user_guide_libraries: - -Libraries -========= - -.. note:: - - This is an experimental feature. Some of these libraries may not work. - -Most of Mechanical's scripting APIs are implemented in C#. However, there are a small number -of Python modules that are distributed with the installation of Mechanical that can be used -from within the Mechanical Scripting Pane. These modules are not available for use from an -embedded instance of Mechanical in Python because Python does not know where to find them. - -But, in order to use these modules, you need to use the experimental function -``add_mechanical_python_libraries`` to help Python locate them and make it possible to import -them. In addition, it is necessary to first initialize the embedded instance of Mechanical -because these libraries may expect the .NET Common Language Runtime to be initialized as well -as for the appropriate C# libraries to be loaded. - -To use the above function, run the following: - -.. code:: python - - - from ansys.mechanical.core import App - from ansys.mechanical.core.embedding import add_mechanical_python_libraries - - app = App(version=242) - - add_mechanical_python_libraries(app) - import materials # This is materials.py that's shipped with Mechanical v242 - -.. warning:: - +.. _ref_embedding_user_guide_libraries: + +Libraries +========= + +.. note:: + + This is an experimental feature. Some of these libraries may not work. + +Most of Mechanical's scripting APIs are implemented in C#. However, there are a small number +of Python modules that are distributed with the installation of Mechanical that can be used +from within the Mechanical Scripting Pane. These modules are not available for use from an +embedded instance of Mechanical in Python because Python does not know where to find them. + +But, in order to use these modules, you need to use the experimental function +``add_mechanical_python_libraries`` to help Python locate them and make it possible to import +them. In addition, it is necessary to first initialize the embedded instance of Mechanical +because these libraries may expect the .NET Common Language Runtime to be initialized as well +as for the appropriate C# libraries to be loaded. + +To use the above function, run the following: + +.. code:: python + + + from ansys.mechanical.core import App + from ansys.mechanical.core.embedding import add_mechanical_python_libraries + + app = App(version=251) + + add_mechanical_python_libraries(app) + import materials # This is materials.py that's shipped with Mechanical v251 + +.. warning:: + Using version as argument to ``add_mechanical_python_libraries()`` is deprecated. \ No newline at end of file diff --git a/doc/source/user_guide_session/index.rst b/doc/source/user_guide_session/index.rst index e09753fa9..1c088fb34 100644 --- a/doc/source/user_guide_session/index.rst +++ b/doc/source/user_guide_session/index.rst @@ -1,85 +1,85 @@ -.. _ref_user_guide_session: - -Remote session -============== - -This section provides an overview of how you use PyMechanical as a client -to a remote Mechanical session. - -.. - This toctreemust be a top level index to get it to show up in - pydata_sphinx_theme - -.. toctree:: - :maxdepth: 1 - :hidden: - - self - server-launcher - mechanical - pool - -Overview --------- - -The `launch_mechanical() <../api/ansys/mechanical/core/mechanical/index.html#mechanical.launch_mechanical>`_ method -creates an instance of the `Mechanical <../api/ansys/mechanical/core/mechanical/Mechanical.html>`_ -class in the background and sends commands to it as a service. Because errors and warnings -are processed Pythonically, you can develop a script in real time without worrying about -whether the script runs correctly when deployed in batch mode. - -Here is how you use the `launch_mechanical()`_ method to launch Mechanical from Python in gRPC mode: - -.. code:: python - - import os - from ansys.mechanical.core import launch_mechanical - - mechanical = launch_mechanical() - -If multiple versions of product are installed, then you can use ``exec_file`` argument -to select the version of the product to launch. - -.. code:: python - - exec_file_path = "C:/Program Files/ANSYS Inc/v242/aisol/bin/win64/AnsysWBU.exe" - mechanical = launch_mechanical( - exec_file=exec_file_path, batch=False, cleanup_on_exit=False - ) - -If ``batch`` option is set ``True`` the Mechanical launches without GUI. The ``cleanup_on_exit`` -option decides whether product exits at the end of the PyMechanical script or not. - -.. note:: - ``version`` argument is used only if PyPIM is configured. For general cases, use ``exec_file`` - -You can send genuine Python class commands to the application when Mechanical is active. -For example, you can send a Python script: - -.. code:: python - - result = mechanical.run_python_script("2+3") - result = mechanical.run_python_script("ExtAPI.DataModel.Project.ProjectDirectory") - -Mechanical interactively returns the result of each command that you send, -saving the result to the logging module. - -Errors are caught immediately. In the following code, an invalid command is sent, -and an error is raised: - -.. code:: pycon - - >>> mechanical.run_python_script("2****3") - grpc.RpcError: - "unexpected token '**'" - -Because the error is caught immediately, you can write your Mechanical scripts in -Python, run them interactively, and then run them in batch without worrying if the -scripts run correctly. This would not be the case if you had instead outputted the -scripts that you wrote to script files. - -The `Mechanical`_ class supports -much more than sending text to Mechanical. It includes higher-level wrapping -that provides for better scripting and interaction with Mechanical. For information -on advanced methods for interacting with Mechanical, see :ref:`ref_examples`. - +.. _ref_user_guide_session: + +Remote session +============== + +This section provides an overview of how you use PyMechanical as a client +to a remote Mechanical session. + +.. + This toctreemust be a top level index to get it to show up in + pydata_sphinx_theme + +.. toctree:: + :maxdepth: 1 + :hidden: + + self + server-launcher + mechanical + pool + +Overview +-------- + +The `launch_mechanical() <../api/ansys/mechanical/core/mechanical/index.html#mechanical.launch_mechanical>`_ method +creates an instance of the `Mechanical <../api/ansys/mechanical/core/mechanical/Mechanical.html>`_ +class in the background and sends commands to it as a service. Because errors and warnings +are processed Pythonically, you can develop a script in real time without worrying about +whether the script runs correctly when deployed in batch mode. + +Here is how you use the `launch_mechanical()`_ method to launch Mechanical from Python in gRPC mode: + +.. code:: python + + import os + from ansys.mechanical.core import launch_mechanical + + mechanical = launch_mechanical() + +If multiple versions of product are installed, then you can use ``exec_file`` argument +to select the version of the product to launch. + +.. code:: python + + exec_file_path = "C:/Program Files/ANSYS Inc/v251/aisol/bin/win64/AnsysWBU.exe" + mechanical = launch_mechanical( + exec_file=exec_file_path, batch=False, cleanup_on_exit=False + ) + +If ``batch`` option is set ``True`` the Mechanical launches without GUI. The ``cleanup_on_exit`` +option decides whether product exits at the end of the PyMechanical script or not. + +.. note:: + ``version`` argument is used only if PyPIM is configured. For general cases, use ``exec_file`` + +You can send genuine Python class commands to the application when Mechanical is active. +For example, you can send a Python script: + +.. code:: python + + result = mechanical.run_python_script("2+3") + result = mechanical.run_python_script("ExtAPI.DataModel.Project.ProjectDirectory") + +Mechanical interactively returns the result of each command that you send, +saving the result to the logging module. + +Errors are caught immediately. In the following code, an invalid command is sent, +and an error is raised: + +.. code:: pycon + + >>> mechanical.run_python_script("2****3") + grpc.RpcError: + "unexpected token '**'" + +Because the error is caught immediately, you can write your Mechanical scripts in +Python, run them interactively, and then run them in batch without worrying if the +scripts run correctly. This would not be the case if you had instead outputted the +scripts that you wrote to script files. + +The `Mechanical`_ class supports +much more than sending text to Mechanical. It includes higher-level wrapping +that provides for better scripting and interaction with Mechanical. For information +on advanced methods for interacting with Mechanical, see :ref:`ref_examples`. + diff --git a/doc/source/user_guide_session/pool.rst b/doc/source/user_guide_session/pool.rst index 1172ce8fe..2a370a062 100644 --- a/doc/source/user_guide_session/pool.rst +++ b/doc/source/user_guide_session/pool.rst @@ -1,83 +1,83 @@ -Create a pool of Mechanical servers -=================================== - -The `LocalMechanicalPool <../api/ansys/mechanical/core/pool/LocalMechanicalPool.html>`_ -class simplifies creating and connecting to multiple servers of the -`Mechanical <../api/ansys/mechanical/core/mechanical/Mechanical.html>`_ class for batch -processing. You can use this class for batch processing a set of input files or -other batch-related processes. - -This code shows how to create a pool with 10 instances: - -.. code:: pycon - - >>> from ansys.mechanical.core import LocalMechanicalPool - >>> pool = LocalMechanicalPool(10, version="242") - 'Mechanical Pool with 10 active instances' - -When you are creating a pool, you can supply additional keyword arguments. -For example, to restart failed instances, you can set ``restart_failed=True``: - -.. code:: pycon - - >>> import os - >>> my_path = os.getcmd() - >>> pool = LocalMechanicalPool(10, version="242", restart_failed=True) - Creating Pool: 100%|########| 10/10 [00:01<00:00, 1.43it/s] - -You can access each individual instance of Mechanical with this code: - -.. code:: pycon - - >>> pool[0] - - -Because this is a *self-healing pool*, if an instance of Mechanical stops -during a batch process, this instance is automatically restarted. When creating -the pool, you can disable this behavior by setting ``restart_failed=False``. - -Run a set of input files ------------------------- - -You can use the pool to run a set of pre-generated input files using the -`run_batch() <../api/ansys/mechanical/core/pool/LocalMechanicalPool.html#LocalMechanicalPool.run_batch>`_ method. - -For example, you can run the first set of 20 verification files with this code: - -.. code:: pycon - - >>>>>> from ansys.mechanical.core import examples - >>> files = [f"test{index}.py" for index in range(1, 21)] - >>> outputs = pool.run_batch(files) - >>> len(outputs) - 20 - -Run a user-defined function ---------------------------- - -While the previous example uses the `run_batch()`_ -method to run a set of inputs files, you can also use the -`map() <../api/ansys/mechanical/core/pool/LocalMechanicalPool.html#LocalMechanicalPool.map>`_ method to run a custom user-defined function on -each instance of Mechanical over a set of input files. - -.. code:: pycon - - >>> completed_indices = [] - >>> def func(mechanical, input_file, index): - ... # input_file, index = args - ... mechanical.clear() - ... output = mechanical.run_python_script_from_file(input_file) - ... completed_indices.append(index) - ... return output - ... - >>> inputs = [("test{index}.py", i) for i in range(1, 10)] - >>> output = pool.map(func, inputs, progress_bar=True, wait=True) - ['result1', - 'result2', - 'result3', - 'result4', - 'result5', - 'result6', - 'result7', - 'result8', - 'result9'] +Create a pool of Mechanical servers +=================================== + +The `LocalMechanicalPool <../api/ansys/mechanical/core/pool/LocalMechanicalPool.html>`_ +class simplifies creating and connecting to multiple servers of the +`Mechanical <../api/ansys/mechanical/core/mechanical/Mechanical.html>`_ class for batch +processing. You can use this class for batch processing a set of input files or +other batch-related processes. + +This code shows how to create a pool with 10 instances: + +.. code:: pycon + + >>> from ansys.mechanical.core import LocalMechanicalPool + >>> pool = LocalMechanicalPool(10, version="251") + 'Mechanical Pool with 10 active instances' + +When you are creating a pool, you can supply additional keyword arguments. +For example, to restart failed instances, you can set ``restart_failed=True``: + +.. code:: pycon + + >>> import os + >>> my_path = os.getcmd() + >>> pool = LocalMechanicalPool(10, version="251", restart_failed=True) + Creating Pool: 100%|########| 10/10 [00:01<00:00, 1.43it/s] + +You can access each individual instance of Mechanical with this code: + +.. code:: pycon + + >>> pool[0] + + +Because this is a *self-healing pool*, if an instance of Mechanical stops +during a batch process, this instance is automatically restarted. When creating +the pool, you can disable this behavior by setting ``restart_failed=False``. + +Run a set of input files +------------------------ + +You can use the pool to run a set of pre-generated input files using the +`run_batch() <../api/ansys/mechanical/core/pool/LocalMechanicalPool.html#LocalMechanicalPool.run_batch>`_ method. + +For example, you can run the first set of 20 verification files with this code: + +.. code:: pycon + + >>>>>> from ansys.mechanical.core import examples + >>> files = [f"test{index}.py" for index in range(1, 21)] + >>> outputs = pool.run_batch(files) + >>> len(outputs) + 20 + +Run a user-defined function +--------------------------- + +While the previous example uses the `run_batch()`_ +method to run a set of inputs files, you can also use the +`map() <../api/ansys/mechanical/core/pool/LocalMechanicalPool.html#LocalMechanicalPool.map>`_ method to run a custom user-defined function on +each instance of Mechanical over a set of input files. + +.. code:: pycon + + >>> completed_indices = [] + >>> def func(mechanical, input_file, index): + ... # input_file, index = args + ... mechanical.clear() + ... output = mechanical.run_python_script_from_file(input_file) + ... completed_indices.append(index) + ... return output + ... + >>> inputs = [("test{index}.py", i) for i in range(1, 10)] + >>> output = pool.map(func, inputs, progress_bar=True, wait=True) + ['result1', + 'result2', + 'result3', + 'result4', + 'result5', + 'result6', + 'result7', + 'result8', + 'result9'] diff --git a/doc/source/user_guide_session/server-launcher.rst b/doc/source/user_guide_session/server-launcher.rst index 24a815762..e92de84fd 100644 --- a/doc/source/user_guide_session/server-launcher.rst +++ b/doc/source/user_guide_session/server-launcher.rst @@ -24,13 +24,13 @@ Mechanical executable file. .. code:: - Enter location of Mechanical executable: /usr/ansys_inc/v242/aisol/.workbench + Enter location of Mechanical executable: /usr/ansys_inc/v251/aisol/.workbench **On Windows** .. code:: - Enter location of Mechanical executable: C:/Program Files/ANSYS Inc/v242/aisol/bin/winx64/AnsysWBU.exe + Enter location of Mechanical executable: C:/Program Files/ANSYS Inc/v251/aisol/bin/winx64/AnsysWBU.exe The settings file for Mechanical is stored locally. You do not need to enter the path again. If you must change the path, perhaps to change the default @@ -40,7 +40,7 @@ version of Mechanical, run the following: from ansys.mechanical import core as pymechanical - new_path = "C:/Program Files/ANSYS Inc/v242/aisol/bin/winx64/AnsysWBU.exe" + new_path = "C:/Program Files/ANSYS Inc/v251/aisol/bin/winx64/AnsysWBU.exe" pymechanical.change_default_mechanical_path(new_path) For more information, see the `change_default_mechanical_path() <../api/_autosummary/ansys.tools.path.change_default_mechanical_path.html#ansys.tools.path.change_default_mechanical_path>`_ @@ -55,7 +55,7 @@ Mechanical executable file. from ansys.mechanical.core import launch_mechanical - mechanical = launch_mechanical(exec_file="/usr/ansys_inc/v242/aisol/.workbench") + mechanical = launch_mechanical(exec_file="/usr/ansys_inc/v251/aisol/.workbench") **On Windows** @@ -64,7 +64,7 @@ Mechanical executable file. from ansys.mechanical.core import launch_mechanical mechanical = launch_mechanical( - exec_file="C:\\Program File\\ANSYS Inc\\v242\\aisol\\bin\\winx64\\AnsysWBU.exe" + exec_file="C:\\Program File\\ANSYS Inc\\v251\\aisol\\bin\\winx64\\AnsysWBU.exe" ) You can use the ``additional_switches`` keyword argument to specify additional arguments. @@ -73,7 +73,7 @@ You can use the ``additional_switches`` keyword argument to specify additional a from ansys.mechanical.core import launch_mechanical - custom_exec = "/usr/ansys_inc/v242/aisol/.workbench" + custom_exec = "/usr/ansys_inc/v251/aisol/.workbench" add_switch = f"-featureflags mechanical.material.import;" mechanical = launch_mechanical(additional_switches=add_switch) diff --git a/docker/make_container.rst b/docker/make_container.rst index 5b80b04df..22b19e162 100644 --- a/docker/make_container.rst +++ b/docker/make_container.rst @@ -1,139 +1,138 @@ - -Create your own Mechanical Docker container -=========================================== - -.. warning:: You need a valid Ansys license and an Ansys account to - complete the steps in this section. - -You can create your own Mechanical Docker container following -the steps on this page. -These steps use a local Ubuntu machine to generate the needed -files for the Mechanical container by installing Ansys products first -and then copying the generated files to the container. - - -Requirements -============ - -* A Linux machine, preferable with Ubuntu 20.04 or later. - CentOS Linux distribution is no longer supported. - This machine needs to have `Docker `_ installed. - -* A valid Ansys account. Your Ansys reseller should have - provided you with an account. - -* These files are provided: - - * `Dockerfile `_ - - * `.dockerignore `_ - - -Procedure -========= - -Download Mechanical installation files --------------------------------------------- - -Download the latest Mechanical version from the Ansys Customer Portal -(`Current Release `_). -You need to have a valid Ansys account with access to -the products to download. - -If you do not Ansys account information, contact your -IT manager. - - -Install Mechanical product --------------------------------- - -To install Mechanical product on an Ubuntu machine you can follow -`Install Mechanical `_ -if you are using the graphical user interface -or `Install Ansys products in WSL `_ -for the command line interface. The later approach can be reused with small changes in a -continuous integration workflow. - -To reduce the size of the final image, you might want to -install the minimal files by using: - -.. code:: bash - - sh /path-to-mechanical-installer \ - -silent -overwrite_preview -mechapdl -lsdyna \ - -install_dir /path-to-install-mechanical/ - - # example - # sh /home/username/download/linx/INSTALL \ - # -silent -overwrite_preview -mechapdl -lsdyna \ - # -install_dir /install/ansys_inc/ - - -Use ``sudo`` if you do not have write permissions in the installation directory. -The ``-mechapdl`` command installs Mechanical. - -Take note of where you are installing Ansys because the -directory path is need in the following section. - -Build Docker image ------------------- - -To build the Docker image, you must create a directory and copy -all the files you need in the image into this directory. - -The steps to copy these files and build the image are provided in the following script, -which you should modify to adapt it to your needs. - -.. code:: bash - - # Create env vars for the Dockerfile - export ANS_MAJOR_VERSION=25 - - export ANS_MINOR_VERSION=1 - export ANS_VERSION=${ANS_MAJOR_VERSION}${ANS_MINOR_VERSION} - - export TAG=mechanical:${ANS_MAJOR_VERSION}.${ANS_MINOR_VERSION} - # example: if Mechanical v251 is installed under /install/ansys_inc/v251 - - # use /install for path_to_mechanical_installation - export MECHANICAL_INSTALL_LOCATION=/path_to_mechanical_installation/ - - # example: if pymechanical is cloned under /some_location/pymechanical - # use /some_location for path-to-pymechanical - export PYMECHANICAL_LOCATION=/path-to-pymechanical - - # Create working directory - cd ${MECHANICAL_INSTALL_LOCATION} - - # Copy the Docker files - cp ${PYMECHANICAL_LOCATION}/pymechanical/docker/${ANS_VERSION}/Dockerfile . - cp ${PYMECHANICAL_LOCATION}/pymechanical/docker/${ANS_VERSION}/.dockerignore . - - # Build Docker image - sudo docker build -t $TAG --build-arg VERSION=$ANS_VERSION . - -Take note of the these paths: - -* ``path-to-pymechanical`` is the path where PyMechanical repository is located. -* ``path_to_mechanical_installation`` is the path to where you have locally installed Mechanical. - -Not all installation files are copied. In fact, the files ignored during the copying -are described in the `.dockerignore file `_. - -The Docker container configuration needed to build the container is described in the -`Dockerfile `_. - - -Summary -======= - - -* **Step 1:** Download the latest Mechanical version from the Ansys Customer Portal - (`Current Release `_). - -* **Step 2:** Install Mechanical in a known folder. You can reuse your local - installation if it is updated and the machine is running the same Ubuntu - version as the target Ubuntu Docker version. - -* **Step 3:** Build the Docker image with the provided Docker configuration files - and script. + +Create your own Mechanical Docker container +=========================================== + +.. warning:: You need a valid Ansys license and an Ansys account to + complete the steps in this section. + +You can create your own Mechanical Docker container following +the steps on this page. +These steps use a local Ubuntu machine to generate the needed +files for the Mechanical container by installing Ansys products first +and then copying the generated files to the container. + + +Requirements +============ + +* A Linux machine, preferable with Ubuntu 20.04 or later. + CentOS Linux distribution is no longer supported. + This machine needs to have `Docker `_ installed. + +* A valid Ansys account. Your Ansys reseller should have + provided you with an account. + +* These files are provided: + + * `Dockerfile `_ + + * `.dockerignore `_ + + +Procedure +========= + +Download Mechanical installation files +-------------------------------------------- + +Download the latest Mechanical version from the Ansys Customer Portal +(`Current Release `_). +You need to have a valid Ansys account with access to +the products to download. + +If you do not Ansys account information, contact your +IT manager. + + +Install Mechanical product +-------------------------------- + +To install Mechanical product on an Ubuntu machine you can follow +`Install Mechanical `_ +if you are using the graphical user interface +or `Install Ansys products in WSL `_ +for the command line interface. The later approach can be reused with small changes in a +continuous integration workflow. + +To reduce the size of the final image, you might want to +install the minimal files by using: + +.. code:: bash + + sh /path-to-mechanical-installer \ + -silent -overwrite_preview -mechapdl -lsdyna \ + -install_dir /path-to-install-mechanical/ + + # example + # sh /home/username/download/linx/INSTALL \ + # -silent -overwrite_preview -mechapdl -lsdyna \ + # -install_dir /install/ansys_inc/ + + +Use ``sudo`` if you do not have write permissions in the installation directory. +The ``-mechapdl`` command installs Mechanical. + +Take note of where you are installing Ansys because the +directory path is need in the following section. + +Build Docker image +------------------ + +To build the Docker image, you must create a directory and copy +all the files you need in the image into this directory. + +The steps to copy these files and build the image are provided in the following script, +which you should modify to adapt it to your needs. + +.. code:: bash + + # Create env vars for the Dockerfile + export ANS_MAJOR_VERSION=25 + export ANS_MINOR_VERSION=1 + export ANS_VERSION=${ANS_MAJOR_VERSION}${ANS_MINOR_VERSION} + + export TAG=mechanical:${ANS_MAJOR_VERSION}.${ANS_MINOR_VERSION} + # example: if Mechanical v251 is installed under /install/ansys_inc/v251 + + # use /install for path_to_mechanical_installation + export MECHANICAL_INSTALL_LOCATION=/path_to_mechanical_installation/ + + # example: if pymechanical is cloned under /some_location/pymechanical + # use /some_location for path-to-pymechanical + export PYMECHANICAL_LOCATION=/path-to-pymechanical + + # Create working directory + cd ${MECHANICAL_INSTALL_LOCATION} + + # Copy the Docker files + cp ${PYMECHANICAL_LOCATION}/pymechanical/docker/${ANS_VERSION}/Dockerfile . + cp ${PYMECHANICAL_LOCATION}/pymechanical/docker/${ANS_VERSION}/.dockerignore . + + # Build Docker image + sudo docker build -t $TAG --build-arg VERSION=$ANS_VERSION . + +Take note of the these paths: + +* ``path-to-pymechanical`` is the path where PyMechanical repository is located. +* ``path_to_mechanical_installation`` is the path to where you have locally installed Mechanical. + +Not all installation files are copied. In fact, the files ignored during the copying +are described in the `.dockerignore file `_. + +The Docker container configuration needed to build the container is described in the +`Dockerfile `_. + + +Summary +======= + + +* **Step 1:** Download the latest Mechanical version from the Ansys Customer Portal + (`Current Release `_). + +* **Step 2:** Install Mechanical in a known folder. You can reuse your local + installation if it is updated and the machine is running the same Ubuntu + version as the target Ubuntu Docker version. + +* **Step 3:** Build the Docker image with the provided Docker configuration files + and script. diff --git a/src/ansys/mechanical/core/_version.py b/src/ansys/mechanical/core/_version.py index 6d535e240..80335ea11 100644 --- a/src/ansys/mechanical/core/_version.py +++ b/src/ansys/mechanical/core/_version.py @@ -1,47 +1,48 @@ -# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. -# SPDX-License-Identifier: MIT -# -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -"""Version of ansys-mechanical-core module. - -On the ``main`` branch, use 'dev0' to denote a development version. -For example: - -# major, minor, patch -version_info = 0, 58, 'dev0' - -""" - -try: - import importlib.metadata as importlib_metadata -except ModuleNotFoundError: # pragma: no cover - import importlib_metadata - -# Read from the pyproject.toml -# major, minor, patch -__version__ = importlib_metadata.version("ansys-mechanical-core") - -SUPPORTED_MECHANICAL_VERSIONS = { - 242: "2024R2", - 241: "2024R1", - 232: "2023R2", -} -"""Supported mechanical versions in descending order.""" +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Version of ansys-mechanical-core module. + +On the ``main`` branch, use 'dev0' to denote a development version. +For example: + +# major, minor, patch +version_info = 0, 58, 'dev0' + +""" + +try: + import importlib.metadata as importlib_metadata +except ModuleNotFoundError: # pragma: no cover + import importlib_metadata + +# Read from the pyproject.toml +# major, minor, patch +__version__ = importlib_metadata.version("ansys-mechanical-core") + +SUPPORTED_MECHANICAL_VERSIONS = { + 251: "2025R1", + 242: "2024R2", + 241: "2024R1", + 232: "2023R2", +} +"""Supported mechanical versions in descending order.""" diff --git a/src/ansys/mechanical/core/embedding/app.py b/src/ansys/mechanical/core/embedding/app.py index 80e79524b..708b3151a 100644 --- a/src/ansys/mechanical/core/embedding/app.py +++ b/src/ansys/mechanical/core/embedding/app.py @@ -1,611 +1,610 @@ -# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. -# SPDX-License-Identifier: MIT -# -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -"""Main application class for embedded Mechanical.""" -from __future__ import annotations - -import atexit -import os -from pathlib import Path -import shutil -import typing -import warnings - -from ansys.mechanical.core.embedding import initializer, runtime -from ansys.mechanical.core.embedding.addins import AddinConfiguration -from ansys.mechanical.core.embedding.appdata import UniqueUserProfile -from ansys.mechanical.core.embedding.imports import global_entry_points, global_variables -from ansys.mechanical.core.embedding.poster import Poster -from ansys.mechanical.core.embedding.ui import launch_ui -from ansys.mechanical.core.embedding.warnings import connect_warnings, disconnect_warnings - -if typing.TYPE_CHECKING: - # Make sure to run ``ansys-mechanical-ideconfig`` to add the autocomplete settings to VS Code - # Run ``ansys-mechanical-ideconfig --help`` for more information - import Ansys # pragma: no cover - -try: - import ansys.tools.visualization_interface # noqa: F401 - - HAS_ANSYS_VIZ = True - """Whether or not PyVista exists.""" -except ImportError: - HAS_ANSYS_VIZ = False - - -def _get_default_addin_configuration() -> AddinConfiguration: - configuration = AddinConfiguration() - return configuration - - -INSTANCES = [] -"""List of instances.""" - - -def _dispose_embedded_app(instances): # pragma: nocover - if len(instances) > 0: - instance = instances[0] - instance._dispose() - - -def _cleanup_private_appdata(profile: UniqueUserProfile): - profile.cleanup() - - -def _start_application(configuration: AddinConfiguration, version, db_file) -> "App": - import clr - - clr.AddReference("Ansys.Mechanical.Embedding") - import Ansys - - if configuration.no_act_addins: - os.environ["ANSYS_MECHANICAL_STANDALONE_NO_ACT_EXTENSIONS"] = "1" - - addin_configuration_name = configuration.addin_configuration - # Starting with version 241 we can pass a configuration name to the constructor - # of Application - if int(version) >= 241: - return Ansys.Mechanical.Embedding.Application(db_file, addin_configuration_name) - else: - return Ansys.Mechanical.Embedding.Application(db_file) - - -class GetterWrapper(object): - """Wrapper class around an attribute of an object.""" - - def __init__(self, obj, getter): - """Create a new instance of GetterWrapper.""" - # immortal class which provides wrapped object - self.__dict__["_immortal_object"] = obj - # function to get the wrapped object from the immortal class - self.__dict__["_get_wrapped_object"] = getter - - def __getattr__(self, attr): - """Wrap getters to the wrapped object.""" - if attr in self.__dict__: - return getattr(self, attr) - return getattr(self._get_wrapped_object(self._immortal_object), attr) - - def __setattr__(self, attr, value): - """Wrap setters to the wrapped object.""" - if attr in self.__dict__: - setattr(self, attr, value) - setattr(self._get_wrapped_object(self._immortal_object), attr, value) - - -class App: - """Mechanical embedding Application. - - Parameters - ---------- - db_file : str, optional - Path to a mechanical database file (.mechdat or .mechdb). - version : int, optional - Version number of the Mechanical application. - private_appdata : bool, optional - Setting for a temporary AppData directory. Default is False. - Enables running parallel instances of Mechanical. - config : AddinConfiguration, optional - Configuration for addins. By default "Mechanical" is used and ACT Addins are disabled. - copy_profile : bool, optional - Whether to copy the user profile when private_appdata is True. Default is True. - - Examples - -------- - Create App with Mechanical project file and version: - - >>> from ansys.mechanical.core import App - >>> app = App(db_file="path/to/file.mechdat", version=241, pri) - - - Disable copying the user profile when private appdata is enabled - - >>> app = App(private_appdata=True, copy_profile=False) - - Create App with "Mechanical" configuration and no ACT Addins - - >>> from ansys.mechanical.core.embedding import AddinConfiguration - >>> from ansys.mechanical.core import App - >>> config = AddinConfiguration("Mechanical") - >>> config.no_act_addins = True - >>> app = App(config=config) - """ - - def __init__(self, db_file=None, private_appdata=False, **kwargs): - """Construct an instance of the mechanical Application.""" - global INSTANCES - from ansys.mechanical.core import BUILDING_GALLERY - - if BUILDING_GALLERY: - if len(INSTANCES) != 0: - instance: App = INSTANCES[0] - instance._share(self) - if db_file is not None: - self.open(db_file) - return - if len(INSTANCES) > 0: - raise Exception("Cannot have more than one embedded mechanical instance!") - version = kwargs.get("version") - if version is not None: - try: - version = int(version) - except ValueError: - raise ValueError( - "The version must be an integer or of type that can be converted to an integer." - ) - self._version = initializer.initialize(version) - configuration = kwargs.get("config", _get_default_addin_configuration()) - - if private_appdata: - copy_profile = kwargs.get("copy_profile", True) - new_profile_name = f"PyMechanical-{os.getpid()}" - profile = UniqueUserProfile(new_profile_name, copy_profile=copy_profile) - profile.update_environment(os.environ) - atexit.register(_cleanup_private_appdata, profile) - - self._app = _start_application(configuration, self._version, db_file) - runtime.initialize(self._version) - connect_warnings(self) - self._poster = None - - self._disposed = False - atexit.register(_dispose_embedded_app, INSTANCES) - INSTANCES.append(self) - self._updated_scopes: typing.List[typing.Dict[str, typing.Any]] = [] - self._subscribe() - - def __repr__(self): - """Get the product info.""" - import clr - - clr.AddReference("Ansys.Mechanical.Application") - import Ansys - - return Ansys.Mechanical.Application.ProductInfo.ProductInfoAsString - - def __enter__(self): # pragma: no cover - """Enter the scope.""" - return self - - def __exit__(self, exc_type, exc_val, exc_tb): # pragma: no cover - """Exit the scope.""" - self._dispose() - - def _dispose(self): - if self._disposed: - return - self._unsubscribe() - disconnect_warnings(self) - self._app.Dispose() - self._disposed = True - - def open(self, db_file, remove_lock=False): - """Open the db file. - - Parameters - ---------- - db_file : str - Path to a Mechanical database file (.mechdat or .mechdb). - remove_lock : bool, optional - Whether or not to remove the lock file if it exists before opening the project file. - """ - if remove_lock: - lock_file = Path(self.DataModel.Project.ProjectDirectory) / ".mech_lock" - # Remove the lock file if it exists before opening the project file - if lock_file.exists(): - warnings.warn( - f"Removing the lock file, {lock_file}, before opening the project. \ -This may corrupt the project file.", - UserWarning, - stacklevel=2, - ) - lock_file.unlink() - - self.DataModel.Project.Open(db_file) - - def save(self, path=None): - """Save the project.""" - if path is not None: - self.DataModel.Project.Save(path) - else: - self.DataModel.Project.Save() - - def save_as(self, path: str, overwrite: bool = False): - """ - Save the project as a new file. - - If the `overwrite` flag is enabled, the current saved file is replaced with the new file. - - Parameters - ---------- - path : str - The path where the file needs to be saved. - overwrite : bool, optional - Whether the file should be overwritten if it already exists (default is False). - - Raises - ------ - Exception - If the file already exists at the specified path and `overwrite` is False. - - Notes - ----- - For version 232, if `overwrite` is True, the existing file and its associated directory - (if any) will be removed before saving the new file. - """ - if not os.path.exists(path): - self.DataModel.Project.SaveAs(path) - return - - if not overwrite: - raise Exception( - f"File already exists in {path}, Use ``overwrite`` flag to " - "replace the existing file." - ) - if self.version < 241: # pragma: no cover - file_name = os.path.basename(path) - file_dir = os.path.dirname(path) - associated_dir = os.path.join(file_dir, os.path.splitext(file_name)[0] + "_Mech_Files") - - # Remove existing files and associated folder - os.remove(path) - if os.path.exists(associated_dir): - shutil.rmtree(associated_dir) - # Save the new file - self.DataModel.Project.SaveAs(path) - else: - self.DataModel.Project.SaveAs(path, overwrite) - - def launch_gui(self, delete_tmp_on_close: bool = True, dry_run: bool = False): - """Launch the GUI.""" - launch_ui(self, delete_tmp_on_close, dry_run) - - def new(self): - """Clear to a new application.""" - self.DataModel.Project.New() - - def close(self): - """Close the active project.""" - # Call New() to remove the lock file of the - # current project on close. - self.DataModel.Project.New() - - def exit(self): - """Exit the application.""" - self._unsubscribe() - if self.version < 241: - self.ExtAPI.Application.Close() - else: - self.ExtAPI.Application.Exit() - - def execute_script(self, script: str) -> typing.Any: - """Execute the given script with the internal IronPython engine.""" - SCRIPT_SCOPE = "pymechanical-internal" - if not hasattr(self, "script_engine"): - import clr - - clr.AddReference("Ansys.Mechanical.Scripting") - import Ansys - - engine_type = Ansys.Mechanical.Scripting.ScriptEngineType.IronPython - script_engine = Ansys.Mechanical.Scripting.EngineFactory.CreateEngine(engine_type) - empty_scope = False - debug_mode = False - script_engine.CreateScope(SCRIPT_SCOPE, empty_scope, debug_mode) - self.script_engine = script_engine - light_mode = True - args = None - rets = None - script_result = self.script_engine.ExecuteCode(script, SCRIPT_SCOPE, light_mode, args, rets) - error_msg = f"Failed to execute the script" - if script_result is None: - raise Exception(error_msg) - if script_result.Error is not None: - error_msg += f": {script_result.Error.Message}" - raise Exception(error_msg) - return script_result.Value - - def execute_script_from_file(self, file_path=None): - """Execute the given script from file with the internal IronPython engine.""" - text_file = open(file_path, "r", encoding="utf-8") - data = text_file.read() - text_file.close() - return self.execute_script(data) - - def plotter(self) -> None: - """Return ``ansys.tools.visualization_interface.Plotter`` object.""" - if not HAS_ANSYS_VIZ: - warnings.warn( - "Installation of viz option required! Use pip install ansys-mechanical-core[viz]" - ) - return - - if self.version < 242: - warnings.warn("Plotting is only supported with version 2024R2 and later!") - return - - # TODO Check if anything loaded inside app or else show warning and return - - from ansys.mechanical.core.embedding.viz.embedding_plotter import to_plotter - - return to_plotter(self) - - def plot(self) -> None: - """Visualize the model in 3d. - - Requires installation using the viz option. E.g. - pip install ansys-mechanical-core[viz] - - Examples - -------- - >>> from ansys.mechanical.core import App - >>> app = App() - >>> app.open("path/to/file.mechdat") - >>> app.plot() - """ - _plotter = self.plotter() - - if _plotter is None: - return - - return _plotter.show() - - @property - def poster(self) -> Poster: - """Returns an instance of Poster.""" - if self._poster is None: - self._poster = Poster() - return self._poster - - @property - def DataModel(self) -> Ansys.Mechanical.DataModel.Interfaces.DataModelObject: - """Return the DataModel.""" - return GetterWrapper(self._app, lambda app: app.DataModel) - - @property - def ExtAPI(self) -> Ansys.ACT.Interfaces.Mechanical.IMechanicalExtAPI: - """Return the ExtAPI object.""" - return GetterWrapper(self._app, lambda app: app.ExtAPI) - - @property - def Tree(self) -> Ansys.ACT.Automation.Mechanical.Tree: - """Return the Tree object.""" - return GetterWrapper(self._app, lambda app: app.DataModel.Tree) - - @property - def Model(self) -> Ansys.ACT.Automation.Mechanical.Model: - """Return the Model object.""" - return GetterWrapper(self._app, lambda app: app.DataModel.Project.Model) - - @property - def Graphics(self) -> Ansys.ACT.Common.Graphics.MechanicalGraphicsWrapper: - """Return the Graphics object.""" - return GetterWrapper(self._app, lambda app: app.ExtAPI.Graphics) - - @property - def readonly(self): - """Return whether the Mechanical object is read-only.""" - import Ansys - - return Ansys.ACT.Mechanical.MechanicalAPI.Instance.ReadOnlyMode - - @property - def version(self): - """Returns the version of the app.""" - return self._version - - @property - def project_directory(self): - """Returns the current project directory.""" - return self.DataModel.Project.ProjectDirectory - - def _share(self, other) -> None: - """Shares the state of self with other. - - Other is another instance of App. - This is used when the BUILDING_GALLERY flag is on. - In that mode, multiple instance of App are used, but - they all point to the same underlying application - object. Because of that, special care needs to be - taken to properly share the state. Other will be - a "weak reference", which doesn't own anything. - """ - # the other app is not expecting to have a project - # already loaded - self.new() - - # set up the type hint (typing.Self is python3.11+) - other: App = other - - # copy `self` state to other. - other._app = self._app - other._version = self._version - other._poster = self._poster - other._updated_scopes = self._updated_scopes - - # all events will be handled by the original App instance - other._subscribed = False - - # finally, set the other disposed flag to be true - # so that the shutdown sequence isn't duplicated - other._disposed = True - - def _subscribe(self): - try: - # This will throw an error when using pythonnet because - # EventSource isn't defined on the IApplication interface - self.ExtAPI.Application.EventSource.OnWorkbenchReady += self._on_workbench_ready - self._subscribed = True - except: - self._subscribed = False - - def _unsubscribe(self): - if not self._subscribed: - return - self._subscribed = False - self.ExtAPI.Application.EventSource.OnWorkbenchReady -= self._on_workbench_ready - - def _on_workbench_ready(self, sender, args) -> None: - self._update_all_globals() - - def update_globals( - self, globals_dict: typing.Dict[str, typing.Any], enums: bool = True - ) -> None: - """Update global variables. - - When scripting inside Mechanical, the Mechanical UI automatically - sets global variables in Python. PyMechanical cannot do that automatically, - but this method can be used. - - By default, all enums will be imported too. To avoid including enums, set - the `enums` argument to False. - - Examples - -------- - >>> from ansys.mechanical.core import App - >>> app = App() - >>> app.update_globals(globals()) - """ - self._updated_scopes.append(globals_dict) - globals_dict.update(global_variables(self, enums)) - - def _update_all_globals(self) -> None: - for scope in self._updated_scopes: - scope.update(global_entry_points(self)) - - def _print_tree(self, node, max_lines, lines_count, indentation): - """Recursively print till provided maximum lines limit. - - Each object in the tree is expected to have the following attributes: - - Name: The name of the object. - - Suppressed : Print as suppressed, if object is suppressed. - - Children: Checks if object have children. - Each child node is expected to have the all these attributes. - - Parameters - ---------- - lines_count: int, optional - The current count of lines printed. Default is 0. - indentation: str, optional - The indentation string used for printing the tree structure. Default is "". - """ - if lines_count >= max_lines and max_lines != -1: - print(f"... truncating after {max_lines} lines") - return lines_count - - if not hasattr(node, "Name"): - raise AttributeError("Object must have a 'Name' attribute") - - node_name = node.Name - if hasattr(node, "Suppressed") and node.Suppressed is True: - node_name += " (Suppressed)" - if hasattr(node, "ObjectState"): - if str(node.ObjectState) == "UnderDefined": - node_name += " (?)" - elif str(node.ObjectState) == "Solved" or str(node.ObjectState) == "FullyDefined": - node_name += " (✓)" - elif str(node.ObjectState) == "NotSolved" or str(node.ObjectState) == "Obsolete": - node_name += " (⚡︎)" - elif str(node.ObjectState) == "SolveFailed": - node_name += " (✕)" - print(f"{indentation}├── {node_name}") - lines_count += 1 - - if lines_count >= max_lines and max_lines != -1: - print(f"... truncating after {max_lines} lines") - return lines_count - - if hasattr(node, "Children") and node.Children is not None and node.Children.Count > 0: - for child in node.Children: - lines_count = self._print_tree(child, max_lines, lines_count, indentation + "| ") - if lines_count >= max_lines and max_lines != -1: - break - - return lines_count - - def print_tree(self, node=None, max_lines=80, lines_count=0, indentation=""): - """ - Print the hierarchical tree representation of the Mechanical project structure. - - Parameters - ---------- - node: DataModel object, optional - The starting object of the tree. - max_lines: int, optional - The maximum number of lines to print. Default is 80. If set to -1, no limit is applied. - - Raises - ------ - AttributeError - If the node does not have the required attributes. - - Examples - -------- - >>> from ansys.mechanical.core import App - >>> app = App() - >>> app.update_globals(globals()) - >>> app.print_tree() - ... ├── Project - ... | ├── Model - ... | | ├── Geometry Imports (⚡︎) - ... | | ├── Geometry (?) - ... | | ├── Materials (✓) - ... | | ├── Coordinate Systems (✓) - ... | | | ├── Global Coordinate System (✓) - ... | | ├── Remote Points (✓) - ... | | ├── Mesh (?) - - >>> app.print_tree(Model, 3) - ... ├── Model - ... | ├── Geometry Imports (⚡︎) - ... | ├── Geometry (?) - ... ... truncating after 3 lines - - >>> app.print_tree(max_lines=2) - ... ├── Project - ... | ├── Model - ... ... truncating after 2 lines - """ - if node is None: - node = self.DataModel.Project - - self._print_tree(node, max_lines, lines_count, indentation) +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Main application class for embedded Mechanical.""" +from __future__ import annotations + +import atexit +import os +from pathlib import Path +import shutil +import typing +import warnings + +from ansys.mechanical.core.embedding import initializer, runtime +from ansys.mechanical.core.embedding.addins import AddinConfiguration +from ansys.mechanical.core.embedding.appdata import UniqueUserProfile +from ansys.mechanical.core.embedding.imports import global_entry_points, global_variables +from ansys.mechanical.core.embedding.poster import Poster +from ansys.mechanical.core.embedding.ui import launch_ui +from ansys.mechanical.core.embedding.warnings import connect_warnings, disconnect_warnings + +if typing.TYPE_CHECKING: + # Make sure to run ``ansys-mechanical-ideconfig`` to add the autocomplete settings to VS Code + # Run ``ansys-mechanical-ideconfig --help`` for more information + import Ansys # pragma: no cover + +try: + import ansys.tools.visualization_interface # noqa: F401 + + HAS_ANSYS_VIZ = True + """Whether or not PyVista exists.""" +except ImportError: + HAS_ANSYS_VIZ = False + + +def _get_default_addin_configuration() -> AddinConfiguration: + configuration = AddinConfiguration() + return configuration + + +INSTANCES = [] +"""List of instances.""" + + +def _dispose_embedded_app(instances): # pragma: nocover + if len(instances) > 0: + instance = instances[0] + instance._dispose() + + +def _cleanup_private_appdata(profile: UniqueUserProfile): + profile.cleanup() + + +def _start_application(configuration: AddinConfiguration, version, db_file) -> "App": + import clr + + clr.AddReference("Ansys.Mechanical.Embedding") + import Ansys + + if configuration.no_act_addins: + os.environ["ANSYS_MECHANICAL_STANDALONE_NO_ACT_EXTENSIONS"] = "1" + + addin_configuration_name = configuration.addin_configuration + # Starting with version 241 we can pass a configuration name to the constructor + # of Application + if int(version) >= 241: + return Ansys.Mechanical.Embedding.Application(db_file, addin_configuration_name) + else: + return Ansys.Mechanical.Embedding.Application(db_file) + + +class GetterWrapper(object): + """Wrapper class around an attribute of an object.""" + + def __init__(self, obj, getter): + """Create a new instance of GetterWrapper.""" + # immortal class which provides wrapped object + self.__dict__["_immortal_object"] = obj + # function to get the wrapped object from the immortal class + self.__dict__["_get_wrapped_object"] = getter + + def __getattr__(self, attr): + """Wrap getters to the wrapped object.""" + if attr in self.__dict__: + return getattr(self, attr) + return getattr(self._get_wrapped_object(self._immortal_object), attr) + + def __setattr__(self, attr, value): + """Wrap setters to the wrapped object.""" + if attr in self.__dict__: + setattr(self, attr, value) + setattr(self._get_wrapped_object(self._immortal_object), attr, value) + + +class App: + """Mechanical embedding Application. + + Parameters + ---------- + db_file : str, optional + Path to a mechanical database file (.mechdat or .mechdb). + version : int, optional + Version number of the Mechanical application. + private_appdata : bool, optional + Setting for a temporary AppData directory. Default is False. + Enables running parallel instances of Mechanical. + config : AddinConfiguration, optional + Configuration for addins. By default "Mechanical" is used and ACT Addins are disabled. + copy_profile : bool, optional + Whether to copy the user profile when private_appdata is True. Default is True. + + Examples + -------- + Create App with Mechanical project file and version: + + >>> from ansys.mechanical.core import App + >>> app = App(db_file="path/to/file.mechdat", version=251) + + Disable copying the user profile when private appdata is enabled + + >>> app = App(private_appdata=True, copy_profile=False) + + Create App with "Mechanical" configuration and no ACT Addins + + >>> from ansys.mechanical.core.embedding import AddinConfiguration + >>> from ansys.mechanical.core import App + >>> config = AddinConfiguration("Mechanical") + >>> config.no_act_addins = True + >>> app = App(config=config) + """ + + def __init__(self, db_file=None, private_appdata=False, **kwargs): + """Construct an instance of the mechanical Application.""" + global INSTANCES + from ansys.mechanical.core import BUILDING_GALLERY + + if BUILDING_GALLERY: + if len(INSTANCES) != 0: + instance: App = INSTANCES[0] + instance._share(self) + if db_file is not None: + self.open(db_file) + return + if len(INSTANCES) > 0: + raise Exception("Cannot have more than one embedded mechanical instance!") + version = kwargs.get("version") + if version is not None: + try: + version = int(version) + except ValueError: + raise ValueError( + "The version must be an integer or of type that can be converted to an integer." + ) + self._version = initializer.initialize(version) + configuration = kwargs.get("config", _get_default_addin_configuration()) + + if private_appdata: + copy_profile = kwargs.get("copy_profile", True) + new_profile_name = f"PyMechanical-{os.getpid()}" + profile = UniqueUserProfile(new_profile_name, copy_profile=copy_profile) + profile.update_environment(os.environ) + atexit.register(_cleanup_private_appdata, profile) + + self._app = _start_application(configuration, self._version, db_file) + runtime.initialize(self._version) + connect_warnings(self) + self._poster = None + + self._disposed = False + atexit.register(_dispose_embedded_app, INSTANCES) + INSTANCES.append(self) + self._updated_scopes: typing.List[typing.Dict[str, typing.Any]] = [] + self._subscribe() + + def __repr__(self): + """Get the product info.""" + import clr + + clr.AddReference("Ansys.Mechanical.Application") + import Ansys + + return Ansys.Mechanical.Application.ProductInfo.ProductInfoAsString + + def __enter__(self): # pragma: no cover + """Enter the scope.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): # pragma: no cover + """Exit the scope.""" + self._dispose() + + def _dispose(self): + if self._disposed: + return + self._unsubscribe() + disconnect_warnings(self) + self._app.Dispose() + self._disposed = True + + def open(self, db_file, remove_lock=False): + """Open the db file. + + Parameters + ---------- + db_file : str + Path to a Mechanical database file (.mechdat or .mechdb). + remove_lock : bool, optional + Whether or not to remove the lock file if it exists before opening the project file. + """ + if remove_lock: + lock_file = Path(self.DataModel.Project.ProjectDirectory) / ".mech_lock" + # Remove the lock file if it exists before opening the project file + if lock_file.exists(): + warnings.warn( + f"Removing the lock file, {lock_file}, before opening the project. \ +This may corrupt the project file.", + UserWarning, + stacklevel=2, + ) + lock_file.unlink() + + self.DataModel.Project.Open(db_file) + + def save(self, path=None): + """Save the project.""" + if path is not None: + self.DataModel.Project.Save(path) + else: + self.DataModel.Project.Save() + + def save_as(self, path: str, overwrite: bool = False): + """ + Save the project as a new file. + + If the `overwrite` flag is enabled, the current saved file is replaced with the new file. + + Parameters + ---------- + path : str + The path where the file needs to be saved. + overwrite : bool, optional + Whether the file should be overwritten if it already exists (default is False). + + Raises + ------ + Exception + If the file already exists at the specified path and `overwrite` is False. + + Notes + ----- + For version 232, if `overwrite` is True, the existing file and its associated directory + (if any) will be removed before saving the new file. + """ + if not os.path.exists(path): + self.DataModel.Project.SaveAs(path) + return + + if not overwrite: + raise Exception( + f"File already exists in {path}, Use ``overwrite`` flag to " + "replace the existing file." + ) + if self.version < 241: # pragma: no cover + file_name = os.path.basename(path) + file_dir = os.path.dirname(path) + associated_dir = os.path.join(file_dir, os.path.splitext(file_name)[0] + "_Mech_Files") + + # Remove existing files and associated folder + os.remove(path) + if os.path.exists(associated_dir): + shutil.rmtree(associated_dir) + # Save the new file + self.DataModel.Project.SaveAs(path) + else: + self.DataModel.Project.SaveAs(path, overwrite) + + def launch_gui(self, delete_tmp_on_close: bool = True, dry_run: bool = False): + """Launch the GUI.""" + launch_ui(self, delete_tmp_on_close, dry_run) + + def new(self): + """Clear to a new application.""" + self.DataModel.Project.New() + + def close(self): + """Close the active project.""" + # Call New() to remove the lock file of the + # current project on close. + self.DataModel.Project.New() + + def exit(self): + """Exit the application.""" + self._unsubscribe() + if self.version < 241: + self.ExtAPI.Application.Close() + else: + self.ExtAPI.Application.Exit() + + def execute_script(self, script: str) -> typing.Any: + """Execute the given script with the internal IronPython engine.""" + SCRIPT_SCOPE = "pymechanical-internal" + if not hasattr(self, "script_engine"): + import clr + + clr.AddReference("Ansys.Mechanical.Scripting") + import Ansys + + engine_type = Ansys.Mechanical.Scripting.ScriptEngineType.IronPython + script_engine = Ansys.Mechanical.Scripting.EngineFactory.CreateEngine(engine_type) + empty_scope = False + debug_mode = False + script_engine.CreateScope(SCRIPT_SCOPE, empty_scope, debug_mode) + self.script_engine = script_engine + light_mode = True + args = None + rets = None + script_result = self.script_engine.ExecuteCode(script, SCRIPT_SCOPE, light_mode, args, rets) + error_msg = f"Failed to execute the script" + if script_result is None: + raise Exception(error_msg) + if script_result.Error is not None: + error_msg += f": {script_result.Error.Message}" + raise Exception(error_msg) + return script_result.Value + + def execute_script_from_file(self, file_path=None): + """Execute the given script from file with the internal IronPython engine.""" + text_file = open(file_path, "r", encoding="utf-8") + data = text_file.read() + text_file.close() + return self.execute_script(data) + + def plotter(self) -> None: + """Return ``ansys.tools.visualization_interface.Plotter`` object.""" + if not HAS_ANSYS_VIZ: + warnings.warn( + "Installation of viz option required! Use pip install ansys-mechanical-core[viz]" + ) + return + + if self.version < 242: + warnings.warn("Plotting is only supported with version 2024R2 and later!") + return + + # TODO Check if anything loaded inside app or else show warning and return + + from ansys.mechanical.core.embedding.viz.embedding_plotter import to_plotter + + return to_plotter(self) + + def plot(self) -> None: + """Visualize the model in 3d. + + Requires installation using the viz option. E.g. + pip install ansys-mechanical-core[viz] + + Examples + -------- + >>> from ansys.mechanical.core import App + >>> app = App() + >>> app.open("path/to/file.mechdat") + >>> app.plot() + """ + _plotter = self.plotter() + + if _plotter is None: + return + + return _plotter.show() + + @property + def poster(self) -> Poster: + """Returns an instance of Poster.""" + if self._poster is None: + self._poster = Poster() + return self._poster + + @property + def DataModel(self) -> Ansys.Mechanical.DataModel.Interfaces.DataModelObject: + """Return the DataModel.""" + return GetterWrapper(self._app, lambda app: app.DataModel) + + @property + def ExtAPI(self) -> Ansys.ACT.Interfaces.Mechanical.IMechanicalExtAPI: + """Return the ExtAPI object.""" + return GetterWrapper(self._app, lambda app: app.ExtAPI) + + @property + def Tree(self) -> Ansys.ACT.Automation.Mechanical.Tree: + """Return the Tree object.""" + return GetterWrapper(self._app, lambda app: app.DataModel.Tree) + + @property + def Model(self) -> Ansys.ACT.Automation.Mechanical.Model: + """Return the Model object.""" + return GetterWrapper(self._app, lambda app: app.DataModel.Project.Model) + + @property + def Graphics(self) -> Ansys.ACT.Common.Graphics.MechanicalGraphicsWrapper: + """Return the Graphics object.""" + return GetterWrapper(self._app, lambda app: app.ExtAPI.Graphics) + + @property + def readonly(self): + """Return whether the Mechanical object is read-only.""" + import Ansys + + return Ansys.ACT.Mechanical.MechanicalAPI.Instance.ReadOnlyMode + + @property + def version(self): + """Returns the version of the app.""" + return self._version + + @property + def project_directory(self): + """Returns the current project directory.""" + return self.DataModel.Project.ProjectDirectory + + def _share(self, other) -> None: + """Shares the state of self with other. + + Other is another instance of App. + This is used when the BUILDING_GALLERY flag is on. + In that mode, multiple instance of App are used, but + they all point to the same underlying application + object. Because of that, special care needs to be + taken to properly share the state. Other will be + a "weak reference", which doesn't own anything. + """ + # the other app is not expecting to have a project + # already loaded + self.new() + + # set up the type hint (typing.Self is python3.11+) + other: App = other + + # copy `self` state to other. + other._app = self._app + other._version = self._version + other._poster = self._poster + other._updated_scopes = self._updated_scopes + + # all events will be handled by the original App instance + other._subscribed = False + + # finally, set the other disposed flag to be true + # so that the shutdown sequence isn't duplicated + other._disposed = True + + def _subscribe(self): + try: + # This will throw an error when using pythonnet because + # EventSource isn't defined on the IApplication interface + self.ExtAPI.Application.EventSource.OnWorkbenchReady += self._on_workbench_ready + self._subscribed = True + except: + self._subscribed = False + + def _unsubscribe(self): + if not self._subscribed: + return + self._subscribed = False + self.ExtAPI.Application.EventSource.OnWorkbenchReady -= self._on_workbench_ready + + def _on_workbench_ready(self, sender, args) -> None: + self._update_all_globals() + + def update_globals( + self, globals_dict: typing.Dict[str, typing.Any], enums: bool = True + ) -> None: + """Update global variables. + + When scripting inside Mechanical, the Mechanical UI automatically + sets global variables in Python. PyMechanical cannot do that automatically, + but this method can be used. + + By default, all enums will be imported too. To avoid including enums, set + the `enums` argument to False. + + Examples + -------- + >>> from ansys.mechanical.core import App + >>> app = App() + >>> app.update_globals(globals()) + """ + self._updated_scopes.append(globals_dict) + globals_dict.update(global_variables(self, enums)) + + def _update_all_globals(self) -> None: + for scope in self._updated_scopes: + scope.update(global_entry_points(self)) + + def _print_tree(self, node, max_lines, lines_count, indentation): + """Recursively print till provided maximum lines limit. + + Each object in the tree is expected to have the following attributes: + - Name: The name of the object. + - Suppressed : Print as suppressed, if object is suppressed. + - Children: Checks if object have children. + Each child node is expected to have the all these attributes. + + Parameters + ---------- + lines_count: int, optional + The current count of lines printed. Default is 0. + indentation: str, optional + The indentation string used for printing the tree structure. Default is "". + """ + if lines_count >= max_lines and max_lines != -1: + print(f"... truncating after {max_lines} lines") + return lines_count + + if not hasattr(node, "Name"): + raise AttributeError("Object must have a 'Name' attribute") + + node_name = node.Name + if hasattr(node, "Suppressed") and node.Suppressed is True: + node_name += " (Suppressed)" + if hasattr(node, "ObjectState"): + if str(node.ObjectState) == "UnderDefined": + node_name += " (?)" + elif str(node.ObjectState) == "Solved" or str(node.ObjectState) == "FullyDefined": + node_name += " (✓)" + elif str(node.ObjectState) == "NotSolved" or str(node.ObjectState) == "Obsolete": + node_name += " (⚡︎)" + elif str(node.ObjectState) == "SolveFailed": + node_name += " (✕)" + print(f"{indentation}├── {node_name}") + lines_count += 1 + + if lines_count >= max_lines and max_lines != -1: + print(f"... truncating after {max_lines} lines") + return lines_count + + if hasattr(node, "Children") and node.Children is not None and node.Children.Count > 0: + for child in node.Children: + lines_count = self._print_tree(child, max_lines, lines_count, indentation + "| ") + if lines_count >= max_lines and max_lines != -1: + break + + return lines_count + + def print_tree(self, node=None, max_lines=80, lines_count=0, indentation=""): + """ + Print the hierarchical tree representation of the Mechanical project structure. + + Parameters + ---------- + node: DataModel object, optional + The starting object of the tree. + max_lines: int, optional + The maximum number of lines to print. Default is 80. If set to -1, no limit is applied. + + Raises + ------ + AttributeError + If the node does not have the required attributes. + + Examples + -------- + >>> from ansys.mechanical.core import App + >>> app = App() + >>> app.update_globals(globals()) + >>> app.print_tree() + ... ├── Project + ... | ├── Model + ... | | ├── Geometry Imports (⚡︎) + ... | | ├── Geometry (?) + ... | | ├── Materials (✓) + ... | | ├── Coordinate Systems (✓) + ... | | | ├── Global Coordinate System (✓) + ... | | ├── Remote Points (✓) + ... | | ├── Mesh (?) + + >>> app.print_tree(Model, 3) + ... ├── Model + ... | ├── Geometry Imports (⚡︎) + ... | ├── Geometry (?) + ... ... truncating after 3 lines + + >>> app.print_tree(max_lines=2) + ... ├── Project + ... | ├── Model + ... ... truncating after 2 lines + """ + if node is None: + node = self.DataModel.Project + + self._print_tree(node, max_lines, lines_count, indentation) diff --git a/src/ansys/mechanical/core/embedding/initializer.py b/src/ansys/mechanical/core/embedding/initializer.py index 3bb3d200d..bf5160dc1 100644 --- a/src/ansys/mechanical/core/embedding/initializer.py +++ b/src/ansys/mechanical/core/embedding/initializer.py @@ -34,7 +34,12 @@ INITIALIZED_VERSION = None """Constant for the initialized version.""" -SUPPORTED_MECHANICAL_EMBEDDING_VERSIONS = {242: "2024R2", 241: "2024R1", 232: "2023R2"} +SUPPORTED_MECHANICAL_EMBEDDING_VERSIONS = { + 251: "2025R1", + 242: "2024R2", + 241: "2024R1", + 232: "2023R2", +} """Supported Mechanical embedding versions on Windows.""" diff --git a/src/ansys/mechanical/core/embedding/logger/__init__.py b/src/ansys/mechanical/core/embedding/logger/__init__.py index 20818e19d..617ea2969 100644 --- a/src/ansys/mechanical/core/embedding/logger/__init__.py +++ b/src/ansys/mechanical/core/embedding/logger/__init__.py @@ -1,219 +1,219 @@ -# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. -# SPDX-License-Identifier: MIT -# -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -"""Embedding logger. - -Module to interact with the built-in logging system of Mechanical. - -Usage ------ - -Configuring logger -~~~~~~~~~~~~~~~~~~ - -Configuring the logger can be done using the :class:`Configuration ` class: - -.. code:: python - import ansys.mechanical.core as mech - from ansys.mechanical.core.embedding.logger import Configuration, Logger - - Configuration.configure(level=logging.INFO, to_stdout=True, base_directory=None) - app = mech.App(version=242) - -Then, the :class:`Logger ` class can be used to write messages to the log: - -.. code:: python - - Logger.error("message") - - -""" - -import logging -import os -import typing - -from ansys.mechanical.core.embedding import initializer -from ansys.mechanical.core.embedding.logger import environ, linux_api, sinks, windows_api - -LOGGING_SINKS: typing.Set[int] = set() -"""Constant for logging sinks.""" - -LOGGING_CONTEXT: str = "PYMECHANICAL" -"""Constant for logging context.""" - - -def _get_backend() -> ( - typing.Union[windows_api.APIBackend, linux_api.APIBackend, environ.EnvironBackend] -): - """Get the appropriate logger backend. - - Before embedding is initialized, logging is configured via environment variables. - After embedding is initialized, logging is configured by making API calls into the - Mechanical logging system. - - However, the API is mostly the same in both cases, though some methods only work - in one of the two backends. - - Setting the base directory only works before initializing. - Actually logging a message or flushing the log only works after initializing. - """ - # TODO - use abc instead of a union type? - embedding_initialized = initializer.INITIALIZED_VERSION is not None - if not embedding_initialized: - return environ.EnvironBackend() - if os.name == "nt": - return windows_api.APIBackend() - return linux_api.APIBackend() - - -class Configuration: - """Configures logger for Mechanical embedding.""" - - @classmethod - def configure(cls, level=logging.WARNING, directory=None, base_directory=None, to_stdout=True): - """Configure the logger for PyMechanical embedding. - - Parameters - ---------- - level : int, optional - Level of logging that is defined in the ``logging`` package. The default is 'DEBUG'. - Options are ``"DEBUG"``, ``"INFO"``, ``"WARNING"``, and ``"ERROR"``. - directory : str, optional - Directory to write log file to. The default is ``None``, but by default the log - will appear somewhere in the system temp folder. - base_directory: str, optional - Base directory to write log files to. Each instance of Mechanical will write its - log to a time-stamped subfolder within this directory. This is only possible to set - before Mechanical is initialized. - to_stdout : bool, optional - Whether to write log messages to the standard output, which is the - command line. The default is ``True``. - """ - # Set up the global log configuration. - cls.set_log_directory(directory) - cls.set_log_base_directory(base_directory) - - # Set up the sink-specific log configuration and store to global state. - cls._store_stdout_sink_enabled(to_stdout) - file_sink_enabled = directory is not None or base_directory is not None - cls._store_file_sink_enabled(file_sink_enabled) - - # Commit the sink-specific log configuration global state to the backend. - cls._commit_enabled_configuration() - cls.set_log_level(level) - - @classmethod - def set_log_to_stdout(cls, value: bool) -> None: - """Configure logging to write to the standard output.""" - cls._store_stdout_sink_enabled(value) - cls._commit_enabled_configuration() - - @classmethod - def set_log_to_file(cls, value: bool) -> None: - """Configure logging to write to a file.""" - cls._store_file_sink_enabled(value) - cls._commit_enabled_configuration() - - @classmethod - def set_log_level(cls, level: int) -> None: - """Set the log level for all configured sinks.""" - if len(LOGGING_SINKS) == 0: - raise Exception("No logging backend configured!") - cls._commit_level_configuration(level) - - @classmethod - def set_log_directory(cls, value: str) -> None: - """Configure logging to write to a directory.""" - if value is None: - return - _get_backend().set_directory(value) - - @classmethod - def set_log_base_directory(cls, directory: str) -> None: - """Configure logging to write in a time-stamped subfolder in this directory.""" - if directory is None: - return - _get_backend().set_base_directory(directory) - - @classmethod - def _commit_level_configuration(cls, level: int) -> None: - for sink in LOGGING_SINKS: - _get_backend().set_log_level(level, sink) - - @classmethod - def _commit_enabled_configuration(cls) -> None: - for sink in LOGGING_SINKS: - _get_backend().enable(sink) - - @classmethod - def _store_stdout_sink_enabled(cls, value: bool) -> None: - if value: - LOGGING_SINKS.add(sinks.StandardSinks.CONSOLE) - else: - LOGGING_SINKS.discard(sinks.StandardSinks.CONSOLE) - - @classmethod - def _store_file_sink_enabled(cls, value: bool) -> None: - if value: - LOGGING_SINKS.add(sinks.StandardSinks.STANDARD_LOG_FILE) - else: - LOGGING_SINKS.discard(sinks.StandardSinks.STANDARD_LOG_FILE) - - -class Logger: - """Provides the ``Logger`` class for embedding.""" - - @classmethod - def flush(cls): - """Flush the log.""" - _get_backend().flush() - - @classmethod - def can_log_message(cls, level: int) -> bool: - """Get whether a message at this level is logged.""" - return _get_backend().can_log_message(level) - - @classmethod - def debug(cls, msg: str): - """Write a debug message to the log.""" - _get_backend().log_message(logging.DEBUG, LOGGING_CONTEXT, msg) - - @classmethod - def error(cls, msg: str): - """Write a error message to the log.""" - _get_backend().log_message(logging.ERROR, LOGGING_CONTEXT, msg) - - @classmethod - def info(cls, msg: str): - """Write an info message to the log.""" - _get_backend().log_message(logging.INFO, LOGGING_CONTEXT, msg) - - @classmethod - def warning(cls, msg: str): - """Write a warning message to the log.""" - _get_backend().log_message(logging.WARNING, LOGGING_CONTEXT, msg) - - @classmethod - def fatal(cls, msg: str): - """Write a fatal message to the log.""" - _get_backend().log_message(logging.FATAL, LOGGING_CONTEXT, msg) +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Embedding logger. + +Module to interact with the built-in logging system of Mechanical. + +Usage +----- + +Configuring logger +~~~~~~~~~~~~~~~~~~ + +Configuring the logger can be done using the :class:`Configuration ` class: + +.. code:: python + import ansys.mechanical.core as mech + from ansys.mechanical.core.embedding.logger import Configuration, Logger + + Configuration.configure(level=logging.INFO, to_stdout=True, base_directory=None) + app = mech.App(version=251) + +Then, the :class:`Logger ` class can be used to write messages to the log: + +.. code:: python + + Logger.error("message") + + +""" + +import logging +import os +import typing + +from ansys.mechanical.core.embedding import initializer +from ansys.mechanical.core.embedding.logger import environ, linux_api, sinks, windows_api + +LOGGING_SINKS: typing.Set[int] = set() +"""Constant for logging sinks.""" + +LOGGING_CONTEXT: str = "PYMECHANICAL" +"""Constant for logging context.""" + + +def _get_backend() -> ( + typing.Union[windows_api.APIBackend, linux_api.APIBackend, environ.EnvironBackend] +): + """Get the appropriate logger backend. + + Before embedding is initialized, logging is configured via environment variables. + After embedding is initialized, logging is configured by making API calls into the + Mechanical logging system. + + However, the API is mostly the same in both cases, though some methods only work + in one of the two backends. + + Setting the base directory only works before initializing. + Actually logging a message or flushing the log only works after initializing. + """ + # TODO - use abc instead of a union type? + embedding_initialized = initializer.INITIALIZED_VERSION is not None + if not embedding_initialized: + return environ.EnvironBackend() + if os.name == "nt": + return windows_api.APIBackend() + return linux_api.APIBackend() + + +class Configuration: + """Configures logger for Mechanical embedding.""" + + @classmethod + def configure(cls, level=logging.WARNING, directory=None, base_directory=None, to_stdout=True): + """Configure the logger for PyMechanical embedding. + + Parameters + ---------- + level : int, optional + Level of logging that is defined in the ``logging`` package. The default is 'DEBUG'. + Options are ``"DEBUG"``, ``"INFO"``, ``"WARNING"``, and ``"ERROR"``. + directory : str, optional + Directory to write log file to. The default is ``None``, but by default the log + will appear somewhere in the system temp folder. + base_directory: str, optional + Base directory to write log files to. Each instance of Mechanical will write its + log to a time-stamped subfolder within this directory. This is only possible to set + before Mechanical is initialized. + to_stdout : bool, optional + Whether to write log messages to the standard output, which is the + command line. The default is ``True``. + """ + # Set up the global log configuration. + cls.set_log_directory(directory) + cls.set_log_base_directory(base_directory) + + # Set up the sink-specific log configuration and store to global state. + cls._store_stdout_sink_enabled(to_stdout) + file_sink_enabled = directory is not None or base_directory is not None + cls._store_file_sink_enabled(file_sink_enabled) + + # Commit the sink-specific log configuration global state to the backend. + cls._commit_enabled_configuration() + cls.set_log_level(level) + + @classmethod + def set_log_to_stdout(cls, value: bool) -> None: + """Configure logging to write to the standard output.""" + cls._store_stdout_sink_enabled(value) + cls._commit_enabled_configuration() + + @classmethod + def set_log_to_file(cls, value: bool) -> None: + """Configure logging to write to a file.""" + cls._store_file_sink_enabled(value) + cls._commit_enabled_configuration() + + @classmethod + def set_log_level(cls, level: int) -> None: + """Set the log level for all configured sinks.""" + if len(LOGGING_SINKS) == 0: + raise Exception("No logging backend configured!") + cls._commit_level_configuration(level) + + @classmethod + def set_log_directory(cls, value: str) -> None: + """Configure logging to write to a directory.""" + if value is None: + return + _get_backend().set_directory(value) + + @classmethod + def set_log_base_directory(cls, directory: str) -> None: + """Configure logging to write in a time-stamped subfolder in this directory.""" + if directory is None: + return + _get_backend().set_base_directory(directory) + + @classmethod + def _commit_level_configuration(cls, level: int) -> None: + for sink in LOGGING_SINKS: + _get_backend().set_log_level(level, sink) + + @classmethod + def _commit_enabled_configuration(cls) -> None: + for sink in LOGGING_SINKS: + _get_backend().enable(sink) + + @classmethod + def _store_stdout_sink_enabled(cls, value: bool) -> None: + if value: + LOGGING_SINKS.add(sinks.StandardSinks.CONSOLE) + else: + LOGGING_SINKS.discard(sinks.StandardSinks.CONSOLE) + + @classmethod + def _store_file_sink_enabled(cls, value: bool) -> None: + if value: + LOGGING_SINKS.add(sinks.StandardSinks.STANDARD_LOG_FILE) + else: + LOGGING_SINKS.discard(sinks.StandardSinks.STANDARD_LOG_FILE) + + +class Logger: + """Provides the ``Logger`` class for embedding.""" + + @classmethod + def flush(cls): + """Flush the log.""" + _get_backend().flush() + + @classmethod + def can_log_message(cls, level: int) -> bool: + """Get whether a message at this level is logged.""" + return _get_backend().can_log_message(level) + + @classmethod + def debug(cls, msg: str): + """Write a debug message to the log.""" + _get_backend().log_message(logging.DEBUG, LOGGING_CONTEXT, msg) + + @classmethod + def error(cls, msg: str): + """Write a error message to the log.""" + _get_backend().log_message(logging.ERROR, LOGGING_CONTEXT, msg) + + @classmethod + def info(cls, msg: str): + """Write an info message to the log.""" + _get_backend().log_message(logging.INFO, LOGGING_CONTEXT, msg) + + @classmethod + def warning(cls, msg: str): + """Write a warning message to the log.""" + _get_backend().log_message(logging.WARNING, LOGGING_CONTEXT, msg) + + @classmethod + def fatal(cls, msg: str): + """Write a fatal message to the log.""" + _get_backend().log_message(logging.FATAL, LOGGING_CONTEXT, msg) diff --git a/src/ansys/mechanical/core/embedding/resolver.py b/src/ansys/mechanical/core/embedding/resolver.py index ebf8fe4da..af211e9e0 100644 --- a/src/ansys/mechanical/core/embedding/resolver.py +++ b/src/ansys/mechanical/core/embedding/resolver.py @@ -1,41 +1,41 @@ -# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. -# SPDX-License-Identifier: MIT -# -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -"""This is the .NET assembly resolving for embedding Ansys Mechanical. - -Note that for some Mechanical Addons - additional resolving may be -necessary. A resolve handler is shipped with Ansys Mechanical on windows -starting in version 23.1 and on linux starting in version 23.2 -""" - - -def resolve(version): - """Resolve function for all versions of Ansys Mechanical.""" - import clr # isort: skip - import System # isort: skip - - clr.AddReference("Ansys.Mechanical.Embedding") - import Ansys # isort: skip - - assembly_resolver = Ansys.Mechanical.Embedding.AssemblyResolver - resolve_handler = assembly_resolver.MechanicalResolveEventHandler - System.AppDomain.CurrentDomain.AssemblyResolve += resolve_handler +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""This is the .NET assembly resolving for embedding Ansys Mechanical. + +Note that for some Mechanical Addons - additional resolving may be +necessary. A resolve handler is shipped with Ansys Mechanical on Windows +starting in version 23.1 and on Linux starting in version 23.2 +""" + + +def resolve(version): + """Resolve function for all versions of Ansys Mechanical.""" + import clr # isort: skip + import System # isort: skip + + clr.AddReference("Ansys.Mechanical.Embedding") + import Ansys # isort: skip + + assembly_resolver = Ansys.Mechanical.Embedding.AssemblyResolver + resolve_handler = assembly_resolver.MechanicalResolveEventHandler + System.AppDomain.CurrentDomain.AssemblyResolve += resolve_handler diff --git a/src/ansys/mechanical/core/ide_config.py b/src/ansys/mechanical/core/ide_config.py index e7c1252e7..3c7813a11 100644 --- a/src/ansys/mechanical/core/ide_config.py +++ b/src/ansys/mechanical/core/ide_config.py @@ -1,212 +1,212 @@ -# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. -# SPDX-License-Identifier: MIT -# -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -"""Convenience CLI to run mechanical.""" - -import json -import os -from pathlib import Path -import re -import site -import sys - -import ansys.tools.path as atp -import click - - -def get_stubs_location(): - """Find the ansys-mechanical-stubs installation location in site-packages. - - Returns - ------- - pathlib.Path - The path to the ansys-mechanical-stubs installation in site-packages. - """ - site_packages = site.getsitepackages() - prefix_path = sys.prefix.replace("\\", "\\\\") - site_packages_regex = re.compile(f"{prefix_path}.*site-packages$") - site_packages_paths = list(filter(site_packages_regex.match, site_packages)) - - if len(site_packages_paths) == 1: - # Get the stubs location - stubs_location = Path(site_packages_paths[0]) / "ansys" / "mechanical" / "stubs" - return stubs_location - - raise Exception("Could not retrieve the location of the ansys-mechanical-stubs package.") - - -def get_stubs_versions(stubs_location: Path): - """Retrieve the revision numbers in ansys-mechanical-stubs. - - Parameters - ---------- - pathlib.Path - The path to the ansys-mechanical-stubs installation in site-packages. - - Returns - ------- - list - The list containing minimum and maximum versions in the ansys-mechanical-stubs package. - """ - # Get revision numbers in stubs folder - revns = [ - int(revision[1:]) - for revision in os.listdir(stubs_location) - if os.path.isdir(os.path.join(stubs_location, revision)) and revision.startswith("v") - ] - return revns - - -def _vscode_impl( - target: str = "user", - revision: int = None, -): - """Get the IDE configuration for autocomplete in VS Code. - - Parameters - ---------- - target: str - The type of settings to update. Either "user" or "workspace" in VS Code. - By default, it's ``user``. - revision: int - The Mechanical revision number. For example, "242". - If unspecified, it finds the default Mechanical version from ansys-tools-path. - """ - # Update the user or workspace settings - if target == "user": - # Get the path to the user's settings.json file depending on the platform - if "win" in sys.platform: - settings_json = ( - Path(os.environ.get("APPDATA")) / "Code" / "User" / "settings.json" - ) # pragma: no cover - elif "lin" in sys.platform: - settings_json = ( - Path(os.environ.get("HOME")) / ".config" / "Code" / "User" / "settings.json" - ) - elif target == "workspace": - # Get the current working directory - current_dir = Path.cwd() - # Get the path to the settings.json file based on the git root & .vscode folder - settings_json = current_dir / ".vscode" / "settings.json" - - # Location where the stubs are installed -> .venv/Lib/site-packages, for example - stubs_location = get_stubs_location() / f"v{revision}" - - # The settings to add to settings.json for autocomplete to work - settings_json_data = { - "python.autoComplete.extraPaths": [str(stubs_location)], - "python.analysis.extraPaths": [str(stubs_location)], - } - # Pretty print dictionary - pretty_dict = json.dumps(settings_json_data, indent=4) - - print(f"Update {settings_json} with the following information:\n") - - if target == "workspace": - print( - "Note: Please ensure the .vscode folder is in the root of your project or repository.\n" - ) - - print(pretty_dict) - - -def _cli_impl( - ide: str = "vscode", - target: str = "user", - revision: int = None, -): - """Provide the user with the path to the settings.json file and IDE settings. - - Parameters - ---------- - ide: str - The IDE to set up autocomplete settings. By default, it's ``vscode``. - target: str - The type of settings to update. Either "user" or "workspace" in VS Code. - By default, it's ``user``. - revision: int - The Mechanical revision number. For example, "242". - If unspecified, it finds the default Mechanical version from ansys-tools-path. - """ - # Get the ansys-mechanical-stubs install location - stubs_location = get_stubs_location() - # Get all revision numbers available in ansys-mechanical-stubs - revns = get_stubs_versions(stubs_location) - # Check the IDE and raise an exception if it's not VS Code - if revision < min(revns) or revision > max(revns): - raise Exception(f"PyMechanical Stubs are not available for {revision}") - elif ide != "vscode": - raise Exception(f"{ide} is not supported at the moment.") - else: - return _vscode_impl(target, revision) - - -@click.command() -@click.help_option("--help", "-h") -@click.option( - "--ide", - default="vscode", - type=str, - help="The IDE being used. By default, it's ``vscode``.", -) -@click.option( - "--target", - default="user", - type=str, - help="The type of settings to update - either ``user`` or ``workspace`` settings in VS Code.", -) -@click.option( - "--revision", - default=None, - type=int, - help='The Mechanical revision number, e.g. "242" or "241". If unspecified,\ -it finds and uses the default version from ansys-tools-path.', -) -def cli(ide: str, target: str, revision: int) -> None: - """CLI tool to update settings.json files for autocomplete with ansys-mechanical-stubs. - - Parameters - ---------- - ide: str - The IDE to set up autocomplete settings. By default, it's ``vscode``. - target: str - The type of settings to update. Either "user" or "workspace" in VS Code. - By default, it's ``user``. - revision: int - The Mechanical revision number. For example, "242". - If unspecified, it finds the default Mechanical version from ansys-tools-path. - - Usage - ----- - The following example demonstrates the main use of this tool: - - $ ansys-mechanical-ideconfig --ide vscode --target user --revision 242 - - """ - exe = atp.get_mechanical_path(allow_input=False, version=revision) - version = atp.version_from_path("mechanical", exe) - - return _cli_impl( - ide, - target, - version, - ) +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Convenience CLI to run mechanical.""" + +import json +import os +from pathlib import Path +import re +import site +import sys + +import ansys.tools.path as atp +import click + + +def get_stubs_location(): + """Find the ansys-mechanical-stubs installation location in site-packages. + + Returns + ------- + pathlib.Path + The path to the ansys-mechanical-stubs installation in site-packages. + """ + site_packages = site.getsitepackages() + prefix_path = sys.prefix.replace("\\", "\\\\") + site_packages_regex = re.compile(f"{prefix_path}.*site-packages$") + site_packages_paths = list(filter(site_packages_regex.match, site_packages)) + + if len(site_packages_paths) == 1: + # Get the stubs location + stubs_location = Path(site_packages_paths[0]) / "ansys" / "mechanical" / "stubs" + return stubs_location + + raise Exception("Could not retrieve the location of the ansys-mechanical-stubs package.") + + +def get_stubs_versions(stubs_location: Path): + """Retrieve the revision numbers in ansys-mechanical-stubs. + + Parameters + ---------- + pathlib.Path + The path to the ansys-mechanical-stubs installation in site-packages. + + Returns + ------- + list + The list containing minimum and maximum versions in the ansys-mechanical-stubs package. + """ + # Get revision numbers in stubs folder + revns = [ + int(revision[1:]) + for revision in os.listdir(stubs_location) + if os.path.isdir(os.path.join(stubs_location, revision)) and revision.startswith("v") + ] + return revns + + +def _vscode_impl( + target: str = "user", + revision: int = None, +): + """Get the IDE configuration for autocomplete in VS Code. + + Parameters + ---------- + target: str + The type of settings to update. Either "user" or "workspace" in VS Code. + By default, it's ``user``. + revision: int + The Mechanical revision number. For example, "251". + If unspecified, it finds the default Mechanical version from ansys-tools-path. + """ + # Update the user or workspace settings + if target == "user": + # Get the path to the user's settings.json file depending on the platform + if "win" in sys.platform: + settings_json = ( + Path(os.environ.get("APPDATA")) / "Code" / "User" / "settings.json" + ) # pragma: no cover + elif "lin" in sys.platform: + settings_json = ( + Path(os.environ.get("HOME")) / ".config" / "Code" / "User" / "settings.json" + ) + elif target == "workspace": + # Get the current working directory + current_dir = Path.cwd() + # Get the path to the settings.json file based on the git root & .vscode folder + settings_json = current_dir / ".vscode" / "settings.json" + + # Location where the stubs are installed -> .venv/Lib/site-packages, for example + stubs_location = get_stubs_location() / f"v{revision}" + + # The settings to add to settings.json for autocomplete to work + settings_json_data = { + "python.autoComplete.extraPaths": [str(stubs_location)], + "python.analysis.extraPaths": [str(stubs_location)], + } + # Pretty print dictionary + pretty_dict = json.dumps(settings_json_data, indent=4) + + print(f"Update {settings_json} with the following information:\n") + + if target == "workspace": + print( + "Note: Please ensure the .vscode folder is in the root of your project or repository.\n" + ) + + print(pretty_dict) + + +def _cli_impl( + ide: str = "vscode", + target: str = "user", + revision: int = None, +): + """Provide the user with the path to the settings.json file and IDE settings. + + Parameters + ---------- + ide: str + The IDE to set up autocomplete settings. By default, it's ``vscode``. + target: str + The type of settings to update. Either "user" or "workspace" in VS Code. + By default, it's ``user``. + revision: int + The Mechanical revision number. For example, "251". + If unspecified, it finds the default Mechanical version from ansys-tools-path. + """ + # Get the ansys-mechanical-stubs install location + stubs_location = get_stubs_location() + # Get all revision numbers available in ansys-mechanical-stubs + revns = get_stubs_versions(stubs_location) + # Check the IDE and raise an exception if it's not VS Code + if revision < min(revns) or revision > max(revns): + raise Exception(f"PyMechanical Stubs are not available for {revision}") + elif ide != "vscode": + raise Exception(f"{ide} is not supported at the moment.") + else: + return _vscode_impl(target, revision) + + +@click.command() +@click.help_option("--help", "-h") +@click.option( + "--ide", + default="vscode", + type=str, + help="The IDE being used. By default, it's ``vscode``.", +) +@click.option( + "--target", + default="user", + type=str, + help="The type of settings to update - either ``user`` or ``workspace`` settings in VS Code.", +) +@click.option( + "--revision", + default=None, + type=int, + help='The Mechanical revision number, e.g. "251" or "242". If unspecified,\ +it finds and uses the default version from ansys-tools-path.', +) +def cli(ide: str, target: str, revision: int) -> None: + """CLI tool to update settings.json files for autocomplete with ansys-mechanical-stubs. + + Parameters + ---------- + ide: str + The IDE to set up autocomplete settings. By default, it's ``vscode``. + target: str + The type of settings to update. Either "user" or "workspace" in VS Code. + By default, it's ``user``. + revision: int + The Mechanical revision number. For example, "251". + If unspecified, it finds the default Mechanical version from ansys-tools-path. + + Usage + ----- + The following example demonstrates the main use of this tool: + + $ ansys-mechanical-ideconfig --ide vscode --target user --revision 251 + + """ + exe = atp.get_mechanical_path(allow_input=False, version=revision) + version = atp.version_from_path("mechanical", exe) + + return _cli_impl( + ide, + target, + version, + ) diff --git a/src/ansys/mechanical/core/mechanical.py b/src/ansys/mechanical/core/mechanical.py index 58ac5c0f1..6651f5ef8 100644 --- a/src/ansys/mechanical/core/mechanical.py +++ b/src/ansys/mechanical/core/mechanical.py @@ -1,2324 +1,2324 @@ -# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. -# SPDX-License-Identifier: MIT -# -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -"""Connect to Mechanical gRPC server and issues commands.""" -import atexit -from contextlib import closing -import datetime -import fnmatch -from functools import wraps -import glob -import os -import pathlib -import socket -import threading -import time -import weakref - -import ansys.api.mechanical.v0.mechanical_pb2 as mechanical_pb2 -import ansys.api.mechanical.v0.mechanical_pb2_grpc as mechanical_pb2_grpc -import ansys.platform.instancemanagement as pypim -from ansys.platform.instancemanagement import Instance -import ansys.tools.path as atp -import grpc - -import ansys.mechanical.core as pymechanical -from ansys.mechanical.core import LOG -from ansys.mechanical.core.errors import ( - MechanicalExitedError, - MechanicalRuntimeError, - VersionError, - protect_grpc, -) -from ansys.mechanical.core.launcher import MechanicalLauncher -from ansys.mechanical.core.misc import ( - check_valid_ip, - check_valid_port, - check_valid_start_instance, - threaded, -) - -# Checking if tqdm is installed. -# If it is, the default value for progress_bar is true. -try: - from tqdm import tqdm - - _HAS_TQDM = True - """Whether or not tqdm is installed.""" -except ModuleNotFoundError: # pragma: no cover - _HAS_TQDM = False - -# Default 256 MB message length -MAX_MESSAGE_LENGTH = int(os.environ.get("PYMECHANICAL_MAX_MESSAGE_LENGTH", 256 * 1024**2)) -"""Default message length.""" - -# Chunk sizes for streaming and file streaming -DEFAULT_CHUNK_SIZE = 256 * 1024 # 256 kB -"""Default chunk size.""" -DEFAULT_FILE_CHUNK_SIZE = 1024 * 1024 # 1MB -"""Default file chunk size.""" - - -def setup_logger(loglevel="INFO", log_file=True, mechanical_instance=None): - """Initialize the logger for the given mechanical instance.""" - # Return existing log if this function has already been called - if hasattr(setup_logger, "log"): - return setup_logger.log - else: - setup_logger.log = LOG.add_instance_logger("Mechanical", mechanical_instance) - - setup_logger.log.setLevel(loglevel) - - if log_file: - if isinstance(log_file, str): - setup_logger.log.log_to_file(filename=log_file, level=loglevel) - - return setup_logger.log - - -def suppress_logging(func): - """Decorate a function to suppress the logging for a Mechanical instance.""" - - @wraps(func) - def wrapper(*args, **kwargs): - mechanical = args[0] - prior_log_level = mechanical.log.level - if prior_log_level != "CRITICAL": - mechanical.set_log_level("CRITICAL") - - out = func(*args, **kwargs) - - if prior_log_level != "CRITICAL": - mechanical.set_log_level(prior_log_level) - - return out - - return wrapper - - -LOCALHOST = "127.0.0.1" -"""Localhost address.""" - -MECHANICAL_DEFAULT_PORT = 10000 -"""Default Mechanical port.""" - -GALLERY_INSTANCE = [None] -"""List of gallery instances.""" - - -def _cleanup_gallery_instance(): # pragma: no cover - """Clean up any leftover instances of Mechanical from building the gallery.""" - if GALLERY_INSTANCE[0] is not None: - mechanical = Mechanical( - ip=GALLERY_INSTANCE[0]["ip"], - port=GALLERY_INSTANCE[0]["port"], - ) - mechanical.exit(force=True) - - -atexit.register(_cleanup_gallery_instance) - - -def port_in_use(port, host=LOCALHOST): - """Check whether a port is in use at the given host. - - You must actually *bind* the address. Just checking if you can create - a socket is insufficient because it is possible to run into permission - errors like:: - - An attempt was made to access a socket in a way forbidden by its - access permissions. - """ - with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: - if sock.connect_ex((host, port)) == 0: - return True - else: - return False - - -def check_ports(port_range, ip="localhost"): - """Check the state of ports in a port range.""" - ports = {} - for port in port_range: - ports[port] = port_in_use(port, ip) - return ports - - -def close_all_local_instances(port_range=None, use_thread=True): - """Close all Mechanical instances within a port range. - - You can use this method when cleaning up from a failed pool or - batch run. - - Parameters - ---------- - port_range : list, optional - List of a range of ports to use when cleaning up Mechanical. The - default is ``None``, in which case the ports managed by - PyMechanical are used. - - use_thread : bool, optional - Whether to use threads to close the Mechanical instances. - The default is ``True``. So this call will return immediately. - - Examples - -------- - Close all Mechanical instances connected on local ports. - - >>> import ansys.mechanical.core as pymechanical - >>> pymechanical.close_all_local_instances() - - """ - if port_range is None: - port_range = pymechanical.LOCAL_PORTS - - @threaded - def close_mechanical_threaded(port, name="Closing Mechanical instance in a thread"): - close_mechanical(port, name) - - def close_mechanical(port, name="Closing Mechanical instance"): - try: - mechanical = Mechanical(port=port) - LOG.debug(f"{name}: {mechanical.name}.") - mechanical.exit(force=True) - except OSError: # pragma: no cover - pass - - ports = check_ports(port_range) - for port_temp, state in ports.items(): - if state: - if use_thread: - close_mechanical_threaded(port_temp) - else: - close_mechanical(port_temp) - - -def create_ip_file(ip, path): - """Create the ``mylocal.ip`` file needed to change the IP address of the gRPC server.""" - file_name = os.path.join(path, "mylocal.ip") - with open(file_name, "w", encoding="utf-8") as f: - f.write(ip) - - -def get_mechanical_path(allow_input=True): - """Get path. - - Deprecated - use `ansys.tools.path.get_mechanical_path` instead - """ - return atp.get_mechanical_path(allow_input) - - -def check_valid_mechanical(): - """Change to see if the default Mechanical path is valid. - - Example (windows) - ----------------- - - >>> from ansys.mechanical.core import mechanical - >>> from ansys.tools.path import change_default_mechanical_path - >>> mechanical_path = 'C:/Program Files/ANSYS Inc/v242/aisol/bin/win64/AnsysWBU.exe' - >>> change_default_mechanical_path(mechanical_pth) - >>> mechanical.check_valid_mechanical() - True - - - """ - mechanical_path = atp.get_mechanical_path(False) - if mechanical_path is None: - return False - mechanical_version = atp.version_from_path("mechanical", mechanical_path) - return not (mechanical_version < 232 and os.name != "posix") - - -def change_default_mechanical_path(exe_loc): - """Change default path. - - Deprecated - use `ansys.tools.path.change_default_mechanical_path` instead. - """ - return atp.change_default_mechanical_path(exe_loc) - - -def save_mechanical_path(exe_loc=None): # pragma: no cover - """Save path. - - Deprecated - use `ansys.tools.path.save_mechanical_path` instead. - """ - return atp.save_mechanical_path(exe_loc) - - -client_to_server_loglevel = { - "DEBUG": 1, - "INFO": 2, - "WARN": 3, - "WARNING": 3, - "ERROR": 4, - "CRITICAL": 5, -} - - -class Mechanical(object): - """Connects to a gRPC Mechanical server and allows commands to be passed.""" - - # Required by `_name` method to be defined before __init__ be - _ip = None - _port = None - - def __init__( - self, - ip=None, - port=None, - timeout=60.0, - loglevel="WARNING", - log_file=False, - log_mechanical=None, - cleanup_on_exit=False, - channel=None, - remote_instance=None, - keep_connection_alive=True, - **kwargs, - ): - """Initialize the member variable based on the arguments. - - Parameters - ---------- - ip : str, optional - IP address to connect to the server. The default is ``None`` - in which case ``localhost`` is used. - port : int, optional - Port to connect to the Mecahnical server. The default is ``None``, - in which case ``10000`` is used. - timeout : float, optional - Maximum allowable time for connecting to the Mechanical server. - The default is ``60.0``. - loglevel : str, optional - Level of messages to print to the console. The default is ``WARNING``. - - - ``ERROR`` prints only error messages. - - ``WARNING`` prints warning and error messages. - - ``INFO`` prints info, warning and error messages. - - ``DEBUG`` prints debug, info, warning and error messages. - - log_file : bool, optional - Whether to copy the messages to a file named ``logs.log``, which is - located where the Python script is executed. The default is ``False``. - log_mechanical : str, optional - Path to the output file on the local disk for writing every script - command to. The default is ``None``. However, you might set - ``"log_mechanical='pymechanical_log.txt"`` to write all commands that are - sent to Mechanical via PyMechanical in this file so that you can use them - to run a script within Mechanical without PyMechanical. - cleanup_on_exit : bool, optional - Whether to exit Mechanical when Python exits. The default is ``False``, - in which case Mechanical is not exited when the garbage for this Mechanical - instance is collected. - channel : grpc.Channel, optional - gRPC channel to use for the connection. The default is ``None``. - You can use this parameter as an alternative to the ``ip`` and ``port`` - parameters. - remote_instance : ansys.platform.instancemanagement.Instance - Corresponding remote instance when Mechanical is launched - through PyPIM. The default is ``None``. If a remote instance - is specified, this instance is deleted when the - :func:`mecahnical.exit ` - function is called. - keep_connection_alive : bool, optional - Whether to keep the gRPC connection alive by running a background thread - and making dummy calls for remote connections. The default is ``True``. - - Examples - -------- - Connect to a Mechanical instance already running on locally on the - default port (``10000``). - - >>> from ansys.mechanical import core as pymechanical - >>> mechanical = pymechanical.Mechanical() - - Connect to a Mechanical instance running on the LAN on a default port. - - >>> mechanical = pymechanical.Mechanical('192.168.1.101') - - Connect to a Mechanical instance running on the LAN on a non-default port. - - >>> mechanical = pymechanical.Mechanical('192.168.1.101', port=60001) - - If you want to customize the channel, you can connect directly to gRPC channels. - For example, if you want to create an insecure channel with a maximum message - length of 8 MB, you would run: - - >>> import grpc - >>> channel_temp = grpc.insecure_channel( - ... '127.0.0.1:10000', - ... options=[ - ... ("grpc.max_receive_message_length", 8*1024**2), - ... ], - ... ) - >>> mechanical = pymechanical.Mechanical(channel=channel_temp) - """ - self._remote_instance = remote_instance - self._channel = channel - self._keep_connection_alive = keep_connection_alive - - self._locked = False # being used within MechanicalPool - - # ip could be a machine name. Convert it to an IP address. - ip_temp = ip - if channel is not None: - if ip is not None or port is not None: - raise ValueError( - "If `channel` is specified, neither `port` nor `ip` can be specified." - ) - elif ip is None: - ip_temp = "127.0.0.1" - else: - ip_temp = socket.gethostbyname(ip) # Converting ip or host name to ip - - self._ip = ip_temp - self._port = port - - self._start_parm = kwargs - - self._cleanup_on_exit = cleanup_on_exit - self._busy = False # used to check if running a command on the server - - self._local = ip_temp in ["127.0.0.1", "127.0.1.1", "localhost"] - if "local" in kwargs: # pragma: no cover # allow this to be overridden - self._local = kwargs["local"] - - self._health_response_queue = None - self._exiting = False - self._exited = None - - self._version = None - - if port is None: - port = MECHANICAL_DEFAULT_PORT - self._port = port - - self._stub = None - self._timeout = timeout - - if channel is None: - self._channel = self._create_channel(ip_temp, port) - else: - self._channel = channel - - self._logLevel = loglevel - self._log_file = log_file - self._log_mechanical = log_mechanical - - self._log = LOG.add_instance_logger(self.name, self, level=loglevel) # instance logger - # adding a file handler to the logger - if log_file: - if not isinstance(log_file, str): - log_file = "instance.log" - self._log.log_to_file(filename=log_file, level=loglevel) - - self._log_file_mechanical = log_mechanical - if log_mechanical: - if not isinstance(log_mechanical, str): - self._log_file_mechanical = "pymechanical_log.txt" - else: - self._log_file_mechanical = log_mechanical - - # temporarily disable logging - # useful when we run some dummy calls - self._disable_logging = False - - if self._local: - self.log_info("Mechanical connection is treated as local.") - else: - self.log_info("Mechanical connection is treated as remote.") - - # connect and validate to the channel - self._multi_connect(timeout=timeout) - - self.log_info("Mechanical is ready to accept grpc calls.") - - def __del__(self): # pragma: no cover - """Clean up on exit.""" - if self._cleanup_on_exit: - try: - self.exit(force=True) - except grpc.RpcError as e: - self.log_error(f"exit: {e}") - - # def _set_log_level(self, level): - # """Set an alias for the log level.""" - # self.set_log_level(level) - - @property - def log(self): - """Log associated with the current Mechanical instance.""" - return self._log - - @property - def version(self) -> str: - """Get the Mechanical version based on the instance. - - Examples - -------- - Get the version of the connected Mechanical instance. - - >>> mechanical.version - '242' - """ - if self._version is None: - try: - self._disable_logging = True - script = ( - 'clr.AddReference("Ans.Utilities")\n' - "import Ansys\n" - "config = Ansys.Utilities.ApplicationConfiguration.DefaultConfiguration\n" - "config.VersionInfo.VersionString" - ) - self._version = self.run_python_script(script) - except grpc.RpcError: # pragma: no cover - raise - finally: - self._disable_logging = False - return self._version - - @property - def name(self): - """Name (unique identifier) of the Mechanical instance.""" - try: - if self._channel is not None: - if self._remote_instance is not None: # pragma: no cover - return f"GRPC_{self._channel._channel._channel.target().decode()}" - else: - return f"GRPC_{self._channel._channel.target().decode()}" - except Exception as e: # pragma: no cover - LOG.error(f"Error getting the Mechanical instance name: {str(e)}") - - return f"GRPC_instance_{id(self)}" # pragma: no cover - - @property - def busy(self): - """Return True when the Mechanical gRPC server is executing a command.""" - return self._busy - - @property - def locked(self): - """Instance is in use within a pool.""" - return self._locked - - @locked.setter - def locked(self, new_value): - """Instance is in use within a pool.""" - self._locked = new_value - - def _multi_connect(self, n_attempts=5, timeout=60): - """Try to connect over a series of attempts to the channel. - - Parameters - ---------- - n_attempts : int, optional - Number of connection attempts. The default is ``5``. - timeout : float, optional - Maximum allowable time in seconds for establishing a connection. - The default is ``60``. - """ - # This prevents a single failed connection from blocking other attempts - connected = False - attempt_timeout = timeout / n_attempts - self.log_debug( - f"timetout:{timeout} n_attempts:{n_attempts} attempt_timeout={attempt_timeout}" - ) - - max_time = time.time() + timeout - i = 1 - while time.time() < max_time and i <= n_attempts: - self.log_debug(f"Connection attempt {i} with attempt timeout {attempt_timeout}s") - connected = self._connect(timeout=attempt_timeout) - - if connected: - self.log_debug(f"Connection attempt {i} succeeded.") - break - - i += 1 - else: # pragma: no cover - self.log_debug( - f"Reached either maximum amount of connection attempts " - f"({n_attempts}) or timeout ({timeout} s)." - ) - - if not connected: # pragma: no cover - raise IOError(f"Unable to connect to Mechanical instance at {self._channel_str}.") - - @property - def _channel_str(self): - """Target string, generally in the form of ``ip:port``, such as ``127.0.0.1:10000``.""" - if self._channel is not None: - if self._remote_instance is not None: - return self._channel._channel._channel.target().decode() # pragma: no cover - else: - return self._channel._channel.target().decode() - return "" # pragma: no cover - - def _connect(self, timeout=12, enable_health_check=False): - """Connect a gRPC channel to a remote or local Mechanical instance. - - Parameters - ---------- - timeout : float - Maximum allowable time in seconds for establishing a connection. The - default is ``12``. - enable_health_check : bool, optional - Whether to enable a check to see if the connection is healthy. - The default is ``False``. - """ - self._state = grpc.channel_ready_future(self._channel) - self._stub = mechanical_pb2_grpc.MechanicalServiceStub(self._channel) - - # verify connection - time_start = time.time() - while ((time.time() - time_start) < timeout) and not self._state._matured: - time.sleep(0.01) - - if not self._state._matured: # pragma: no cover - return False - - self.log_debug("Established a connection to the Mechanical gRPC server.") - - self.wait_till_mechanical_is_ready(timeout) - - # keeps Mechanical session alive - self._timer = None - if not self._local and self._keep_connection_alive: # pragma: no cover - self._initialised = threading.Event() - self._t_trigger = time.time() - self._t_delay = 30 - self._timer = threading.Thread( - target=Mechanical._threaded_heartbeat, args=(weakref.proxy(self),) - ) - self._timer.daemon = True - self._timer.start() - - # enable health check - if enable_health_check: # pragma: no cover - self._enable_health_check() - - self.__server_version = None - - return True - - def _enable_health_check(self): # pragma: no cover - """Place the status of the health check in the health response queue.""" - # lazy imports here to speed up module load - from grpc_health.v1 import health_pb2, health_pb2_grpc - - def _consume_responses(response_iterator, response_queue): - try: - for response in response_iterator: - response_queue.put(response) - # NOTE: We're doing absolutely nothing with this as - # this point since the server-side health check - # doesn't change state. - except Exception: - if self._exiting: - return - self._exited = True - raise MechanicalExitedError( - "Lost connection with the Mechanical gRPC server." - ) from None - - # enable health check - from queue import Queue - - request = health_pb2.HealthCheckRequest() - self._health_stub = health_pb2_grpc.HealthStub(self._channel) - rendezvous = self._health_stub.Watch(request) - - # health check feature implemented after 2023 R1 - try: - status = rendezvous.next() - except Exception as err: - if err.code().name != "UNIMPLEMENTED": - raise err - return - - if status.status != health_pb2.HealthCheckResponse.SERVING: - raise MechanicalRuntimeError( - "Cannot enable health check and/or connect to the Mechanical server." - ) - - self._health_response_queue = Queue() - - # allow main process to exit by setting daemon to true - thread = threading.Thread( - target=_consume_responses, - args=(rendezvous, self._health_response_queue), - daemon=True, - ) - thread.start() - - def _threaded_heartbeat(self): # pragma: no cover - """To call from a thread to verify that a Mechanical instance is alive.""" - self._initialised.set() - while True: - if self._exited: - break - try: - time.sleep(self._t_delay) - if not self.is_alive: - break - except ReferenceError: - break - # except Exception: - # continue - - def _create_channel(self, ip, port): - """Create an unsecured gRPC channel.""" - check_valid_ip(ip) - - # open the channel - channel_str = f"{ip}:{port}" - LOG.debug(f"Opening insecure channel at {channel_str}.") - return grpc.insecure_channel( - channel_str, - options=[ - ("grpc.max_receive_message_length", MAX_MESSAGE_LENGTH), - ], - ) - - @property - def is_alive(self) -> bool: - """Whether there is an active connection to the Mechanical gRPC server.""" - if self._exited: - return False - - if self._busy: # pragma: no cover - return True - - try: # pragma: no cover - self._make_dummy_call() - return True - except grpc.RpcError: - return False - - @staticmethod - def set_log_level(loglevel): - """Set the log level. - - Parameters - ---------- - loglevel : str, int - Level of logging. Options are ``"DEBUG"``, ``"INFO"``, ``"WARNING"`` - and ``"ERROR"``. - - Examples - -------- - Set the log level to the ``"DEBUG"`` level. - - # >>> mechanical.set_log_level('DEBUG') - # - # Set the log level to info - # - # >>> mechanical.set_log_level('INFO') - # - # Set the log level to warning - # - # >>> mechanical.set_log_level('WARNING') - # - # Set the log level to error - # - # >>> mechanical.set_log_level('ERROR') - """ - if isinstance(loglevel, str): - loglevel = loglevel.upper() - setup_logger(loglevel=loglevel) - - def get_product_info(self): - """Get product information by running a script on the Mechanical gRPC server.""" - - def _get_jscript_product_info_command(): - return ( - 'ExtAPI.Application.ScriptByName("jscript").ExecuteCommand' - '("var productInfo = DS.Script.getProductInfo();returnFromScript(productInfo);")' - ) - - def _get_python_product_info_command(): - return ( - 'clr.AddReference("Ansys.Mechanical.Application")\n' - "Ansys.Mechanical.Application.ProductInfo.ProductInfoAsString" - ) - - try: - self._disable_logging = True - if int(self.version) >= 232: - script = _get_python_product_info_command() - else: - script = _get_jscript_product_info_command() - return self.run_python_script(script) - except grpc.RpcError: - raise - finally: - self._disable_logging = False - - @suppress_logging - def __repr__(self): - """Get the user-readable string form of the Mechanical instance.""" - try: - if self._exited: - return "Mechanical exited." - return self.get_product_info() - except grpc.RpcError: - return "Error getting product info." - - def launch(self, cleanup_on_exit=True): - """Launch Mechanical in batch or UI mode. - - Parameters - ---------- - cleanup_on_exit : bool, optional - Whether to exit Mechanical when Python exits. The default is ``True``. - When ``False``, Mechanical is not exited when the garbage for this - Mechanical instance is collected. - """ - if not self._local: - raise RuntimeError("Can only launch with a local instance of Mechanical.") - - # let us respect the current cleanup behavior - if self._cleanup_on_exit: - self.exit() - - exec_file = self._start_parm.get("exec_file", get_mechanical_path(allow_input=False)) - batch = self._start_parm.get("batch", True) - additional_switches = self._start_parm.get("additional_switches", None) - additional_envs = self._start_parm.get("additional_envs", None) - port = launch_grpc( - exec_file=exec_file, - batch=batch, - additional_switches=additional_switches, - additional_envs=additional_envs, - verbose=True, - ) - # update the new cleanup behavior - self._cleanup_on_exit = cleanup_on_exit - self._port = port - self._channel = self._create_channel(self._ip, port) - self._connect(port) - - self.log_info("Mechanical is ready to accept gRPC calls.") - - def wait_till_mechanical_is_ready(self, wait_time=-1): - """Wait until Mechanical is ready. - - Parameters - ---------- - wait_time : float, optional - Maximum allowable time in seconds for connecting to the Mechanical gRPC server. - """ - time_1 = datetime.datetime.now() - - sleep_time = 0.5 - if wait_time == -1: # pragma: no cover - self.log_info("Waiting for Mechanical to be ready...") - else: - self.log_info(f"Waiting for Mechanical to be ready. Maximum wait time: {wait_time}s") - - while not self.__isMechanicalReady(): - time_2 = datetime.datetime.now() - time_interval = time_2 - time_1 - time_interval_seconds = int(time_interval.total_seconds()) - - self.log_debug( - f"Mechanical is not ready. You've been waiting for {time_interval_seconds}." - ) - if self._timeout != -1: - if time_interval_seconds > wait_time: - self.log_debug( - f"Allowed wait time {wait_time}s. " - f"Waited so for {time_interval_seconds}s, " - f"before throwing the error." - ) - raise RuntimeError( - f"Couldn't connect to Mechanical. " f"Waited for {time_interval_seconds}s." - ) - - time.sleep(sleep_time) - - time_2 = datetime.datetime.now() - time_interval = time_2 - time_1 - time_interval_seconds = int(time_interval.total_seconds()) - - self.log_info(f"Mechanical is ready. It took {time_interval_seconds} seconds to verify.") - - def __isMechanicalReady(self): - """Whether the Mechanical gRPC server is ready. - - Returns - ------- - bool - ``True`` if Mechanical is ready, ``False`` otherwise. - """ - try: - script = "ExtAPI.DataModel.Project.ProductVersion" - self.run_python_script(script) - except grpc.RpcError as error: - self.log_debug(f"Mechanical is not ready. Error:{error}.") - return False - - return True - - @staticmethod - def convert_to_server_log_level(log_level): - """Convert the log level to the server log level. - - Parameters - ---------- - log_level : str - Level of logging. Options are ``"DEBUG"``, ``"INFO"``, ``"WARNING"``, - ``"ERROR"``, and ``"CRITICAL"``. - - Returns - ------- - Converted log level for the server. - """ - value = client_to_server_loglevel.get(log_level) - - if value is not None: - return value - - raise ValueError( - f"Log level {log_level} is invalid. Possible values are " - f"'DEBUG','INFO', 'WARNING', 'ERROR', and 'CRITICAL'." - ) - - def run_python_script( - self, script_block: str, enable_logging=False, log_level="WARNING", progress_interval=2000 - ): - """Run a Python script block inside Mechanical. - - It returns the string value of the last executed statement. If the value cannot be - returned as a string, it will return an empty string. - - Parameters - ---------- - script_block : str - Script block (one or more lines) to run. - enable_logging: bool, optional - Whether to enable logging. The default is ``False``. - log_level: str - Level of logging. The default is ``"WARNING"``. Options are ``"DEBUG"``, - ``"INFO"``, ``"WARNING"``, and ``"ERROR"``. - progress_interval: int, optional - Frequency in milliseconds for getting log messages from the server. - The default is ``2000``. - - Returns - ------- - str - Script result. - - Examples - -------- - Return a value from a simple calculation. - - >>> mechanical.run_python_script('2+3') - '5' - - Return a string value from Project object. - - >>> mechanical.run_python_script('ExtAPI.DataModel.Project.ProductVersion') - '2024 R2' - - Return an empty string, when you try to return the Project object. - - >>> mechanical.run_python_script('ExtAPI.DataModel.Project') - '' - - Return an empty string for assignments. - - >>> mechanical.run_python_script('version = ExtAPI.DataModel.Project.ProductVersion') - '' - - Return value from the last executed statement from a variable. - - >>> script=''' - addition = 2 + 3 - multiplication = 3 * 4 - multiplication - ''' - >>> mechanical.run_python_script(script) - '12' - - Return value from last executed statement from a function call. - - >>> script=''' - import math - math.pow(2,3) - ''' - >>> mechanical.run_python_script(script) - '8' - - Handle an error scenario. - - >>> script = 'hello_world()' - >>> import grpc - >>> try: - mechanical.run_python_script(script) - except grpc.RpcError as error: - print(error.details()) - name 'hello_world' is not defined - - """ - self.verify_valid_connection() - result_as_string = self.__call_run_python_script( - script_block, enable_logging, log_level, progress_interval - ) - return result_as_string - - def run_python_script_from_file( - self, file_path, enable_logging=False, log_level="WARNING", progress_interval=2000 - ): - """Run the contents a python file inside Mechanical. - - It returns the string value of the last executed statement. If the value cannot be - returned as a string, it will return an empty string. - - Parameters - ---------- - file_path : - Path for the Python file. - enable_logging: bool, optional - Whether to enable logging. The default is ``False``. - log_level: str - Level of logging. The default is ``"WARNING"``. Options are ``"DEBUG"``, - ``"INFO"``, ``"WARNING"``, and ``"ERROR"``. - progress_interval: int, optional - Frequency in milliseconds for getting log messages from the server. - The default is ``2000``. - - Returns - ------- - str - Script result. - - Examples - -------- - Return a value from a simple calculation. - - Contents of **simple.py** file - - 2+3 - - >>> mechanical.run_python_script_from_file('simple.py') - '5' - - Return a value from a simple function call. - - Contents of **test.py** file - - import math - - math.pow(2,3) - - >>> mechanical.run_python_script_from_file('test.py') - '8' - - """ - self.verify_valid_connection() - self.log_debug(f"run_python_script_from_file started") - script_code = Mechanical.__readfile(file_path) - self.log_debug(f"run_python_script_from_file started") - return self.run_python_script(script_code, enable_logging, log_level, progress_interval) - - def exit(self, force=False): - """Exit Mechanical. - - Parameters - ---------- - force : bool, optional - Whether to force Mechanical to exit. The default is ``False``, in which case - only Mechanical in UI mode asks for confirmation. This parameter overrides - any environment variables that may inhibit exiting Mechanical. - - Examples - -------- - Exit Mechanical. - - >>> mechanical.Exit(force=True) - - """ - if not force: - if not get_start_instance(): - self.log_info("Ignoring exit due to PYMECHANICAL_START_INSTANCE=False") - return - - # or building the gallery - if pymechanical.BUILDING_GALLERY: - self._log.info("Ignoring exit due to BUILDING_GALLERY=True") - return - - if self._exited: - return - - self.verify_valid_connection() - - self._exiting = True - - self.log_debug("In shutdown.") - request = mechanical_pb2.ShutdownRequest(force_exit=force) - self.log_debug("Shutting down...") - - self._busy = True - try: - self._stub.Shutdown(request) - except grpc._channel._InactiveRpcError as error: - self.log_warning("Mechanical exit failed: {str(error}.") - finally: - self._busy = False - - self._exited = True - self._stub = None - - if self._remote_instance is not None: # pragma: no cover - self.log_debug("PyPIM delete has started.") - try: - self._remote_instance.delete() - except Exception as error: - self.log_warning("Remote instance delete failed: {str(error}.") - self.log_debug("PyPIM delete has finished.") - - self._remote_instance = None - self._channel = None - else: - self.log_debug("No PyPIM cleanup is needed.") - - local_ports = pymechanical.LOCAL_PORTS - if self._local and self._port in local_ports: - local_ports.remove(self._port) - - self.log_info("Shutdown has finished.") - - @protect_grpc - def upload( - self, - file_name, - file_location_destination=None, - chunk_size=DEFAULT_FILE_CHUNK_SIZE, - progress_bar=True, - ): - """Upload a file to the Mechanical instance. - - Parameters - ---------- - file_name : str - Local file to upload. Only the file name is needed if the file - is relative to the current working directory. Otherwise, the full path - is needed. - file_location_destination : str, optional - File location on the Mechanical server to upload the file to. The default is - ``None``, in which case the project directory is used. - chunk_size : int, optional - Chunk size in bytes. The default is ``1048576``. - progress_bar : bool, optional - Whether to show a progress bar using ``tqdm``. The default is ``True``. - A progress bar is helpful for viewing upload progress. - - Returns - ------- - str - Base name of the uploaded file. - - Examples - -------- - Upload the ``hsec.x_t`` file with the progress bar not shown. - - >>> mechanical.upload('hsec.x_t', progress_bar=False) - """ - self.verify_valid_connection() - - if not os.path.isfile(file_name): - raise FileNotFoundError(f"Unable to locate filename {file_name}.") - - self._log.debug(f"Uploading file '{file_name}' to the Mechanical instance.") - - if file_location_destination is None: - file_location_destination = self.project_directory - - self._busy = True - try: - chunks_generator = self.get_file_chunks( - file_location_destination, - file_name, - chunk_size=chunk_size, - progress_bar=progress_bar, - ) - response = self._stub.UploadFile(chunks_generator) - self.log_debug(f"upload_file response is {response.is_ok}.") - finally: - self._busy = False - - if not response.is_ok: # pragma: no cover - raise IOError("File failed to upload.") - return os.path.basename(file_name) - - def get_file_chunks(self, file_location, file_name, chunk_size, progress_bar): - """Construct the file upload request for the server. - - Parameters - ---------- - file_location_destination : str, optional - Directory where the file to upload to the server is located. - file_name : str - Name of the file to upload. - chunk_size : int - Chunk size in bytes. - progress_bar : bool - Whether to show a progress bar using ``tqdm``. - """ - pbar = None - if progress_bar: - if not _HAS_TQDM: # pragma: no cover - raise ModuleNotFoundError( - f"To use the keyword argument 'progress_bar', you must have " - f"installed the 'tqdm' package. To avoid this message, you can " - f"set 'progress_bar=False'." - ) - - n_bytes = os.path.getsize(file_name) - - base_name = os.path.basename(file_name) - pbar = tqdm( - total=n_bytes, - desc=f"Uploading {base_name} to {self._channel_str}:{file_location}.", - unit="B", - unit_scale=True, - unit_divisor=1024, - ) - - with open(file_name, "rb") as f: - while True: - piece = f.read(chunk_size) - length = len(piece) - if length == 0: - if pbar is not None: - pbar.close() - return - - if pbar is not None: - pbar.update(length) - - chunk = mechanical_pb2.Chunk(payload=piece, size=length) - yield mechanical_pb2.FileUploadRequest( - file_name=os.path.basename(file_name), file_location=file_location, chunk=chunk - ) - - @property - def project_directory(self): - """Get the project directory for the currently connected Mechanical instance. - - Examples - -------- - Get the project directory of the connected Mechanical instance. - - >>> mechanical.project_directory - '/tmp/ANSYS.username.1/AnsysMech3F97/Project_Mech_Files/' - - """ - return self.run_python_script("ExtAPI.DataModel.Project.ProjectDirectory") - - def list_files(self): - """List the files in the working directory of Mechanical. - - Returns - ------- - list - List of files in the working directory of Mechanical. - - Examples - -------- - List the files in the working directory. - - >>> files = mechanical.list_files() - >>> for file in files: print(file) - """ - result = self.run_python_script( - "import pymechanical_helpers\npymechanical_helpers.GetAllProjectFiles(ExtAPI)" - ) - - files_out = result.splitlines() - if not files_out: # pragma: no cover - self.log_warning("No files listed") - return files_out - - def _get_files(self, files, recursive=False): - self_files = self.list_files() # to avoid calling it too much - - if isinstance(files, str): - if self._local: # pragma: no cover - # in local mode - if os.path.exists(files): - if not os.path.isabs(files): - list_files = [os.path.join(os.getcwd(), files)] - else: - # file exist - list_files = [files] - elif "*" in files: - # using filter - list_files = glob.glob(files, recursive=recursive) - if not list_files: - raise ValueError( - f"The `'files'` parameter ({files}) didn't match any file using " - f"glob expressions in the local client." - ) - else: - raise ValueError( - f"The files parameter ('{files}') does not match any file or pattern." - ) - else: # Remote or looking into Mechanical working directory - if files in self_files: - list_files = [files] - elif "*" in files: - # try filter on the list_files - if recursive: - self.log_warning( - "Because the 'recursive' keyword argument does not work with " - "remote instances, it is ignored." - ) - list_files = fnmatch.filter(self_files, files) - if not list_files: - raise ValueError( - f"The `'files'` parameter ({files}) didn't match any file using " - f"glob expressions in the remote server." - ) - else: - raise ValueError( - f"The `'files'` parameter ('{files}') does not match any file or pattern." - ) - - elif isinstance(files, (list, tuple)): - if not all([isinstance(each, str) for each in files]): - raise ValueError( - "The parameter `'files'` can be a list or tuple, but it " - "should only contain strings." - ) - list_files = files - else: - raise ValueError( - f"The `file` parameter type ({type(files)}) is not supported." - "Only strings, tuple of strings, or list of strings are allowed." - ) - - return list_files - - def download( - self, - files, - target_dir=None, - chunk_size=DEFAULT_CHUNK_SIZE, - progress_bar=None, - recursive=False, - ): # pragma: no cover - """Download files from the working directory of the Mechanical instance. - - It downloads them from the working directory to the target directory. It returns the list - of local file paths for the downloaded files. - - Parameters - ---------- - files : str, list[str], tuple(str) - One or more files on the Mechanical server to download. The files must be - in the same directory as the Mechanical instance. You can use the - :func:`Mechanical.list_files ` - function to list current files. Alternatively, you can specify *glob expressions* to - match file names. For example, you could use ``file*`` to match every file whose - name starts with ``file``. - target_dir: str - Default directory to copy the downloaded files to. The default is ``None`` and - current working directory will be used as target directory. - chunk_size : int, optional - Chunk size in bytes. The default is ``262144``. The value must be less than 4 MB. - progress_bar : bool, optional - Whether to show a progress bar using ``tqdm``. The default is ``None``, in - which case a progress bar is shown. A progress bar is helpful for viewing download - progress. - recursive : bool, optional - Whether to use recursion when using a glob pattern search. The default is ``False``. - - Returns - ------- - List[str] - List of local file paths. - - Notes - ----- - There are some considerations to keep in mind when using the ``download()`` method: - - * The glob pattern search does not search recursively in remote instances. - * In a remote instance, it is not possible to list or download files in a - location other than the Mechanical working directory. - * If you are connected to a local instance and provide a file path, downloading files - from a different folder is allowed but is not recommended. - - Examples - -------- - Download a single file. - - >>> local_file_path_list = mechanical.download('file.out') - - Download all files starting with ``file``. - - >>> local_file_path_list = mechanical.download('file*') - - Download every file in the Mechanical working directory. - - >>> local_file_path_list = mechanical.download('*.*') - - Alternatively, the recommended method is to use the - :func:`download_project() ` - method to download all files. - - >>> local_file_path_list = mechanical.download_project() - - """ - self.verify_valid_connection() - - if chunk_size > 4 * 1024 * 1024: # 4MB - raise ValueError( - "Chunk sizes bigger than 4 MB can generate unstable behaviour in PyMechanical. " - "Decrease the ``chunk_size`` value." - ) - - list_files = self._get_files(files, recursive=recursive) - - if target_dir: - path = pathlib.Path(target_dir) - path.mkdir(parents=True, exist_ok=True) - else: - target_dir = os.getcwd() - - out_files = [] - - for each_file in list_files: - try: - file_name = os.path.basename(each_file) # Getting only the name of the file. - # We try to avoid that when the full path is supplied. It crashes when trying - # to do `os.path.join(target_dir"os.getcwd()", file_name "full filename path"` - # This produces the file structure to flat out, but it is fine, - # because recursive does not work in remote. - self._busy = True - out_file_path = self._download( - each_file, - out_file_name=os.path.join(target_dir, file_name), - chunk_size=chunk_size, - progress_bar=progress_bar, - ) - out_files.append(out_file_path) - except FileNotFoundError: - # So far the gRPC interface returns the size of the file equal - # zero, if the file does not exist, or if its size is zero, - # but they are two different things. - # In theory, since we are obtaining the files name from - # `mechanical.list_files()`, they do exist, so - # if there is any error, it means their size is zero. - pass # This is not the best. - finally: - self._busy = False - - return out_files - - @protect_grpc - def _download( - self, - target_name, - out_file_name, - chunk_size=DEFAULT_CHUNK_SIZE, - progress_bar=None, - ): - """Download a file from the Mechanical instance. - - Parameters - ---------- - target_name : str - Name of the target file on the server. The file must be in the same - directory as the Mechanical instance. You can use the - ``mechanical.list_files()`` function to list current files. - out_file_name : str - Name of the output file if the name is to differ from that for the target - file. - chunk_size : int, optional - Chunk size in bytes. The default is ``"DEFAULT_CHUNK_SIZE"``, in which case - 256 kB is used. The value must be less than 4 MB. - progress_bar : bool, optional - Whether to show a progress bar using ``tqdm``. The default is ``None``, in - which case a progress bar is shown. A progress bar is helpful for showing download - progress. - - Examples - -------- - Download the remote result file "file.rst" as "my_result.rst". - - >>> mechanical.download('file.rst', 'my_result.rst') - """ - self.verify_valid_connection() - - if not progress_bar and _HAS_TQDM: - progress_bar = True - - request = mechanical_pb2.FileDownloadRequest(file_path=target_name, chunk_size=chunk_size) - - responses = self._stub.DownloadFile(request) - - file_size = self.save_chunks_to_file( - responses, out_file_name, progress_bar=progress_bar, target_name=target_name - ) - - if not file_size: # pragma: no cover - raise FileNotFoundError(f'File "{out_file_name}" is empty or does not exist') - - self.log_info(f"{out_file_name} with size {file_size} has been written.") - - return out_file_name - - def save_chunks_to_file(self, responses, filename, progress_bar=False, target_name=""): - """Save chunks to a local file. - - Parameters - ---------- - responses : - filename : str - Name of the local file to save chunks to. - progress_bar : bool, optional - Whether to show a progress bar using ``tqdm``. The default is ``False``. - target_name : str, optional - Name of the target file on the server. The default is ``""``. The file - must be in the same directory as the Mechanical instance. You can use the - ``mechanical.list_files()`` function to list current files. - - Returns - ------- - file_size : int - File size saved in bytes. If ``0`` is returned, no file was written. - """ - pbar = None - if progress_bar: - if not _HAS_TQDM: # pragma: no cover - raise ModuleNotFoundError( - "To use the keyword argument 'progress_bar', you need to have installed " - "the 'tqdm' package.To avoid this message you can set 'progress_bar=False'." - ) - - file_size = 0 - with open(filename, "wb") as f: - for response in responses: - f.write(response.chunk.payload) - payload_size = len(response.chunk.payload) - file_size += payload_size - if pbar is None: - pbar = tqdm( - total=response.file_size, - desc=f"Downloading {self._channel_str}:{target_name} to {filename}", - unit="B", - unit_scale=True, - unit_divisor=1024, - ) - pbar.update(payload_size) - else: - pbar.update(payload_size) - - if pbar is not None: - pbar.close() - - return file_size - - def download_project(self, extensions=None, target_dir=None, progress_bar=False): - """Download all project files in the working directory of the Mechanical instance. - - It downloads them from the working directory to the target directory. It returns the list - of local file paths for the downloaded files. - - Parameters - ---------- - extensions : list[str], tuple[str], optional - List of extensions for filtering files before downloading them. The - default is ``None``. - target_dir : str, optional - Path for downloading the files to. The default is ``None``. - progress_bar : bool, optional - Whether to show a progress bar using ``tqdm``. The default is ``False``. - A progress bar is helpful for viewing download progress. - - Returns - ------- - List[str] - List of local file paths. - - Examples - -------- - Download all the files in the project. - - >>> local_file_path_list = mechanical.download_project() - """ - destination_directory = target_dir.rstrip("\\/") - - # let us create the directory, if it doesn't exist - if destination_directory: - path = pathlib.Path(destination_directory) - path.mkdir(parents=True, exist_ok=True) - else: - destination_directory = os.getcwd() - - # relative directory? - if os.path.isdir(destination_directory): - if not os.path.isabs(destination_directory): - # construct full path - destination_directory = os.path.join(os.getcwd(), destination_directory) - - project_directory = self.project_directory - # remove the trailing slash - server could be windows or linux - project_directory = project_directory.rstrip("\\/") - - # this is where .mechddb resides - parent_directory = os.path.dirname(project_directory) - - list_of_files = [] - - if not extensions: - files = self.list_files() - else: - files = [] - for each_extension in extensions: - # mechdb resides one level above project directory - if "mechdb" == each_extension.lower(): - file_temp = os.path.join(parent_directory, f"*.{each_extension}") - else: - file_temp = os.path.join(project_directory, "**", f"*.{each_extension}") - - if self._local: - list_files_expanded = self._get_files(file_temp, recursive=True) - - if "mechdb" == each_extension.lower(): - # if we have more than one .mechdb in the parent folder - # filter to have only the current mechdb - self_files = self.list_files() - filtered_files = [] - for temp_file in list_files_expanded: - if temp_file in self_files: - filtered_files.append(temp_file) - list_files = filtered_files - else: - list_files = list_files_expanded - else: - list_files = self._get_files(file_temp, recursive=False) - - files.extend(list_files) - - for file in files: - # create similar hierarchy locally - new_path = file.replace(parent_directory, destination_directory) - new_path_dir = os.path.dirname(new_path) - temp_files = self.download( - files=file, target_dir=new_path_dir, progress_bar=progress_bar - ) - list_of_files.extend(temp_files) - - return list_of_files - - def clear(self): - """Clear the database. - - Examples - -------- - Clear the database. - - >>> mechanical.clear() - - """ - self.run_python_script("ExtAPI.DataModel.Project.New()") - - def _make_dummy_call(self): - try: - self._disable_logging = True - self.run_python_script("ExtAPI.DataModel.Project.ProjectDirectory") - except grpc.RpcError: # pragma: no cover - raise - finally: - self._disable_logging = False - - @staticmethod - def __readfile(file_path): - """Get the contents of the file as a string.""" - # open text file in read mode - text_file = open(file_path, "r", encoding="utf-8") - # read whole file to a string - data = text_file.read() - # close file - text_file.close() - - return data - - def __call_run_python_script( - self, script_code: str, enable_logging, log_level, progress_interval - ): - """Run the Python script block on the server. - - Parameters - ---------- - script_block : str - Script block (one or more lines) to run. - enable_logging: bool - Whether to enable logging - log_level: str - Level of logging. Options are ``"DEBUG"``, ``"INFO"``, ``"WARNING"``, - and ``"ERROR"``. - timeout: int, optional - Frequency in milliseconds for getting log messages from the server. - - Returns - ------- - str - Script result. - - """ - log_level_server = self.convert_to_server_log_level(log_level) - request = mechanical_pb2.RunScriptRequest() - request.script_code = script_code - request.enable_logging = enable_logging - request.logger_severity = log_level_server - request.progress_interval = progress_interval - - result = "" - self._busy = True - - try: - for runscript_response in self._stub.RunPythonScript(request): - if runscript_response.log_info == "__done__": - result = runscript_response.script_result - break - else: - if enable_logging: - self.log_message(log_level, runscript_response.log_info) - except grpc.RpcError as error: - error_info = error.details() - error_info_lower = error_info.lower() - # For the given script, return value cannot be converted to string. - if ( - "the expected result" in error_info_lower - and "cannot be return via this API." in error_info - ): - if enable_logging: - self.log_debug(f"Ignoring the conversion error.{error_info}") - result = "" - else: - raise - finally: - self._busy = False - - self._log_mechanical_script(script_code) - - return result - - def log_message(self, log_level, message): - """Log the message using the given log level. - - Parameters - ---------- - log_level: str - Level of logging. Options are ``"DEBUG"``, ``"INFO"``, ``"WARNING"``, - and ``"ERROR"``. - message : str - Message to log. - - Examples - -------- - Log a debug message. - - >>> mechanical.log_message('DEBUG', 'debug message') - - Log an info message. - - >>> mechanical.log_message('INFO', 'info message') - - """ - if log_level == "DEBUG": - self.log_debug(message) - elif log_level == "INFO": - self.log_info(message) - elif log_level == "WARNING": - self.log_warning(message) - elif log_level == "ERROR": - self.log_error(message) - - def log_debug(self, message): - """Log the debug message.""" - if self._disable_logging: - return - self._log.debug(message) - - def log_info(self, message): - """Log the info message.""" - if self._disable_logging: - return - self._log.info(message) - - def log_warning(self, message): - """Log the warning message.""" - if self._disable_logging: - return - self._log.warning(message) - - def log_error(self, message): - """Log the error message.""" - if self._disable_logging: - return - self._log.error(message) - - def verify_valid_connection(self): - """Verify whether the connection to Mechanical is valid.""" - if self._exited: - raise MechanicalExitedError("Mechanical has already exited.") - - if self._stub is None: # pragma: no cover - raise ValueError( - "There is not a valid connection to Mechanical. Launch or connect to it first." - ) - - @property - def exited(self): - """Whether Mechanical already exited.""" - return self._exited - - def _log_mechanical_script(self, script_code): - if self._disable_logging: - return - - if self._log_file_mechanical: - try: - with open(self._log_file_mechanical, "a", encoding="utf-8") as file: - file.write(script_code) - file.write("\n") - except IOError as e: # pragma: no cover - self.log_warning(f"I/O error({e.errno}): {e.strerror}") - except Exception as e: # pragma: no cover - self.log_warning("Unexpected error:" + str(e)) - - -def get_start_instance(start_instance_default=True): - """Check if the ``PYMECHANICAL_START_INSTANCE`` environment variable exists and is valid. - - Parameters - ---------- - start_instance_default : bool, optional - Value to return when ``PYMECHANICAL_START_INSTANCE`` is unset. - - Returns - ------- - bool - ``True`` when the ``PYMECHANICAL_START_INSTANCE`` environment variable exists - and is valid, ``False`` when this environment variable does not exist or is not valid. - If it is unset, ``start_instance_default`` is returned. - - Raises - ------ - OSError - Raised when ``PYMECHANICAL_START_INSTANCE`` is not either ``True`` or ``False`` - (case independent). - - """ - if "PYMECHANICAL_START_INSTANCE" in os.environ: - if os.environ["PYMECHANICAL_START_INSTANCE"].lower() not in [ - "true", - "false", - ]: # pragma: no cover - val = os.environ["PYMECHANICAL_START_INSTANCE"] - raise OSError( - f'Invalid value "{val}" for PYMECHANICAL_START_INSTANCE\n' - 'PYMECHANICAL_START_INSTANCE should be either "TRUE" or "FALSE"' - ) - return os.environ["PYMECHANICAL_START_INSTANCE"].lower() == "true" - return start_instance_default - - -def launch_grpc( - exec_file="", - batch=True, - port=MECHANICAL_DEFAULT_PORT, - additional_switches=None, - additional_envs=None, - verbose=False, -) -> int: - """Start Mechanical locally in gRPC mode. - - Parameters - ---------- - exec_file : str, optional - Path for the Mechanical executable file. The default is ``None``, in which - case the cached location is used. - batch : bool, optional - Whether to launch Mechanical in batch mode. The default is ``True``. - When ``False``, Mechanical is launched in UI mode. - port : int, optional - Port to launch the Mechanical instance on. The default is - ``MECHANICAL_DEFAULT_PORT``. The final port is the first - port available after (or including) this port. - additional_switches : list, optional - List of additional arguments to pass. The default is ``None``. - additional_envs : dictionary, optional - Dictionary of additional environment variables to pass. The default - is ``None``. - verbose : bool, optional - Whether to print all output when launching and running Mechanical. The - default is ``False``. Printing all output is not recommended unless - you are debugging the startup of Mechanical. - - Returns - ------- - int - Port number that the Mechanical instance started on. - - Notes - ----- - If ``PYMECHANICAL_START_INSTANCE`` is set to FALSE, the ``launch_mechanical`` - method looks for an existing instance of Mechanical at ``PYMECHANICAL_IP`` on port - ``PYMECHANICAL_PORT``, with default to ``127.0.0.1`` and ``10000`` if unset. - This is typically used for automated documentation and testing. - - Examples - -------- - Launch Mechanical using the default configuration. - - >>> from ansys.mechanical.core import launch_mechanical - >>> mechanical = launch_mechanical() - - Launch Mechanical using a specified executable file. - - >>> exec_file_path = 'C:/Program Files/ANSYS Inc/v242/aisol/bin/win64/AnsysWBU.exe' - >>> mechanical = launch_mechanical(exec_file_path) - - """ - # verify version - if atp.version_from_path("mechanical", exec_file) < 232: - raise VersionError("The Mechanical gRPC interface requires Mechanical 2023 R2 or later.") - - # get the next available port - local_ports = pymechanical.LOCAL_PORTS - if port is None: - if not local_ports: - port = MECHANICAL_DEFAULT_PORT - else: - port = max(local_ports) + 1 - - while port_in_use(port) or port in local_ports: - port += 1 - local_ports.append(port) - - mechanical_launcher = MechanicalLauncher( - batch, port, exec_file, additional_switches, additional_envs, verbose - ) - mechanical_launcher.launch() - - return port - - -def launch_remote_mechanical(version=None) -> (grpc.Channel, Instance): # pragma: no cover - """Start Mechanical remotely using the Product Instance Management (PIM) API. - - When calling this method, you must ensure that you are in an environment - where PyPIM is configured. You can use the - :func:`pypim.is_configured ` - method to verify that PyPIM is configured. - - Parameters - ---------- - version : str, optional - Mechanical version to run in the three-digit format. For example, ``"242"`` to - run 2024 R2. The default is ``None``, in which case the server runs the latest - installed version. - - Returns - ------- - Tuple containing channel, remote_instance. - """ - pim = pypim.connect() - instance = pim.create_instance(product_name="mechanical", product_version=version) - - LOG.info("PyPIM wait for ready has started.") - instance.wait_for_ready() - LOG.info("PyPIM wait for ready has finished.") - - channel = instance.build_grpc_channel( - options=[ - ("grpc.max_receive_message_length", MAX_MESSAGE_LENGTH), - ] - ) - - return channel, instance - - -def launch_mechanical( - allow_input=True, - exec_file=None, - batch=True, - loglevel="ERROR", - log_file=False, - log_mechanical=None, - additional_switches=None, - additional_envs=None, - start_timeout=120, - port=None, - ip=None, - start_instance=None, - verbose_mechanical=False, - clear_on_connect=False, - cleanup_on_exit=True, - version=None, - keep_connection_alive=True, -) -> Mechanical: - """Start Mechanical locally. - - Parameters - ---------- - allow_input: bool, optional - Whether to allow user input when discovering the path to the Mechanical - executable file. - exec_file : str, optional - Path for the Mechanical executable file. The default is ``None``, - in which case the cached location is used. If PyPIM is configured - and this parameter is set to ``None``, PyPIM launches Mechanical - using its ``version`` parameter. - batch : bool, optional - Whether to launch Mechanical in batch mode. The default is ``True``. - When ``False``, Mechanical launches in UI mode. - loglevel : str, optional - Level of messages to print to the console. - Options are: - - - ``"WARNING"``: Prints only Ansys warning messages. - - ``"ERROR"``: Prints only Ansys error messages. - - ``"INFO"``: Prints all Ansys messages. - - The default is ``WARNING``. - log_file : bool, optional - Whether to copy the messages to a file named ``logs.log``, which is - located where the Python script is executed. The default is ``False``. - log_mechanical : str, optional - Path to the output file on the local disk to write every script - command to. The default is ``None``. However, you might set - ``"log_mechanical='pymechanical_log.txt'"`` to write all commands that are - sent to Mechanical via PyMechanical to this file. You can then use these - commands to run a script within Mechanical without PyMechanical. - additional_switches : list, optional - Additional switches for Mechanical. The default is ``None``. - additional_envs : dictionary, optional - Dictionary of additional environment variables to pass. The default - is ``None``. - start_timeout : float, optional - Maximum allowable time in seconds to connect to the Mechanical server. - The default is ``120``. - port : int, optional - Port to launch the Mechanical gRPC server on. The default is ``None``, - in which case ``10000`` is used. The final port is the first - port available after (or including) this port. You can override the - default behavior of this parameter with the - ``PYMECHANICAL_PORT=`` environment variable. - ip : str, optional - IP address to use only when ``start_instance`` is ``False``. The - default is ``None``, in which case ``"127.0.0.1"`` is used. If you - provide an IP address, ``start_instance`` is set to ``False``. - A host name can be provided as an alternative to an IP address. - start_instance : bool, optional - Whether to launch and connect to a new Mechanical instance. The default - is ``None``, in which case an attempt is made to connect to an existing - Mechanical instance at the given ``ip`` and ``port`` parameters, which have - defaults of ``"127.0.0.1"`` and ``10000`` respectively. When ``True``, - a local instance of Mechanical is launched. You can override the default - behavior of this parameter with the ``PYMECHANICAL_START_INSTANCE=FALSE`` - environment variable. - verbose_mechanical : bool, optional - Whether to enable printing of all output when launching and running - a Mechanical instance. The default is ``False``. This parameter should be - set to ``True`` for debugging only as output can be tracked within - PyMechanical. - clear_on_connect : bool, optional - When ``start_instance`` is ``False``, whether to clear the environment - when connecting to Mechanical. The default is ``False``. When ``True``, - a fresh environment is provided when you connect to Mechanical. - cleanup_on_exit : bool, optional - Whether to exit Mechanical when Python exits. The default is ``True``. - When ``False``, Mechanical is not exited when the garbage for this Mechanical - instance is collected. - version : str, optional - Mechanical version to run in the three-digit format. For example, ``"242"`` - for 2024 R2. The default is ``None``, in which case the server runs the - latest installed version. If PyPIM is configured and ``exce_file=None``, - PyPIM launches Mechanical using its ``version`` parameter. - keep_connection_alive : bool, optional - Whether to keep the gRPC connection alive by running a background thread - and making dummy calls for remote connections. The default is ``True``. - - Returns - ------- - ansys.mechanical.core.mechanical.Mechanical - Instance of Mechanical. - - Notes - ----- - If the environment is configured to use `PyPIM `_ - and ``start_instance=True``, then starting the instance is delegated to PyPIM. - In this case, most of the preceding parameters are ignored because the server-side - configuration is used. - - Examples - -------- - Launch Mechanical. - - >>> from ansys.mechanical.core import launch_mechanical - >>> mech = launch_mechanical() - - Launch Mechanical using a specified executable file. - - >>> exec_file_path = 'C:/Program Files/ANSYS Inc/v242/aisol/bin/win64/AnsysWBU.exe' - >>> mech = launch_mechanical(exec_file_path) - - Connect to an existing Mechanical instance at IP address ``192.168.1.30`` on port - ``50001``. - - >>> mech = launch_mechanical(start_instance=False, ip='192.168.1.30', port=50001) - """ - # Start Mechanical with PyPIM if the environment is configured for it - # and a directive on how to launch Mechanical was not passed. - if pypim.is_configured() and exec_file is None: # pragma: no cover - LOG.info("Starting Mechanical remotely. The startup configuration will be ignored.") - channel, remote_instance = launch_remote_mechanical(version=version) - return Mechanical( - channel=channel, - remote_instance=remote_instance, - loglevel=loglevel, - log_file=log_file, - log_mechanical=log_mechanical, - timeout=start_timeout, - cleanup_on_exit=cleanup_on_exit, - keep_connection_alive=keep_connection_alive, - ) - - if ip is None: - ip = os.environ.get("PYMECHANICAL_IP", LOCALHOST) - else: # pragma: no cover - start_instance = False - ip = socket.gethostbyname(ip) # Converting ip or host name to ip - - check_valid_ip(ip) # double check - - if port is None: - port = int(os.environ.get("PYMECHANICAL_PORT", MECHANICAL_DEFAULT_PORT)) - check_valid_port(port) - - # connect to an existing instance if enabled - if start_instance is None: - start_instance = check_valid_start_instance( - os.environ.get("PYMECHANICAL_START_INSTANCE", True) - ) - - # special handling when building the gallery outside of CI. This - # creates an instance of Mechanical the first time if PYMECHANICAL_START_INSTANCE - # is False. - # when you launch, treat it as local. - # when you connect, treat it as remote. We cannot differentiate between - # local vs container scenarios. In the container scenarios, we could be connecting - # to a container using local ip and port - if pymechanical.BUILDING_GALLERY: # pragma: no cover - # launch an instance of PyMechanical if it does not already exist and - # starting instances is allowed - if start_instance and GALLERY_INSTANCE[0] is None: - mechanical = launch_mechanical( - start_instance=True, - cleanup_on_exit=False, - loglevel=loglevel, - ) - GALLERY_INSTANCE[0] = {"ip": mechanical._ip, "port": mechanical._port} - return mechanical - - # otherwise, connect to the existing gallery instance if available - elif GALLERY_INSTANCE[0] is not None: - mechanical = Mechanical( - ip=GALLERY_INSTANCE[0]["ip"], - port=GALLERY_INSTANCE[0]["port"], - cleanup_on_exit=False, - loglevel=loglevel, - local=False, - ) - # we are connecting to the existing gallery instance, - # we need to clear Mechanical. - mechanical.clear() - - return mechanical - - # finally, if running on CI/CD, connect to the default instance - else: - mechanical = Mechanical( - ip=ip, port=port, cleanup_on_exit=False, loglevel=loglevel, local=False - ) - # we are connecting for gallery generation, - # we need to clear Mechanical. - mechanical.clear() - return mechanical - - if not start_instance: - mechanical = Mechanical( - ip=ip, - port=port, - loglevel=loglevel, - log_file=log_file, - log_mechanical=log_mechanical, - timeout=start_timeout, - cleanup_on_exit=cleanup_on_exit, - keep_connection_alive=keep_connection_alive, - local=False, - ) - if clear_on_connect: - mechanical.clear() - - # setting ip for the grpc server - if ip != LOCALHOST: # Default local ip is 127.0.0.1 - create_ip_file(ip, os.getcwd()) - - return mechanical - - # verify executable - if exec_file is None: - exec_file = get_mechanical_path(allow_input) - if exec_file is None: # pragma: no cover - raise FileNotFoundError( - "Path to the Mechanical executable file is invalid or cache cannot be loaded. " - "Enter a path manually by specifying a value for the " - "'exec_file' parameter." - ) - else: # verify ansys exists at this location - if not os.path.isfile(exec_file): - raise FileNotFoundError( - f'This path for the Mechanical executable is invalid: "{exec_file}"\n' - "Enter a path manually by specifying a value for the " - "'exec_file' parameter." - ) - - start_parm = { - "exec_file": exec_file, - "batch": batch, - "additional_switches": additional_switches, - "additional_envs": additional_envs, - } - - try: - port = launch_grpc(port=port, verbose=verbose_mechanical, **start_parm) - start_parm["local"] = True - mechanical = Mechanical( - ip=ip, - port=port, - loglevel=loglevel, - log_file=log_file, - log_mechanical=log_mechanical, - timeout=start_timeout, - cleanup_on_exit=cleanup_on_exit, - keep_connection_alive=keep_connection_alive, - **start_parm, - ) - except Exception as exception: # pragma: no cover - # pass - raise exception - - return mechanical - - -def connect_to_mechanical( - ip=None, - port=None, - loglevel="ERROR", - log_file=False, - log_mechanical=None, - connect_timeout=120, - clear_on_connect=False, - cleanup_on_exit=False, - keep_connection_alive=True, -) -> Mechanical: - """Connect to an existing Mechanical server instance. - - Parameters - ---------- - ip : str, optional - IP address for connecting to an existing Mechanical instance. The - IP address defaults to ``"127.0.0.1"``. - port : int, optional - Port to listen on for an existing Mechanical instance. The default is ``None``, - in which case ``10000`` is used. You can override the - default behavior of this parameter with the - ``PYMECHANICAL_PORT=`` environment variable. - loglevel : str, optional - Level of messages to print to the console. - Options are: - - - ``"WARNING"``: Prints only Ansys warning messages. - - ``"ERROR"``: Prints only Ansys error messages. - - ``"INFO"``: Prints all Ansys messages. - - The default is ``WARNING``. - log_file : bool, optional - Whether to copy the messages to a file named ``logs.log``, which is - located where the Python script is executed. The default is ``False``. - log_mechanical : str, optional - Path to the output file on the local disk to write every script - command to. The default is ``None``. However, you might set - ``"log_mechanical='pymechanical_log.txt'"`` to write all commands that are - sent to Mechanical via PyMechanical to this file. You can then use these - commands to run a script within Mechanical without PyMechanical. - connect_timeout : float, optional - Maximum allowable time in seconds to connect to the Mechanical server. - The default is ``120``. - clear_on_connect : bool, optional - Whether to clear the Mechanical instance when connecting. The default is ``False``. - When ``True``, a fresh environment is provided when you connect to Mechanical. - cleanup_on_exit : bool, optional - Whether to exit Mechanical when Python exits. The default is ``False``. - When ``False``, Mechanical is not exited when the garbage for this Mechanical - instance is collected. - keep_connection_alive : bool, optional - Whether to keep the gRPC connection alive by running a background thread - and making dummy calls for remote connections. The default is ``True``. - - Returns - ------- - ansys.mechanical.core.mechanical.Mechanical - Instance of Mechanical. - - Examples - -------- - Connect to an existing Mechanical instance at IP address ``192.168.1.30`` on port - ``50001``.. - - - >>> from ansys.mechanical.core import connect_to_mechanical - >>> pymech = connect_to_mechanical(ip='192.168.1.30', port=50001) - """ - return launch_mechanical( - start_instance=False, - loglevel=loglevel, - log_file=log_file, - log_mechanical=log_mechanical, - start_timeout=connect_timeout, - port=port, - ip=ip, - clear_on_connect=clear_on_connect, - cleanup_on_exit=cleanup_on_exit, - keep_connection_alive=keep_connection_alive, - ) +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Connect to Mechanical gRPC server and issues commands.""" +import atexit +from contextlib import closing +import datetime +import fnmatch +from functools import wraps +import glob +import os +import pathlib +import socket +import threading +import time +import weakref + +import ansys.api.mechanical.v0.mechanical_pb2 as mechanical_pb2 +import ansys.api.mechanical.v0.mechanical_pb2_grpc as mechanical_pb2_grpc +import ansys.platform.instancemanagement as pypim +from ansys.platform.instancemanagement import Instance +import ansys.tools.path as atp +import grpc + +import ansys.mechanical.core as pymechanical +from ansys.mechanical.core import LOG +from ansys.mechanical.core.errors import ( + MechanicalExitedError, + MechanicalRuntimeError, + VersionError, + protect_grpc, +) +from ansys.mechanical.core.launcher import MechanicalLauncher +from ansys.mechanical.core.misc import ( + check_valid_ip, + check_valid_port, + check_valid_start_instance, + threaded, +) + +# Checking if tqdm is installed. +# If it is, the default value for progress_bar is true. +try: + from tqdm import tqdm + + _HAS_TQDM = True + """Whether or not tqdm is installed.""" +except ModuleNotFoundError: # pragma: no cover + _HAS_TQDM = False + +# Default 256 MB message length +MAX_MESSAGE_LENGTH = int(os.environ.get("PYMECHANICAL_MAX_MESSAGE_LENGTH", 256 * 1024**2)) +"""Default message length.""" + +# Chunk sizes for streaming and file streaming +DEFAULT_CHUNK_SIZE = 256 * 1024 # 256 kB +"""Default chunk size.""" +DEFAULT_FILE_CHUNK_SIZE = 1024 * 1024 # 1MB +"""Default file chunk size.""" + + +def setup_logger(loglevel="INFO", log_file=True, mechanical_instance=None): + """Initialize the logger for the given mechanical instance.""" + # Return existing log if this function has already been called + if hasattr(setup_logger, "log"): + return setup_logger.log + else: + setup_logger.log = LOG.add_instance_logger("Mechanical", mechanical_instance) + + setup_logger.log.setLevel(loglevel) + + if log_file: + if isinstance(log_file, str): + setup_logger.log.log_to_file(filename=log_file, level=loglevel) + + return setup_logger.log + + +def suppress_logging(func): + """Decorate a function to suppress the logging for a Mechanical instance.""" + + @wraps(func) + def wrapper(*args, **kwargs): + mechanical = args[0] + prior_log_level = mechanical.log.level + if prior_log_level != "CRITICAL": + mechanical.set_log_level("CRITICAL") + + out = func(*args, **kwargs) + + if prior_log_level != "CRITICAL": + mechanical.set_log_level(prior_log_level) + + return out + + return wrapper + + +LOCALHOST = "127.0.0.1" +"""Localhost address.""" + +MECHANICAL_DEFAULT_PORT = 10000 +"""Default Mechanical port.""" + +GALLERY_INSTANCE = [None] +"""List of gallery instances.""" + + +def _cleanup_gallery_instance(): # pragma: no cover + """Clean up any leftover instances of Mechanical from building the gallery.""" + if GALLERY_INSTANCE[0] is not None: + mechanical = Mechanical( + ip=GALLERY_INSTANCE[0]["ip"], + port=GALLERY_INSTANCE[0]["port"], + ) + mechanical.exit(force=True) + + +atexit.register(_cleanup_gallery_instance) + + +def port_in_use(port, host=LOCALHOST): + """Check whether a port is in use at the given host. + + You must actually *bind* the address. Just checking if you can create + a socket is insufficient because it is possible to run into permission + errors like:: + + An attempt was made to access a socket in a way forbidden by its + access permissions. + """ + with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: + if sock.connect_ex((host, port)) == 0: + return True + else: + return False + + +def check_ports(port_range, ip="localhost"): + """Check the state of ports in a port range.""" + ports = {} + for port in port_range: + ports[port] = port_in_use(port, ip) + return ports + + +def close_all_local_instances(port_range=None, use_thread=True): + """Close all Mechanical instances within a port range. + + You can use this method when cleaning up from a failed pool or + batch run. + + Parameters + ---------- + port_range : list, optional + List of a range of ports to use when cleaning up Mechanical. The + default is ``None``, in which case the ports managed by + PyMechanical are used. + + use_thread : bool, optional + Whether to use threads to close the Mechanical instances. + The default is ``True``. So this call will return immediately. + + Examples + -------- + Close all Mechanical instances connected on local ports. + + >>> import ansys.mechanical.core as pymechanical + >>> pymechanical.close_all_local_instances() + + """ + if port_range is None: + port_range = pymechanical.LOCAL_PORTS + + @threaded + def close_mechanical_threaded(port, name="Closing Mechanical instance in a thread"): + close_mechanical(port, name) + + def close_mechanical(port, name="Closing Mechanical instance"): + try: + mechanical = Mechanical(port=port) + LOG.debug(f"{name}: {mechanical.name}.") + mechanical.exit(force=True) + except OSError: # pragma: no cover + pass + + ports = check_ports(port_range) + for port_temp, state in ports.items(): + if state: + if use_thread: + close_mechanical_threaded(port_temp) + else: + close_mechanical(port_temp) + + +def create_ip_file(ip, path): + """Create the ``mylocal.ip`` file needed to change the IP address of the gRPC server.""" + file_name = os.path.join(path, "mylocal.ip") + with open(file_name, "w", encoding="utf-8") as f: + f.write(ip) + + +def get_mechanical_path(allow_input=True): + """Get path. + + Deprecated - use `ansys.tools.path.get_mechanical_path` instead + """ + return atp.get_mechanical_path(allow_input) + + +def check_valid_mechanical(): + """Change to see if the default Mechanical path is valid. + + Example (windows) + ----------------- + + >>> from ansys.mechanical.core import mechanical + >>> from ansys.tools.path import change_default_mechanical_path + >>> mechanical_path = 'C:/Program Files/ANSYS Inc/v251/aisol/bin/win64/AnsysWBU.exe' + >>> change_default_mechanical_path(mechanical_pth) + >>> mechanical.check_valid_mechanical() + True + + + """ + mechanical_path = atp.get_mechanical_path(False) + if mechanical_path is None: + return False + mechanical_version = atp.version_from_path("mechanical", mechanical_path) + return not (mechanical_version < 232 and os.name != "posix") + + +def change_default_mechanical_path(exe_loc): + """Change default path. + + Deprecated - use `ansys.tools.path.change_default_mechanical_path` instead. + """ + return atp.change_default_mechanical_path(exe_loc) + + +def save_mechanical_path(exe_loc=None): # pragma: no cover + """Save path. + + Deprecated - use `ansys.tools.path.save_mechanical_path` instead. + """ + return atp.save_mechanical_path(exe_loc) + + +client_to_server_loglevel = { + "DEBUG": 1, + "INFO": 2, + "WARN": 3, + "WARNING": 3, + "ERROR": 4, + "CRITICAL": 5, +} + + +class Mechanical(object): + """Connects to a gRPC Mechanical server and allows commands to be passed.""" + + # Required by `_name` method to be defined before __init__ be + _ip = None + _port = None + + def __init__( + self, + ip=None, + port=None, + timeout=60.0, + loglevel="WARNING", + log_file=False, + log_mechanical=None, + cleanup_on_exit=False, + channel=None, + remote_instance=None, + keep_connection_alive=True, + **kwargs, + ): + """Initialize the member variable based on the arguments. + + Parameters + ---------- + ip : str, optional + IP address to connect to the server. The default is ``None`` + in which case ``localhost`` is used. + port : int, optional + Port to connect to the Mecahnical server. The default is ``None``, + in which case ``10000`` is used. + timeout : float, optional + Maximum allowable time for connecting to the Mechanical server. + The default is ``60.0``. + loglevel : str, optional + Level of messages to print to the console. The default is ``WARNING``. + + - ``ERROR`` prints only error messages. + - ``WARNING`` prints warning and error messages. + - ``INFO`` prints info, warning and error messages. + - ``DEBUG`` prints debug, info, warning and error messages. + + log_file : bool, optional + Whether to copy the messages to a file named ``logs.log``, which is + located where the Python script is executed. The default is ``False``. + log_mechanical : str, optional + Path to the output file on the local disk for writing every script + command to. The default is ``None``. However, you might set + ``"log_mechanical='pymechanical_log.txt"`` to write all commands that are + sent to Mechanical via PyMechanical in this file so that you can use them + to run a script within Mechanical without PyMechanical. + cleanup_on_exit : bool, optional + Whether to exit Mechanical when Python exits. The default is ``False``, + in which case Mechanical is not exited when the garbage for this Mechanical + instance is collected. + channel : grpc.Channel, optional + gRPC channel to use for the connection. The default is ``None``. + You can use this parameter as an alternative to the ``ip`` and ``port`` + parameters. + remote_instance : ansys.platform.instancemanagement.Instance + Corresponding remote instance when Mechanical is launched + through PyPIM. The default is ``None``. If a remote instance + is specified, this instance is deleted when the + :func:`mecahnical.exit ` + function is called. + keep_connection_alive : bool, optional + Whether to keep the gRPC connection alive by running a background thread + and making dummy calls for remote connections. The default is ``True``. + + Examples + -------- + Connect to a Mechanical instance already running on locally on the + default port (``10000``). + + >>> from ansys.mechanical import core as pymechanical + >>> mechanical = pymechanical.Mechanical() + + Connect to a Mechanical instance running on the LAN on a default port. + + >>> mechanical = pymechanical.Mechanical('192.168.1.101') + + Connect to a Mechanical instance running on the LAN on a non-default port. + + >>> mechanical = pymechanical.Mechanical('192.168.1.101', port=60001) + + If you want to customize the channel, you can connect directly to gRPC channels. + For example, if you want to create an insecure channel with a maximum message + length of 8 MB, you would run: + + >>> import grpc + >>> channel_temp = grpc.insecure_channel( + ... '127.0.0.1:10000', + ... options=[ + ... ("grpc.max_receive_message_length", 8*1024**2), + ... ], + ... ) + >>> mechanical = pymechanical.Mechanical(channel=channel_temp) + """ + self._remote_instance = remote_instance + self._channel = channel + self._keep_connection_alive = keep_connection_alive + + self._locked = False # being used within MechanicalPool + + # ip could be a machine name. Convert it to an IP address. + ip_temp = ip + if channel is not None: + if ip is not None or port is not None: + raise ValueError( + "If `channel` is specified, neither `port` nor `ip` can be specified." + ) + elif ip is None: + ip_temp = "127.0.0.1" + else: + ip_temp = socket.gethostbyname(ip) # Converting ip or host name to ip + + self._ip = ip_temp + self._port = port + + self._start_parm = kwargs + + self._cleanup_on_exit = cleanup_on_exit + self._busy = False # used to check if running a command on the server + + self._local = ip_temp in ["127.0.0.1", "127.0.1.1", "localhost"] + if "local" in kwargs: # pragma: no cover # allow this to be overridden + self._local = kwargs["local"] + + self._health_response_queue = None + self._exiting = False + self._exited = None + + self._version = None + + if port is None: + port = MECHANICAL_DEFAULT_PORT + self._port = port + + self._stub = None + self._timeout = timeout + + if channel is None: + self._channel = self._create_channel(ip_temp, port) + else: + self._channel = channel + + self._logLevel = loglevel + self._log_file = log_file + self._log_mechanical = log_mechanical + + self._log = LOG.add_instance_logger(self.name, self, level=loglevel) # instance logger + # adding a file handler to the logger + if log_file: + if not isinstance(log_file, str): + log_file = "instance.log" + self._log.log_to_file(filename=log_file, level=loglevel) + + self._log_file_mechanical = log_mechanical + if log_mechanical: + if not isinstance(log_mechanical, str): + self._log_file_mechanical = "pymechanical_log.txt" + else: + self._log_file_mechanical = log_mechanical + + # temporarily disable logging + # useful when we run some dummy calls + self._disable_logging = False + + if self._local: + self.log_info("Mechanical connection is treated as local.") + else: + self.log_info("Mechanical connection is treated as remote.") + + # connect and validate to the channel + self._multi_connect(timeout=timeout) + + self.log_info("Mechanical is ready to accept grpc calls.") + + def __del__(self): # pragma: no cover + """Clean up on exit.""" + if self._cleanup_on_exit: + try: + self.exit(force=True) + except grpc.RpcError as e: + self.log_error(f"exit: {e}") + + # def _set_log_level(self, level): + # """Set an alias for the log level.""" + # self.set_log_level(level) + + @property + def log(self): + """Log associated with the current Mechanical instance.""" + return self._log + + @property + def version(self) -> str: + """Get the Mechanical version based on the instance. + + Examples + -------- + Get the version of the connected Mechanical instance. + + >>> mechanical.version + '251' + """ + if self._version is None: + try: + self._disable_logging = True + script = ( + 'clr.AddReference("Ans.Utilities")\n' + "import Ansys\n" + "config = Ansys.Utilities.ApplicationConfiguration.DefaultConfiguration\n" + "config.VersionInfo.VersionString" + ) + self._version = self.run_python_script(script) + except grpc.RpcError: # pragma: no cover + raise + finally: + self._disable_logging = False + return self._version + + @property + def name(self): + """Name (unique identifier) of the Mechanical instance.""" + try: + if self._channel is not None: + if self._remote_instance is not None: # pragma: no cover + return f"GRPC_{self._channel._channel._channel.target().decode()}" + else: + return f"GRPC_{self._channel._channel.target().decode()}" + except Exception as e: # pragma: no cover + LOG.error(f"Error getting the Mechanical instance name: {str(e)}") + + return f"GRPC_instance_{id(self)}" # pragma: no cover + + @property + def busy(self): + """Return True when the Mechanical gRPC server is executing a command.""" + return self._busy + + @property + def locked(self): + """Instance is in use within a pool.""" + return self._locked + + @locked.setter + def locked(self, new_value): + """Instance is in use within a pool.""" + self._locked = new_value + + def _multi_connect(self, n_attempts=5, timeout=60): + """Try to connect over a series of attempts to the channel. + + Parameters + ---------- + n_attempts : int, optional + Number of connection attempts. The default is ``5``. + timeout : float, optional + Maximum allowable time in seconds for establishing a connection. + The default is ``60``. + """ + # This prevents a single failed connection from blocking other attempts + connected = False + attempt_timeout = timeout / n_attempts + self.log_debug( + f"timetout:{timeout} n_attempts:{n_attempts} attempt_timeout={attempt_timeout}" + ) + + max_time = time.time() + timeout + i = 1 + while time.time() < max_time and i <= n_attempts: + self.log_debug(f"Connection attempt {i} with attempt timeout {attempt_timeout}s") + connected = self._connect(timeout=attempt_timeout) + + if connected: + self.log_debug(f"Connection attempt {i} succeeded.") + break + + i += 1 + else: # pragma: no cover + self.log_debug( + f"Reached either maximum amount of connection attempts " + f"({n_attempts}) or timeout ({timeout} s)." + ) + + if not connected: # pragma: no cover + raise IOError(f"Unable to connect to Mechanical instance at {self._channel_str}.") + + @property + def _channel_str(self): + """Target string, generally in the form of ``ip:port``, such as ``127.0.0.1:10000``.""" + if self._channel is not None: + if self._remote_instance is not None: + return self._channel._channel._channel.target().decode() # pragma: no cover + else: + return self._channel._channel.target().decode() + return "" # pragma: no cover + + def _connect(self, timeout=12, enable_health_check=False): + """Connect a gRPC channel to a remote or local Mechanical instance. + + Parameters + ---------- + timeout : float + Maximum allowable time in seconds for establishing a connection. The + default is ``12``. + enable_health_check : bool, optional + Whether to enable a check to see if the connection is healthy. + The default is ``False``. + """ + self._state = grpc.channel_ready_future(self._channel) + self._stub = mechanical_pb2_grpc.MechanicalServiceStub(self._channel) + + # verify connection + time_start = time.time() + while ((time.time() - time_start) < timeout) and not self._state._matured: + time.sleep(0.01) + + if not self._state._matured: # pragma: no cover + return False + + self.log_debug("Established a connection to the Mechanical gRPC server.") + + self.wait_till_mechanical_is_ready(timeout) + + # keeps Mechanical session alive + self._timer = None + if not self._local and self._keep_connection_alive: # pragma: no cover + self._initialised = threading.Event() + self._t_trigger = time.time() + self._t_delay = 30 + self._timer = threading.Thread( + target=Mechanical._threaded_heartbeat, args=(weakref.proxy(self),) + ) + self._timer.daemon = True + self._timer.start() + + # enable health check + if enable_health_check: # pragma: no cover + self._enable_health_check() + + self.__server_version = None + + return True + + def _enable_health_check(self): # pragma: no cover + """Place the status of the health check in the health response queue.""" + # lazy imports here to speed up module load + from grpc_health.v1 import health_pb2, health_pb2_grpc + + def _consume_responses(response_iterator, response_queue): + try: + for response in response_iterator: + response_queue.put(response) + # NOTE: We're doing absolutely nothing with this as + # this point since the server-side health check + # doesn't change state. + except Exception: + if self._exiting: + return + self._exited = True + raise MechanicalExitedError( + "Lost connection with the Mechanical gRPC server." + ) from None + + # enable health check + from queue import Queue + + request = health_pb2.HealthCheckRequest() + self._health_stub = health_pb2_grpc.HealthStub(self._channel) + rendezvous = self._health_stub.Watch(request) + + # health check feature implemented after 2023 R1 + try: + status = rendezvous.next() + except Exception as err: + if err.code().name != "UNIMPLEMENTED": + raise err + return + + if status.status != health_pb2.HealthCheckResponse.SERVING: + raise MechanicalRuntimeError( + "Cannot enable health check and/or connect to the Mechanical server." + ) + + self._health_response_queue = Queue() + + # allow main process to exit by setting daemon to true + thread = threading.Thread( + target=_consume_responses, + args=(rendezvous, self._health_response_queue), + daemon=True, + ) + thread.start() + + def _threaded_heartbeat(self): # pragma: no cover + """To call from a thread to verify that a Mechanical instance is alive.""" + self._initialised.set() + while True: + if self._exited: + break + try: + time.sleep(self._t_delay) + if not self.is_alive: + break + except ReferenceError: + break + # except Exception: + # continue + + def _create_channel(self, ip, port): + """Create an unsecured gRPC channel.""" + check_valid_ip(ip) + + # open the channel + channel_str = f"{ip}:{port}" + LOG.debug(f"Opening insecure channel at {channel_str}.") + return grpc.insecure_channel( + channel_str, + options=[ + ("grpc.max_receive_message_length", MAX_MESSAGE_LENGTH), + ], + ) + + @property + def is_alive(self) -> bool: + """Whether there is an active connection to the Mechanical gRPC server.""" + if self._exited: + return False + + if self._busy: # pragma: no cover + return True + + try: # pragma: no cover + self._make_dummy_call() + return True + except grpc.RpcError: + return False + + @staticmethod + def set_log_level(loglevel): + """Set the log level. + + Parameters + ---------- + loglevel : str, int + Level of logging. Options are ``"DEBUG"``, ``"INFO"``, ``"WARNING"`` + and ``"ERROR"``. + + Examples + -------- + Set the log level to the ``"DEBUG"`` level. + + # >>> mechanical.set_log_level('DEBUG') + # + # Set the log level to info + # + # >>> mechanical.set_log_level('INFO') + # + # Set the log level to warning + # + # >>> mechanical.set_log_level('WARNING') + # + # Set the log level to error + # + # >>> mechanical.set_log_level('ERROR') + """ + if isinstance(loglevel, str): + loglevel = loglevel.upper() + setup_logger(loglevel=loglevel) + + def get_product_info(self): + """Get product information by running a script on the Mechanical gRPC server.""" + + def _get_jscript_product_info_command(): + return ( + 'ExtAPI.Application.ScriptByName("jscript").ExecuteCommand' + '("var productInfo = DS.Script.getProductInfo();returnFromScript(productInfo);")' + ) + + def _get_python_product_info_command(): + return ( + 'clr.AddReference("Ansys.Mechanical.Application")\n' + "Ansys.Mechanical.Application.ProductInfo.ProductInfoAsString" + ) + + try: + self._disable_logging = True + if int(self.version) >= 232: + script = _get_python_product_info_command() + else: + script = _get_jscript_product_info_command() + return self.run_python_script(script) + except grpc.RpcError: + raise + finally: + self._disable_logging = False + + @suppress_logging + def __repr__(self): + """Get the user-readable string form of the Mechanical instance.""" + try: + if self._exited: + return "Mechanical exited." + return self.get_product_info() + except grpc.RpcError: + return "Error getting product info." + + def launch(self, cleanup_on_exit=True): + """Launch Mechanical in batch or UI mode. + + Parameters + ---------- + cleanup_on_exit : bool, optional + Whether to exit Mechanical when Python exits. The default is ``True``. + When ``False``, Mechanical is not exited when the garbage for this + Mechanical instance is collected. + """ + if not self._local: + raise RuntimeError("Can only launch with a local instance of Mechanical.") + + # let us respect the current cleanup behavior + if self._cleanup_on_exit: + self.exit() + + exec_file = self._start_parm.get("exec_file", get_mechanical_path(allow_input=False)) + batch = self._start_parm.get("batch", True) + additional_switches = self._start_parm.get("additional_switches", None) + additional_envs = self._start_parm.get("additional_envs", None) + port = launch_grpc( + exec_file=exec_file, + batch=batch, + additional_switches=additional_switches, + additional_envs=additional_envs, + verbose=True, + ) + # update the new cleanup behavior + self._cleanup_on_exit = cleanup_on_exit + self._port = port + self._channel = self._create_channel(self._ip, port) + self._connect(port) + + self.log_info("Mechanical is ready to accept gRPC calls.") + + def wait_till_mechanical_is_ready(self, wait_time=-1): + """Wait until Mechanical is ready. + + Parameters + ---------- + wait_time : float, optional + Maximum allowable time in seconds for connecting to the Mechanical gRPC server. + """ + time_1 = datetime.datetime.now() + + sleep_time = 0.5 + if wait_time == -1: # pragma: no cover + self.log_info("Waiting for Mechanical to be ready...") + else: + self.log_info(f"Waiting for Mechanical to be ready. Maximum wait time: {wait_time}s") + + while not self.__isMechanicalReady(): + time_2 = datetime.datetime.now() + time_interval = time_2 - time_1 + time_interval_seconds = int(time_interval.total_seconds()) + + self.log_debug( + f"Mechanical is not ready. You've been waiting for {time_interval_seconds}." + ) + if self._timeout != -1: + if time_interval_seconds > wait_time: + self.log_debug( + f"Allowed wait time {wait_time}s. " + f"Waited so for {time_interval_seconds}s, " + f"before throwing the error." + ) + raise RuntimeError( + f"Couldn't connect to Mechanical. " f"Waited for {time_interval_seconds}s." + ) + + time.sleep(sleep_time) + + time_2 = datetime.datetime.now() + time_interval = time_2 - time_1 + time_interval_seconds = int(time_interval.total_seconds()) + + self.log_info(f"Mechanical is ready. It took {time_interval_seconds} seconds to verify.") + + def __isMechanicalReady(self): + """Whether the Mechanical gRPC server is ready. + + Returns + ------- + bool + ``True`` if Mechanical is ready, ``False`` otherwise. + """ + try: + script = "ExtAPI.DataModel.Project.ProductVersion" + self.run_python_script(script) + except grpc.RpcError as error: + self.log_debug(f"Mechanical is not ready. Error:{error}.") + return False + + return True + + @staticmethod + def convert_to_server_log_level(log_level): + """Convert the log level to the server log level. + + Parameters + ---------- + log_level : str + Level of logging. Options are ``"DEBUG"``, ``"INFO"``, ``"WARNING"``, + ``"ERROR"``, and ``"CRITICAL"``. + + Returns + ------- + Converted log level for the server. + """ + value = client_to_server_loglevel.get(log_level) + + if value is not None: + return value + + raise ValueError( + f"Log level {log_level} is invalid. Possible values are " + f"'DEBUG','INFO', 'WARNING', 'ERROR', and 'CRITICAL'." + ) + + def run_python_script( + self, script_block: str, enable_logging=False, log_level="WARNING", progress_interval=2000 + ): + """Run a Python script block inside Mechanical. + + It returns the string value of the last executed statement. If the value cannot be + returned as a string, it will return an empty string. + + Parameters + ---------- + script_block : str + Script block (one or more lines) to run. + enable_logging: bool, optional + Whether to enable logging. The default is ``False``. + log_level: str + Level of logging. The default is ``"WARNING"``. Options are ``"DEBUG"``, + ``"INFO"``, ``"WARNING"``, and ``"ERROR"``. + progress_interval: int, optional + Frequency in milliseconds for getting log messages from the server. + The default is ``2000``. + + Returns + ------- + str + Script result. + + Examples + -------- + Return a value from a simple calculation. + + >>> mechanical.run_python_script('2+3') + '5' + + Return a string value from Project object. + + >>> mechanical.run_python_script('ExtAPI.DataModel.Project.ProductVersion') + '2025 R1' + + Return an empty string, when you try to return the Project object. + + >>> mechanical.run_python_script('ExtAPI.DataModel.Project') + '' + + Return an empty string for assignments. + + >>> mechanical.run_python_script('version = ExtAPI.DataModel.Project.ProductVersion') + '' + + Return value from the last executed statement from a variable. + + >>> script=''' + addition = 2 + 3 + multiplication = 3 * 4 + multiplication + ''' + >>> mechanical.run_python_script(script) + '12' + + Return value from last executed statement from a function call. + + >>> script=''' + import math + math.pow(2,3) + ''' + >>> mechanical.run_python_script(script) + '8' + + Handle an error scenario. + + >>> script = 'hello_world()' + >>> import grpc + >>> try: + mechanical.run_python_script(script) + except grpc.RpcError as error: + print(error.details()) + name 'hello_world' is not defined + + """ + self.verify_valid_connection() + result_as_string = self.__call_run_python_script( + script_block, enable_logging, log_level, progress_interval + ) + return result_as_string + + def run_python_script_from_file( + self, file_path, enable_logging=False, log_level="WARNING", progress_interval=2000 + ): + """Run the contents a python file inside Mechanical. + + It returns the string value of the last executed statement. If the value cannot be + returned as a string, it will return an empty string. + + Parameters + ---------- + file_path : + Path for the Python file. + enable_logging: bool, optional + Whether to enable logging. The default is ``False``. + log_level: str + Level of logging. The default is ``"WARNING"``. Options are ``"DEBUG"``, + ``"INFO"``, ``"WARNING"``, and ``"ERROR"``. + progress_interval: int, optional + Frequency in milliseconds for getting log messages from the server. + The default is ``2000``. + + Returns + ------- + str + Script result. + + Examples + -------- + Return a value from a simple calculation. + + Contents of **simple.py** file + + 2+3 + + >>> mechanical.run_python_script_from_file('simple.py') + '5' + + Return a value from a simple function call. + + Contents of **test.py** file + + import math + + math.pow(2,3) + + >>> mechanical.run_python_script_from_file('test.py') + '8' + + """ + self.verify_valid_connection() + self.log_debug(f"run_python_script_from_file started") + script_code = Mechanical.__readfile(file_path) + self.log_debug(f"run_python_script_from_file started") + return self.run_python_script(script_code, enable_logging, log_level, progress_interval) + + def exit(self, force=False): + """Exit Mechanical. + + Parameters + ---------- + force : bool, optional + Whether to force Mechanical to exit. The default is ``False``, in which case + only Mechanical in UI mode asks for confirmation. This parameter overrides + any environment variables that may inhibit exiting Mechanical. + + Examples + -------- + Exit Mechanical. + + >>> mechanical.Exit(force=True) + + """ + if not force: + if not get_start_instance(): + self.log_info("Ignoring exit due to PYMECHANICAL_START_INSTANCE=False") + return + + # or building the gallery + if pymechanical.BUILDING_GALLERY: + self._log.info("Ignoring exit due to BUILDING_GALLERY=True") + return + + if self._exited: + return + + self.verify_valid_connection() + + self._exiting = True + + self.log_debug("In shutdown.") + request = mechanical_pb2.ShutdownRequest(force_exit=force) + self.log_debug("Shutting down...") + + self._busy = True + try: + self._stub.Shutdown(request) + except grpc._channel._InactiveRpcError as error: + self.log_warning("Mechanical exit failed: {str(error}.") + finally: + self._busy = False + + self._exited = True + self._stub = None + + if self._remote_instance is not None: # pragma: no cover + self.log_debug("PyPIM delete has started.") + try: + self._remote_instance.delete() + except Exception as error: + self.log_warning("Remote instance delete failed: {str(error}.") + self.log_debug("PyPIM delete has finished.") + + self._remote_instance = None + self._channel = None + else: + self.log_debug("No PyPIM cleanup is needed.") + + local_ports = pymechanical.LOCAL_PORTS + if self._local and self._port in local_ports: + local_ports.remove(self._port) + + self.log_info("Shutdown has finished.") + + @protect_grpc + def upload( + self, + file_name, + file_location_destination=None, + chunk_size=DEFAULT_FILE_CHUNK_SIZE, + progress_bar=True, + ): + """Upload a file to the Mechanical instance. + + Parameters + ---------- + file_name : str + Local file to upload. Only the file name is needed if the file + is relative to the current working directory. Otherwise, the full path + is needed. + file_location_destination : str, optional + File location on the Mechanical server to upload the file to. The default is + ``None``, in which case the project directory is used. + chunk_size : int, optional + Chunk size in bytes. The default is ``1048576``. + progress_bar : bool, optional + Whether to show a progress bar using ``tqdm``. The default is ``True``. + A progress bar is helpful for viewing upload progress. + + Returns + ------- + str + Base name of the uploaded file. + + Examples + -------- + Upload the ``hsec.x_t`` file with the progress bar not shown. + + >>> mechanical.upload('hsec.x_t', progress_bar=False) + """ + self.verify_valid_connection() + + if not os.path.isfile(file_name): + raise FileNotFoundError(f"Unable to locate filename {file_name}.") + + self._log.debug(f"Uploading file '{file_name}' to the Mechanical instance.") + + if file_location_destination is None: + file_location_destination = self.project_directory + + self._busy = True + try: + chunks_generator = self.get_file_chunks( + file_location_destination, + file_name, + chunk_size=chunk_size, + progress_bar=progress_bar, + ) + response = self._stub.UploadFile(chunks_generator) + self.log_debug(f"upload_file response is {response.is_ok}.") + finally: + self._busy = False + + if not response.is_ok: # pragma: no cover + raise IOError("File failed to upload.") + return os.path.basename(file_name) + + def get_file_chunks(self, file_location, file_name, chunk_size, progress_bar): + """Construct the file upload request for the server. + + Parameters + ---------- + file_location_destination : str, optional + Directory where the file to upload to the server is located. + file_name : str + Name of the file to upload. + chunk_size : int + Chunk size in bytes. + progress_bar : bool + Whether to show a progress bar using ``tqdm``. + """ + pbar = None + if progress_bar: + if not _HAS_TQDM: # pragma: no cover + raise ModuleNotFoundError( + f"To use the keyword argument 'progress_bar', you must have " + f"installed the 'tqdm' package. To avoid this message, you can " + f"set 'progress_bar=False'." + ) + + n_bytes = os.path.getsize(file_name) + + base_name = os.path.basename(file_name) + pbar = tqdm( + total=n_bytes, + desc=f"Uploading {base_name} to {self._channel_str}:{file_location}.", + unit="B", + unit_scale=True, + unit_divisor=1024, + ) + + with open(file_name, "rb") as f: + while True: + piece = f.read(chunk_size) + length = len(piece) + if length == 0: + if pbar is not None: + pbar.close() + return + + if pbar is not None: + pbar.update(length) + + chunk = mechanical_pb2.Chunk(payload=piece, size=length) + yield mechanical_pb2.FileUploadRequest( + file_name=os.path.basename(file_name), file_location=file_location, chunk=chunk + ) + + @property + def project_directory(self): + """Get the project directory for the currently connected Mechanical instance. + + Examples + -------- + Get the project directory of the connected Mechanical instance. + + >>> mechanical.project_directory + '/tmp/ANSYS.username.1/AnsysMech3F97/Project_Mech_Files/' + + """ + return self.run_python_script("ExtAPI.DataModel.Project.ProjectDirectory") + + def list_files(self): + """List the files in the working directory of Mechanical. + + Returns + ------- + list + List of files in the working directory of Mechanical. + + Examples + -------- + List the files in the working directory. + + >>> files = mechanical.list_files() + >>> for file in files: print(file) + """ + result = self.run_python_script( + "import pymechanical_helpers\npymechanical_helpers.GetAllProjectFiles(ExtAPI)" + ) + + files_out = result.splitlines() + if not files_out: # pragma: no cover + self.log_warning("No files listed") + return files_out + + def _get_files(self, files, recursive=False): + self_files = self.list_files() # to avoid calling it too much + + if isinstance(files, str): + if self._local: # pragma: no cover + # in local mode + if os.path.exists(files): + if not os.path.isabs(files): + list_files = [os.path.join(os.getcwd(), files)] + else: + # file exist + list_files = [files] + elif "*" in files: + # using filter + list_files = glob.glob(files, recursive=recursive) + if not list_files: + raise ValueError( + f"The `'files'` parameter ({files}) didn't match any file using " + f"glob expressions in the local client." + ) + else: + raise ValueError( + f"The files parameter ('{files}') does not match any file or pattern." + ) + else: # Remote or looking into Mechanical working directory + if files in self_files: + list_files = [files] + elif "*" in files: + # try filter on the list_files + if recursive: + self.log_warning( + "Because the 'recursive' keyword argument does not work with " + "remote instances, it is ignored." + ) + list_files = fnmatch.filter(self_files, files) + if not list_files: + raise ValueError( + f"The `'files'` parameter ({files}) didn't match any file using " + f"glob expressions in the remote server." + ) + else: + raise ValueError( + f"The `'files'` parameter ('{files}') does not match any file or pattern." + ) + + elif isinstance(files, (list, tuple)): + if not all([isinstance(each, str) for each in files]): + raise ValueError( + "The parameter `'files'` can be a list or tuple, but it " + "should only contain strings." + ) + list_files = files + else: + raise ValueError( + f"The `file` parameter type ({type(files)}) is not supported." + "Only strings, tuple of strings, or list of strings are allowed." + ) + + return list_files + + def download( + self, + files, + target_dir=None, + chunk_size=DEFAULT_CHUNK_SIZE, + progress_bar=None, + recursive=False, + ): # pragma: no cover + """Download files from the working directory of the Mechanical instance. + + It downloads them from the working directory to the target directory. It returns the list + of local file paths for the downloaded files. + + Parameters + ---------- + files : str, list[str], tuple(str) + One or more files on the Mechanical server to download. The files must be + in the same directory as the Mechanical instance. You can use the + :func:`Mechanical.list_files ` + function to list current files. Alternatively, you can specify *glob expressions* to + match file names. For example, you could use ``file*`` to match every file whose + name starts with ``file``. + target_dir: str + Default directory to copy the downloaded files to. The default is ``None`` and + current working directory will be used as target directory. + chunk_size : int, optional + Chunk size in bytes. The default is ``262144``. The value must be less than 4 MB. + progress_bar : bool, optional + Whether to show a progress bar using ``tqdm``. The default is ``None``, in + which case a progress bar is shown. A progress bar is helpful for viewing download + progress. + recursive : bool, optional + Whether to use recursion when using a glob pattern search. The default is ``False``. + + Returns + ------- + List[str] + List of local file paths. + + Notes + ----- + There are some considerations to keep in mind when using the ``download()`` method: + + * The glob pattern search does not search recursively in remote instances. + * In a remote instance, it is not possible to list or download files in a + location other than the Mechanical working directory. + * If you are connected to a local instance and provide a file path, downloading files + from a different folder is allowed but is not recommended. + + Examples + -------- + Download a single file. + + >>> local_file_path_list = mechanical.download('file.out') + + Download all files starting with ``file``. + + >>> local_file_path_list = mechanical.download('file*') + + Download every file in the Mechanical working directory. + + >>> local_file_path_list = mechanical.download('*.*') + + Alternatively, the recommended method is to use the + :func:`download_project() ` + method to download all files. + + >>> local_file_path_list = mechanical.download_project() + + """ + self.verify_valid_connection() + + if chunk_size > 4 * 1024 * 1024: # 4MB + raise ValueError( + "Chunk sizes bigger than 4 MB can generate unstable behaviour in PyMechanical. " + "Decrease the ``chunk_size`` value." + ) + + list_files = self._get_files(files, recursive=recursive) + + if target_dir: + path = pathlib.Path(target_dir) + path.mkdir(parents=True, exist_ok=True) + else: + target_dir = os.getcwd() + + out_files = [] + + for each_file in list_files: + try: + file_name = os.path.basename(each_file) # Getting only the name of the file. + # We try to avoid that when the full path is supplied. It crashes when trying + # to do `os.path.join(target_dir"os.getcwd()", file_name "full filename path"` + # This produces the file structure to flat out, but it is fine, + # because recursive does not work in remote. + self._busy = True + out_file_path = self._download( + each_file, + out_file_name=os.path.join(target_dir, file_name), + chunk_size=chunk_size, + progress_bar=progress_bar, + ) + out_files.append(out_file_path) + except FileNotFoundError: + # So far the gRPC interface returns the size of the file equal + # zero, if the file does not exist, or if its size is zero, + # but they are two different things. + # In theory, since we are obtaining the files name from + # `mechanical.list_files()`, they do exist, so + # if there is any error, it means their size is zero. + pass # This is not the best. + finally: + self._busy = False + + return out_files + + @protect_grpc + def _download( + self, + target_name, + out_file_name, + chunk_size=DEFAULT_CHUNK_SIZE, + progress_bar=None, + ): + """Download a file from the Mechanical instance. + + Parameters + ---------- + target_name : str + Name of the target file on the server. The file must be in the same + directory as the Mechanical instance. You can use the + ``mechanical.list_files()`` function to list current files. + out_file_name : str + Name of the output file if the name is to differ from that for the target + file. + chunk_size : int, optional + Chunk size in bytes. The default is ``"DEFAULT_CHUNK_SIZE"``, in which case + 256 kB is used. The value must be less than 4 MB. + progress_bar : bool, optional + Whether to show a progress bar using ``tqdm``. The default is ``None``, in + which case a progress bar is shown. A progress bar is helpful for showing download + progress. + + Examples + -------- + Download the remote result file "file.rst" as "my_result.rst". + + >>> mechanical.download('file.rst', 'my_result.rst') + """ + self.verify_valid_connection() + + if not progress_bar and _HAS_TQDM: + progress_bar = True + + request = mechanical_pb2.FileDownloadRequest(file_path=target_name, chunk_size=chunk_size) + + responses = self._stub.DownloadFile(request) + + file_size = self.save_chunks_to_file( + responses, out_file_name, progress_bar=progress_bar, target_name=target_name + ) + + if not file_size: # pragma: no cover + raise FileNotFoundError(f'File "{out_file_name}" is empty or does not exist') + + self.log_info(f"{out_file_name} with size {file_size} has been written.") + + return out_file_name + + def save_chunks_to_file(self, responses, filename, progress_bar=False, target_name=""): + """Save chunks to a local file. + + Parameters + ---------- + responses : + filename : str + Name of the local file to save chunks to. + progress_bar : bool, optional + Whether to show a progress bar using ``tqdm``. The default is ``False``. + target_name : str, optional + Name of the target file on the server. The default is ``""``. The file + must be in the same directory as the Mechanical instance. You can use the + ``mechanical.list_files()`` function to list current files. + + Returns + ------- + file_size : int + File size saved in bytes. If ``0`` is returned, no file was written. + """ + pbar = None + if progress_bar: + if not _HAS_TQDM: # pragma: no cover + raise ModuleNotFoundError( + "To use the keyword argument 'progress_bar', you need to have installed " + "the 'tqdm' package.To avoid this message you can set 'progress_bar=False'." + ) + + file_size = 0 + with open(filename, "wb") as f: + for response in responses: + f.write(response.chunk.payload) + payload_size = len(response.chunk.payload) + file_size += payload_size + if pbar is None: + pbar = tqdm( + total=response.file_size, + desc=f"Downloading {self._channel_str}:{target_name} to {filename}", + unit="B", + unit_scale=True, + unit_divisor=1024, + ) + pbar.update(payload_size) + else: + pbar.update(payload_size) + + if pbar is not None: + pbar.close() + + return file_size + + def download_project(self, extensions=None, target_dir=None, progress_bar=False): + """Download all project files in the working directory of the Mechanical instance. + + It downloads them from the working directory to the target directory. It returns the list + of local file paths for the downloaded files. + + Parameters + ---------- + extensions : list[str], tuple[str], optional + List of extensions for filtering files before downloading them. The + default is ``None``. + target_dir : str, optional + Path for downloading the files to. The default is ``None``. + progress_bar : bool, optional + Whether to show a progress bar using ``tqdm``. The default is ``False``. + A progress bar is helpful for viewing download progress. + + Returns + ------- + List[str] + List of local file paths. + + Examples + -------- + Download all the files in the project. + + >>> local_file_path_list = mechanical.download_project() + """ + destination_directory = target_dir.rstrip("\\/") + + # let us create the directory, if it doesn't exist + if destination_directory: + path = pathlib.Path(destination_directory) + path.mkdir(parents=True, exist_ok=True) + else: + destination_directory = os.getcwd() + + # relative directory? + if os.path.isdir(destination_directory): + if not os.path.isabs(destination_directory): + # construct full path + destination_directory = os.path.join(os.getcwd(), destination_directory) + + project_directory = self.project_directory + # remove the trailing slash - server could be windows or linux + project_directory = project_directory.rstrip("\\/") + + # this is where .mechddb resides + parent_directory = os.path.dirname(project_directory) + + list_of_files = [] + + if not extensions: + files = self.list_files() + else: + files = [] + for each_extension in extensions: + # mechdb resides one level above project directory + if "mechdb" == each_extension.lower(): + file_temp = os.path.join(parent_directory, f"*.{each_extension}") + else: + file_temp = os.path.join(project_directory, "**", f"*.{each_extension}") + + if self._local: + list_files_expanded = self._get_files(file_temp, recursive=True) + + if "mechdb" == each_extension.lower(): + # if we have more than one .mechdb in the parent folder + # filter to have only the current mechdb + self_files = self.list_files() + filtered_files = [] + for temp_file in list_files_expanded: + if temp_file in self_files: + filtered_files.append(temp_file) + list_files = filtered_files + else: + list_files = list_files_expanded + else: + list_files = self._get_files(file_temp, recursive=False) + + files.extend(list_files) + + for file in files: + # create similar hierarchy locally + new_path = file.replace(parent_directory, destination_directory) + new_path_dir = os.path.dirname(new_path) + temp_files = self.download( + files=file, target_dir=new_path_dir, progress_bar=progress_bar + ) + list_of_files.extend(temp_files) + + return list_of_files + + def clear(self): + """Clear the database. + + Examples + -------- + Clear the database. + + >>> mechanical.clear() + + """ + self.run_python_script("ExtAPI.DataModel.Project.New()") + + def _make_dummy_call(self): + try: + self._disable_logging = True + self.run_python_script("ExtAPI.DataModel.Project.ProjectDirectory") + except grpc.RpcError: # pragma: no cover + raise + finally: + self._disable_logging = False + + @staticmethod + def __readfile(file_path): + """Get the contents of the file as a string.""" + # open text file in read mode + text_file = open(file_path, "r", encoding="utf-8") + # read whole file to a string + data = text_file.read() + # close file + text_file.close() + + return data + + def __call_run_python_script( + self, script_code: str, enable_logging, log_level, progress_interval + ): + """Run the Python script block on the server. + + Parameters + ---------- + script_block : str + Script block (one or more lines) to run. + enable_logging: bool + Whether to enable logging + log_level: str + Level of logging. Options are ``"DEBUG"``, ``"INFO"``, ``"WARNING"``, + and ``"ERROR"``. + timeout: int, optional + Frequency in milliseconds for getting log messages from the server. + + Returns + ------- + str + Script result. + + """ + log_level_server = self.convert_to_server_log_level(log_level) + request = mechanical_pb2.RunScriptRequest() + request.script_code = script_code + request.enable_logging = enable_logging + request.logger_severity = log_level_server + request.progress_interval = progress_interval + + result = "" + self._busy = True + + try: + for runscript_response in self._stub.RunPythonScript(request): + if runscript_response.log_info == "__done__": + result = runscript_response.script_result + break + else: + if enable_logging: + self.log_message(log_level, runscript_response.log_info) + except grpc.RpcError as error: + error_info = error.details() + error_info_lower = error_info.lower() + # For the given script, return value cannot be converted to string. + if ( + "the expected result" in error_info_lower + and "cannot be return via this API." in error_info + ): + if enable_logging: + self.log_debug(f"Ignoring the conversion error.{error_info}") + result = "" + else: + raise + finally: + self._busy = False + + self._log_mechanical_script(script_code) + + return result + + def log_message(self, log_level, message): + """Log the message using the given log level. + + Parameters + ---------- + log_level: str + Level of logging. Options are ``"DEBUG"``, ``"INFO"``, ``"WARNING"``, + and ``"ERROR"``. + message : str + Message to log. + + Examples + -------- + Log a debug message. + + >>> mechanical.log_message('DEBUG', 'debug message') + + Log an info message. + + >>> mechanical.log_message('INFO', 'info message') + + """ + if log_level == "DEBUG": + self.log_debug(message) + elif log_level == "INFO": + self.log_info(message) + elif log_level == "WARNING": + self.log_warning(message) + elif log_level == "ERROR": + self.log_error(message) + + def log_debug(self, message): + """Log the debug message.""" + if self._disable_logging: + return + self._log.debug(message) + + def log_info(self, message): + """Log the info message.""" + if self._disable_logging: + return + self._log.info(message) + + def log_warning(self, message): + """Log the warning message.""" + if self._disable_logging: + return + self._log.warning(message) + + def log_error(self, message): + """Log the error message.""" + if self._disable_logging: + return + self._log.error(message) + + def verify_valid_connection(self): + """Verify whether the connection to Mechanical is valid.""" + if self._exited: + raise MechanicalExitedError("Mechanical has already exited.") + + if self._stub is None: # pragma: no cover + raise ValueError( + "There is not a valid connection to Mechanical. Launch or connect to it first." + ) + + @property + def exited(self): + """Whether Mechanical already exited.""" + return self._exited + + def _log_mechanical_script(self, script_code): + if self._disable_logging: + return + + if self._log_file_mechanical: + try: + with open(self._log_file_mechanical, "a", encoding="utf-8") as file: + file.write(script_code) + file.write("\n") + except IOError as e: # pragma: no cover + self.log_warning(f"I/O error({e.errno}): {e.strerror}") + except Exception as e: # pragma: no cover + self.log_warning("Unexpected error:" + str(e)) + + +def get_start_instance(start_instance_default=True): + """Check if the ``PYMECHANICAL_START_INSTANCE`` environment variable exists and is valid. + + Parameters + ---------- + start_instance_default : bool, optional + Value to return when ``PYMECHANICAL_START_INSTANCE`` is unset. + + Returns + ------- + bool + ``True`` when the ``PYMECHANICAL_START_INSTANCE`` environment variable exists + and is valid, ``False`` when this environment variable does not exist or is not valid. + If it is unset, ``start_instance_default`` is returned. + + Raises + ------ + OSError + Raised when ``PYMECHANICAL_START_INSTANCE`` is not either ``True`` or ``False`` + (case independent). + + """ + if "PYMECHANICAL_START_INSTANCE" in os.environ: + if os.environ["PYMECHANICAL_START_INSTANCE"].lower() not in [ + "true", + "false", + ]: # pragma: no cover + val = os.environ["PYMECHANICAL_START_INSTANCE"] + raise OSError( + f'Invalid value "{val}" for PYMECHANICAL_START_INSTANCE\n' + 'PYMECHANICAL_START_INSTANCE should be either "TRUE" or "FALSE"' + ) + return os.environ["PYMECHANICAL_START_INSTANCE"].lower() == "true" + return start_instance_default + + +def launch_grpc( + exec_file="", + batch=True, + port=MECHANICAL_DEFAULT_PORT, + additional_switches=None, + additional_envs=None, + verbose=False, +) -> int: + """Start Mechanical locally in gRPC mode. + + Parameters + ---------- + exec_file : str, optional + Path for the Mechanical executable file. The default is ``None``, in which + case the cached location is used. + batch : bool, optional + Whether to launch Mechanical in batch mode. The default is ``True``. + When ``False``, Mechanical is launched in UI mode. + port : int, optional + Port to launch the Mechanical instance on. The default is + ``MECHANICAL_DEFAULT_PORT``. The final port is the first + port available after (or including) this port. + additional_switches : list, optional + List of additional arguments to pass. The default is ``None``. + additional_envs : dictionary, optional + Dictionary of additional environment variables to pass. The default + is ``None``. + verbose : bool, optional + Whether to print all output when launching and running Mechanical. The + default is ``False``. Printing all output is not recommended unless + you are debugging the startup of Mechanical. + + Returns + ------- + int + Port number that the Mechanical instance started on. + + Notes + ----- + If ``PYMECHANICAL_START_INSTANCE`` is set to FALSE, the ``launch_mechanical`` + method looks for an existing instance of Mechanical at ``PYMECHANICAL_IP`` on port + ``PYMECHANICAL_PORT``, with default to ``127.0.0.1`` and ``10000`` if unset. + This is typically used for automated documentation and testing. + + Examples + -------- + Launch Mechanical using the default configuration. + + >>> from ansys.mechanical.core import launch_mechanical + >>> mechanical = launch_mechanical() + + Launch Mechanical using a specified executable file. + + >>> exec_file_path = 'C:/Program Files/ANSYS Inc/v251/aisol/bin/win64/AnsysWBU.exe' + >>> mechanical = launch_mechanical(exec_file_path) + + """ + # verify version + if atp.version_from_path("mechanical", exec_file) < 232: + raise VersionError("The Mechanical gRPC interface requires Mechanical 2023 R2 or later.") + + # get the next available port + local_ports = pymechanical.LOCAL_PORTS + if port is None: + if not local_ports: + port = MECHANICAL_DEFAULT_PORT + else: + port = max(local_ports) + 1 + + while port_in_use(port) or port in local_ports: + port += 1 + local_ports.append(port) + + mechanical_launcher = MechanicalLauncher( + batch, port, exec_file, additional_switches, additional_envs, verbose + ) + mechanical_launcher.launch() + + return port + + +def launch_remote_mechanical(version=None) -> (grpc.Channel, Instance): # pragma: no cover + """Start Mechanical remotely using the Product Instance Management (PIM) API. + + When calling this method, you must ensure that you are in an environment + where PyPIM is configured. You can use the + :func:`pypim.is_configured ` + method to verify that PyPIM is configured. + + Parameters + ---------- + version : str, optional + Mechanical version to run in the three-digit format. For example, ``"251"`` to + run 2025 R1. The default is ``None``, in which case the server runs the latest + installed version. + + Returns + ------- + Tuple containing channel, remote_instance. + """ + pim = pypim.connect() + instance = pim.create_instance(product_name="mechanical", product_version=version) + + LOG.info("PyPIM wait for ready has started.") + instance.wait_for_ready() + LOG.info("PyPIM wait for ready has finished.") + + channel = instance.build_grpc_channel( + options=[ + ("grpc.max_receive_message_length", MAX_MESSAGE_LENGTH), + ] + ) + + return channel, instance + + +def launch_mechanical( + allow_input=True, + exec_file=None, + batch=True, + loglevel="ERROR", + log_file=False, + log_mechanical=None, + additional_switches=None, + additional_envs=None, + start_timeout=120, + port=None, + ip=None, + start_instance=None, + verbose_mechanical=False, + clear_on_connect=False, + cleanup_on_exit=True, + version=None, + keep_connection_alive=True, +) -> Mechanical: + """Start Mechanical locally. + + Parameters + ---------- + allow_input: bool, optional + Whether to allow user input when discovering the path to the Mechanical + executable file. + exec_file : str, optional + Path for the Mechanical executable file. The default is ``None``, + in which case the cached location is used. If PyPIM is configured + and this parameter is set to ``None``, PyPIM launches Mechanical + using its ``version`` parameter. + batch : bool, optional + Whether to launch Mechanical in batch mode. The default is ``True``. + When ``False``, Mechanical launches in UI mode. + loglevel : str, optional + Level of messages to print to the console. + Options are: + + - ``"WARNING"``: Prints only Ansys warning messages. + - ``"ERROR"``: Prints only Ansys error messages. + - ``"INFO"``: Prints all Ansys messages. + + The default is ``WARNING``. + log_file : bool, optional + Whether to copy the messages to a file named ``logs.log``, which is + located where the Python script is executed. The default is ``False``. + log_mechanical : str, optional + Path to the output file on the local disk to write every script + command to. The default is ``None``. However, you might set + ``"log_mechanical='pymechanical_log.txt'"`` to write all commands that are + sent to Mechanical via PyMechanical to this file. You can then use these + commands to run a script within Mechanical without PyMechanical. + additional_switches : list, optional + Additional switches for Mechanical. The default is ``None``. + additional_envs : dictionary, optional + Dictionary of additional environment variables to pass. The default + is ``None``. + start_timeout : float, optional + Maximum allowable time in seconds to connect to the Mechanical server. + The default is ``120``. + port : int, optional + Port to launch the Mechanical gRPC server on. The default is ``None``, + in which case ``10000`` is used. The final port is the first + port available after (or including) this port. You can override the + default behavior of this parameter with the + ``PYMECHANICAL_PORT=`` environment variable. + ip : str, optional + IP address to use only when ``start_instance`` is ``False``. The + default is ``None``, in which case ``"127.0.0.1"`` is used. If you + provide an IP address, ``start_instance`` is set to ``False``. + A host name can be provided as an alternative to an IP address. + start_instance : bool, optional + Whether to launch and connect to a new Mechanical instance. The default + is ``None``, in which case an attempt is made to connect to an existing + Mechanical instance at the given ``ip`` and ``port`` parameters, which have + defaults of ``"127.0.0.1"`` and ``10000`` respectively. When ``True``, + a local instance of Mechanical is launched. You can override the default + behavior of this parameter with the ``PYMECHANICAL_START_INSTANCE=FALSE`` + environment variable. + verbose_mechanical : bool, optional + Whether to enable printing of all output when launching and running + a Mechanical instance. The default is ``False``. This parameter should be + set to ``True`` for debugging only as output can be tracked within + PyMechanical. + clear_on_connect : bool, optional + When ``start_instance`` is ``False``, whether to clear the environment + when connecting to Mechanical. The default is ``False``. When ``True``, + a fresh environment is provided when you connect to Mechanical. + cleanup_on_exit : bool, optional + Whether to exit Mechanical when Python exits. The default is ``True``. + When ``False``, Mechanical is not exited when the garbage for this Mechanical + instance is collected. + version : str, optional + Mechanical version to run in the three-digit format. For example, ``"251"`` + for 2025 R1. The default is ``None``, in which case the server runs the + latest installed version. If PyPIM is configured and ``exce_file=None``, + PyPIM launches Mechanical using its ``version`` parameter. + keep_connection_alive : bool, optional + Whether to keep the gRPC connection alive by running a background thread + and making dummy calls for remote connections. The default is ``True``. + + Returns + ------- + ansys.mechanical.core.mechanical.Mechanical + Instance of Mechanical. + + Notes + ----- + If the environment is configured to use `PyPIM `_ + and ``start_instance=True``, then starting the instance is delegated to PyPIM. + In this case, most of the preceding parameters are ignored because the server-side + configuration is used. + + Examples + -------- + Launch Mechanical. + + >>> from ansys.mechanical.core import launch_mechanical + >>> mech = launch_mechanical() + + Launch Mechanical using a specified executable file. + + >>> exec_file_path = 'C:/Program Files/ANSYS Inc/v251/aisol/bin/win64/AnsysWBU.exe' + >>> mech = launch_mechanical(exec_file_path) + + Connect to an existing Mechanical instance at IP address ``192.168.1.30`` on port + ``50001``. + + >>> mech = launch_mechanical(start_instance=False, ip='192.168.1.30', port=50001) + """ + # Start Mechanical with PyPIM if the environment is configured for it + # and a directive on how to launch Mechanical was not passed. + if pypim.is_configured() and exec_file is None: # pragma: no cover + LOG.info("Starting Mechanical remotely. The startup configuration will be ignored.") + channel, remote_instance = launch_remote_mechanical(version=version) + return Mechanical( + channel=channel, + remote_instance=remote_instance, + loglevel=loglevel, + log_file=log_file, + log_mechanical=log_mechanical, + timeout=start_timeout, + cleanup_on_exit=cleanup_on_exit, + keep_connection_alive=keep_connection_alive, + ) + + if ip is None: + ip = os.environ.get("PYMECHANICAL_IP", LOCALHOST) + else: # pragma: no cover + start_instance = False + ip = socket.gethostbyname(ip) # Converting ip or host name to ip + + check_valid_ip(ip) # double check + + if port is None: + port = int(os.environ.get("PYMECHANICAL_PORT", MECHANICAL_DEFAULT_PORT)) + check_valid_port(port) + + # connect to an existing instance if enabled + if start_instance is None: + start_instance = check_valid_start_instance( + os.environ.get("PYMECHANICAL_START_INSTANCE", True) + ) + + # special handling when building the gallery outside of CI. This + # creates an instance of Mechanical the first time if PYMECHANICAL_START_INSTANCE + # is False. + # when you launch, treat it as local. + # when you connect, treat it as remote. We cannot differentiate between + # local vs container scenarios. In the container scenarios, we could be connecting + # to a container using local ip and port + if pymechanical.BUILDING_GALLERY: # pragma: no cover + # launch an instance of PyMechanical if it does not already exist and + # starting instances is allowed + if start_instance and GALLERY_INSTANCE[0] is None: + mechanical = launch_mechanical( + start_instance=True, + cleanup_on_exit=False, + loglevel=loglevel, + ) + GALLERY_INSTANCE[0] = {"ip": mechanical._ip, "port": mechanical._port} + return mechanical + + # otherwise, connect to the existing gallery instance if available + elif GALLERY_INSTANCE[0] is not None: + mechanical = Mechanical( + ip=GALLERY_INSTANCE[0]["ip"], + port=GALLERY_INSTANCE[0]["port"], + cleanup_on_exit=False, + loglevel=loglevel, + local=False, + ) + # we are connecting to the existing gallery instance, + # we need to clear Mechanical. + mechanical.clear() + + return mechanical + + # finally, if running on CI/CD, connect to the default instance + else: + mechanical = Mechanical( + ip=ip, port=port, cleanup_on_exit=False, loglevel=loglevel, local=False + ) + # we are connecting for gallery generation, + # we need to clear Mechanical. + mechanical.clear() + return mechanical + + if not start_instance: + mechanical = Mechanical( + ip=ip, + port=port, + loglevel=loglevel, + log_file=log_file, + log_mechanical=log_mechanical, + timeout=start_timeout, + cleanup_on_exit=cleanup_on_exit, + keep_connection_alive=keep_connection_alive, + local=False, + ) + if clear_on_connect: + mechanical.clear() + + # setting ip for the grpc server + if ip != LOCALHOST: # Default local ip is 127.0.0.1 + create_ip_file(ip, os.getcwd()) + + return mechanical + + # verify executable + if exec_file is None: + exec_file = get_mechanical_path(allow_input) + if exec_file is None: # pragma: no cover + raise FileNotFoundError( + "Path to the Mechanical executable file is invalid or cache cannot be loaded. " + "Enter a path manually by specifying a value for the " + "'exec_file' parameter." + ) + else: # verify ansys exists at this location + if not os.path.isfile(exec_file): + raise FileNotFoundError( + f'This path for the Mechanical executable is invalid: "{exec_file}"\n' + "Enter a path manually by specifying a value for the " + "'exec_file' parameter." + ) + + start_parm = { + "exec_file": exec_file, + "batch": batch, + "additional_switches": additional_switches, + "additional_envs": additional_envs, + } + + try: + port = launch_grpc(port=port, verbose=verbose_mechanical, **start_parm) + start_parm["local"] = True + mechanical = Mechanical( + ip=ip, + port=port, + loglevel=loglevel, + log_file=log_file, + log_mechanical=log_mechanical, + timeout=start_timeout, + cleanup_on_exit=cleanup_on_exit, + keep_connection_alive=keep_connection_alive, + **start_parm, + ) + except Exception as exception: # pragma: no cover + # pass + raise exception + + return mechanical + + +def connect_to_mechanical( + ip=None, + port=None, + loglevel="ERROR", + log_file=False, + log_mechanical=None, + connect_timeout=120, + clear_on_connect=False, + cleanup_on_exit=False, + keep_connection_alive=True, +) -> Mechanical: + """Connect to an existing Mechanical server instance. + + Parameters + ---------- + ip : str, optional + IP address for connecting to an existing Mechanical instance. The + IP address defaults to ``"127.0.0.1"``. + port : int, optional + Port to listen on for an existing Mechanical instance. The default is ``None``, + in which case ``10000`` is used. You can override the + default behavior of this parameter with the + ``PYMECHANICAL_PORT=`` environment variable. + loglevel : str, optional + Level of messages to print to the console. + Options are: + + - ``"WARNING"``: Prints only Ansys warning messages. + - ``"ERROR"``: Prints only Ansys error messages. + - ``"INFO"``: Prints all Ansys messages. + + The default is ``WARNING``. + log_file : bool, optional + Whether to copy the messages to a file named ``logs.log``, which is + located where the Python script is executed. The default is ``False``. + log_mechanical : str, optional + Path to the output file on the local disk to write every script + command to. The default is ``None``. However, you might set + ``"log_mechanical='pymechanical_log.txt'"`` to write all commands that are + sent to Mechanical via PyMechanical to this file. You can then use these + commands to run a script within Mechanical without PyMechanical. + connect_timeout : float, optional + Maximum allowable time in seconds to connect to the Mechanical server. + The default is ``120``. + clear_on_connect : bool, optional + Whether to clear the Mechanical instance when connecting. The default is ``False``. + When ``True``, a fresh environment is provided when you connect to Mechanical. + cleanup_on_exit : bool, optional + Whether to exit Mechanical when Python exits. The default is ``False``. + When ``False``, Mechanical is not exited when the garbage for this Mechanical + instance is collected. + keep_connection_alive : bool, optional + Whether to keep the gRPC connection alive by running a background thread + and making dummy calls for remote connections. The default is ``True``. + + Returns + ------- + ansys.mechanical.core.mechanical.Mechanical + Instance of Mechanical. + + Examples + -------- + Connect to an existing Mechanical instance at IP address ``192.168.1.30`` on port + ``50001``.. + + + >>> from ansys.mechanical.core import connect_to_mechanical + >>> pymech = connect_to_mechanical(ip='192.168.1.30', port=50001) + """ + return launch_mechanical( + start_instance=False, + loglevel=loglevel, + log_file=log_file, + log_mechanical=log_mechanical, + start_timeout=connect_timeout, + port=port, + ip=ip, + clear_on_connect=clear_on_connect, + cleanup_on_exit=cleanup_on_exit, + keep_connection_alive=keep_connection_alive, + ) diff --git a/src/ansys/mechanical/core/misc.py b/src/ansys/mechanical/core/misc.py index a388d11d9..37f18e284 100644 --- a/src/ansys/mechanical/core/misc.py +++ b/src/ansys/mechanical/core/misc.py @@ -1,176 +1,176 @@ -# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. -# SPDX-License-Identifier: MIT -# -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -"""Contain miscellaneous functions and methods at the module level.""" - -from functools import wraps -import os -import socket -from threading import Thread - - -def is_windows(): - """Check if the host machine is on Windows. - - Returns - ------- - ``True`` if the host machine is on Windows, ``False`` otherwise. - """ - if os.name == "nt": # pragma: no cover - return True - - return False - - -def get_mechanical_bin(release_version): - """Get the path for the Mechanical executable file based on the release version. - - Parameters - ---------- - release_version: str - Mechanical version using the three-digit format. For example, ``"242"`` for - 2024 R2. - """ - if is_windows(): # pragma: no cover - program_files = os.getenv("PROGRAMFILES", os.path.join("c:\\", "Program Files")) - ans_root = os.getenv( - f"AWP_ROOT{release_version}", - os.path.join(program_files, "ANSYS Inc", f"v{release_version}"), - ) - mechanical_bin = os.path.join(ans_root, "aisol", "bin", "winx64", f"AnsysWBU.exe") - else: - ans_root = os.getenv(f"AWP_ROOT{release_version}", os.path.join("/", "usr", "ansys_inc")) - mechanical_bin = os.path.join(*ans_root, f"v{release_version}", "aisol", f".workbench") - - return mechanical_bin - - -def threaded(func): - """Decorate a function with this decorator to call it using a thread.""" - - @wraps(func) - def wrapper(*args, **kwargs): - name = kwargs.get("name", f"Threaded `{func.__name__}` function") - thread = Thread(target=func, name=name, args=args, kwargs=kwargs) - thread.start() - return thread - - return wrapper - - -def threaded_daemon(func): - """Decorate a function with this decorator to call it using a daemon thread.""" - - @wraps(func) - def wrapper(*args, **kwargs): - name = kwargs.get("name", f"Threaded (with Daemon) `{func.__name__}` function") - thread = Thread(target=func, name=name, args=args, kwargs=kwargs) - thread.daemon = True - thread.start() - return thread - - return wrapper - - -def check_valid_ip(ip): - """Check if the IP address is valid. - - Parameters - ---------- - ip : str - IP address to check. - - """ - if ip.lower() != "localhost": - ip = ip.replace('"', "").replace("'", "") - socket.inet_aton(ip) - - -def check_valid_port(port, lower_bound=1000, high_bound=60000): - """Check if the port is valid. - - Parameters - ---------- - port : int - Port to check. - lower_bound : int, optional - Lowest possible value for the port. The default is ``1000``. - high_bound : int, optional - Highest possible value for the port. The default is ``60000``. - """ - if not isinstance(port, int): - raise ValueError("The 'port' parameter must be an integer.") - - if lower_bound < port < high_bound: - return - else: - raise ValueError(f"'port' values must be between {lower_bound} and {high_bound}.") - - -def check_valid_start_instance(start_instance): - """ - Check if the value obtained from the environmental variable is valid. - - Parameters - ---------- - start_instance : str, bool - Value obtained from the corresponding environment variable. - - Returns - ------- - bool - ``True`` if ``start_instance`` is ``True`` or ``"True"``, - ``False`` otherwise. - - """ - if not isinstance(start_instance, (str, bool)): - raise ValueError("The value for 'start_instance' should be a string or a boolean.") - - if isinstance(start_instance, bool): - return start_instance - - if start_instance.lower() not in ["true", "false"]: - raise ValueError( - "The value for 'start_instance' should be 'True' or 'False' (case insensitive)." - ) - - return start_instance.lower() == "true" - - -def is_float(input_string): - """Check if a string can be converted to a float. - - Parameters - ---------- - input_string : str - String to check. - - Returns - ------- - bool - ``True`` when conversion is possible, ``False`` otherwise. - """ - try: - float(input_string) - return True - except ValueError: - return False +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Contain miscellaneous functions and methods at the module level.""" + +from functools import wraps +import os +import socket +from threading import Thread + + +def is_windows(): + """Check if the host machine is on Windows. + + Returns + ------- + ``True`` if the host machine is on Windows, ``False`` otherwise. + """ + if os.name == "nt": # pragma: no cover + return True + + return False + + +def get_mechanical_bin(release_version): + """Get the path for the Mechanical executable file based on the release version. + + Parameters + ---------- + release_version: str + Mechanical version using the three-digit format. For example, ``"251"`` for + 2025 R1. + """ + if is_windows(): # pragma: no cover + program_files = os.getenv("PROGRAMFILES", os.path.join("c:\\", "Program Files")) + ans_root = os.getenv( + f"AWP_ROOT{release_version}", + os.path.join(program_files, "ANSYS Inc", f"v{release_version}"), + ) + mechanical_bin = os.path.join(ans_root, "aisol", "bin", "winx64", f"AnsysWBU.exe") + else: + ans_root = os.getenv(f"AWP_ROOT{release_version}", os.path.join("/", "usr", "ansys_inc")) + mechanical_bin = os.path.join(*ans_root, f"v{release_version}", "aisol", f".workbench") + + return mechanical_bin + + +def threaded(func): + """Decorate a function with this decorator to call it using a thread.""" + + @wraps(func) + def wrapper(*args, **kwargs): + name = kwargs.get("name", f"Threaded `{func.__name__}` function") + thread = Thread(target=func, name=name, args=args, kwargs=kwargs) + thread.start() + return thread + + return wrapper + + +def threaded_daemon(func): + """Decorate a function with this decorator to call it using a daemon thread.""" + + @wraps(func) + def wrapper(*args, **kwargs): + name = kwargs.get("name", f"Threaded (with Daemon) `{func.__name__}` function") + thread = Thread(target=func, name=name, args=args, kwargs=kwargs) + thread.daemon = True + thread.start() + return thread + + return wrapper + + +def check_valid_ip(ip): + """Check if the IP address is valid. + + Parameters + ---------- + ip : str + IP address to check. + + """ + if ip.lower() != "localhost": + ip = ip.replace('"', "").replace("'", "") + socket.inet_aton(ip) + + +def check_valid_port(port, lower_bound=1000, high_bound=60000): + """Check if the port is valid. + + Parameters + ---------- + port : int + Port to check. + lower_bound : int, optional + Lowest possible value for the port. The default is ``1000``. + high_bound : int, optional + Highest possible value for the port. The default is ``60000``. + """ + if not isinstance(port, int): + raise ValueError("The 'port' parameter must be an integer.") + + if lower_bound < port < high_bound: + return + else: + raise ValueError(f"'port' values must be between {lower_bound} and {high_bound}.") + + +def check_valid_start_instance(start_instance): + """ + Check if the value obtained from the environmental variable is valid. + + Parameters + ---------- + start_instance : str, bool + Value obtained from the corresponding environment variable. + + Returns + ------- + bool + ``True`` if ``start_instance`` is ``True`` or ``"True"``, + ``False`` otherwise. + + """ + if not isinstance(start_instance, (str, bool)): + raise ValueError("The value for 'start_instance' should be a string or a boolean.") + + if isinstance(start_instance, bool): + return start_instance + + if start_instance.lower() not in ["true", "false"]: + raise ValueError( + "The value for 'start_instance' should be 'True' or 'False' (case insensitive)." + ) + + return start_instance.lower() == "true" + + +def is_float(input_string): + """Check if a string can be converted to a float. + + Parameters + ---------- + input_string : str + String to check. + + Returns + ------- + bool + ``True`` when conversion is possible, ``False`` otherwise. + """ + try: + float(input_string) + return True + except ValueError: + return False diff --git a/src/ansys/mechanical/core/pool.py b/src/ansys/mechanical/core/pool.py index e4d480889..fe0e09930 100644 --- a/src/ansys/mechanical/core/pool.py +++ b/src/ansys/mechanical/core/pool.py @@ -1,712 +1,712 @@ -# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. -# SPDX-License-Identifier: MIT -# -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -"""This module is for threaded implementations of the Mechanical interface.""" - -import os -import time -import warnings - -import ansys.platform.instancemanagement as pypim -from ansys.tools.path import version_from_path - -from ansys.mechanical.core.errors import VersionError -from ansys.mechanical.core.mechanical import ( - _HAS_TQDM, - LOG, - MECHANICAL_DEFAULT_PORT, - get_mechanical_path, - launch_mechanical, - port_in_use, -) -from ansys.mechanical.core.misc import threaded, threaded_daemon - -if _HAS_TQDM: - from tqdm import tqdm - - -def available_ports(n_ports, starting_port=MECHANICAL_DEFAULT_PORT): - """Get a list of a given number of available ports starting from a specified port number. - - Parameters - ---------- - n_ports : int - Number of available ports to return. - starting_port: int, option - Number of the port to start the search from. The default is - ``MECHANICAL_DEFAULT_PORT``. - """ - port = starting_port - ports = [] - while port < 65536 and len(ports) < n_ports: - if not port_in_use(port): - ports.append(port) - port += 1 - - if len(ports) < n_ports: - raise RuntimeError( - f"There are not {n_ports} available ports between {starting_port} and 65536." - ) - - return ports - - -class LocalMechanicalPool: - """Create a pool of Mechanical instances. - - Parameters - ---------- - n_instance : int - Number of Mechanical instances to create in the pool. - wait : bool, optional - Whether to wait for the pool to be initialized. The default is - ``True``. When ``False``, the pool starts in the background, in - which case all resources might not be immediately available. - starting_port : int, optional - Starting port for the instances. The default is ``10000``. - progress_bar : bool, optional - Whether to show a progress bar when starting the pool. The default - is ``True``, but the progress bar is not shown when ``wait=False``. - restart_failed : bool, optional - Whether to restart any failed instances in the pool. The default is - ``True``. - **kwargs : dict, optional - Additional keyword arguments. For a list of all keyword - arguments, use the :func:`ansys.mechanical.core.launch_mechanical` - function. If the ``exec_file`` keyword argument is found, it is used to - start instances. PyPIM is used to create instances if the following - conditions are met: - - - PyPIM is configured. - - ``version`` is specified. - - ``exec_file`` is not specified. - - - Examples - -------- - Create a pool of 10 Mechanical instances. - - >>> from ansys.mechanical.core import LocalMechanicalPool - >>> pool = LocalMechanicalPool(10) - Creating Pool: 100%|########| 10/10 [00:01<00:00, 1.43it/s] - - On Windows, create a pool while specifying the Mechanical executable file. - - >>> exec_file = 'C:/Program Files/ANSYS Inc/v242/aisol/bin/winx64/AnsysWBU.exe' - >>> pool = LocalMechanicalPool(10, exec_file=exec_file) - Creating Pool: 100%|########| 10/10 [00:01<00:00, 1.43it/s] - - On Linux, create a pool while specifying the Mechanical executable file. - - >>> exec_file = '/ansys_inc/v242/aisol/.workbench' - >>> pool = LocalMechanicalPool(10, exec_file=exec_file) - Creating Pool: 100%|########| 10/10 [00:01<00:00, 1.43it/s] - - In the PyPIM environment, create a pool. - - >>> pool = LocalMechanicalPool(10, version="242") - Creating Pool: 100%|########| 10/10 [00:01<00:00, 1.43it/s] - - """ - - def __init__( - self, - n_instances, - wait=True, - port=MECHANICAL_DEFAULT_PORT, - progress_bar=True, - restart_failed=True, - **kwargs, - ): - """Initialize several Mechanical instances. - - Parameters - ---------- - n_instance : int - Number of Mechanical instances to initialize. - wait : bool, optional - Whether to wait for the instances to be initialized. The default is - ``True``. When ``False``, the instances start in the background, in - which case all resources might not be immediately available. - port : int, optional - Port for the first Mechanical instance. The default is - ``MECHANICAL_DEFAULT_PORT``. - progress_bar : bool, optional - Whether to display a progress bar when starting the instances. The default - is ``True``, but the progress bar is not shown when ``wait=False``. - restart_failed : bool, optional - Whether to restart any failed instances. The default is ``True``. - **kwargs : dict, optional - Additional keyword arguments. For a list of all additional keyword - arguments, see the :func:`ansys.mechanical.core.launch_mechanical` - function. If the ``exec_file`` keyword argument is found, it is used to - start instances. Instances are created using PyPIM if the following - conditions are met: - - - PyPIM is configured. - - Version is specified/ - - ``exec_file`` is not specified. - """ - self._instances = [] - self._spawn_kwargs = kwargs - self._remote = False - - # verify that mechanical is 2023R2 or newer - exec_file = None - if "exec_file" in kwargs: - exec_file = kwargs["exec_file"] - else: - if pypim.is_configured(): # pragma: no cover - if "version" in kwargs: - version = kwargs["version"] - self._remote = True - else: - raise ValueError("Pypim is configured, but version is not passed.") - else: # get default executable - exec_file = get_mechanical_path() - if exec_file is None: # pragma: no cover - raise FileNotFoundError( - "Path to Mechanical executable file is invalid or cache cannot be loaded. " - "Enter a path manually by specifying a value for the " - "'exec_file' parameter." - ) - - if not self._remote: # pragma: no cover - if version_from_path("mechanical", exec_file) < 232: - raise VersionError("A local Mechanical pool requires Mechanical 2023 R2 or later.") - - ports = None - - if not self._remote: - # grab available ports - ports = available_ports(n_instances, port) - - self._instances = [] - self._active = True # used by pool monitor - - n_instances = int(n_instances) - if n_instances < 2: - raise ValueError("You must request at least two instances to create a pool.") - - pbar = None - if wait and progress_bar: - if not _HAS_TQDM: # pragma: no cover - raise ModuleNotFoundError( - f"To use the keyword argument 'progress_bar', you must have installed " - f"the 'tqdm' package. To avoid this message, you can set 'progress_bar=False'." - ) - - pbar = tqdm(total=n_instances, desc="Creating Pool") - - # initialize a list of dummy instances - self._instances = [None for _ in range(n_instances)] - - if self._remote: # pragma: no cover - threads = [ - self._spawn_mechanical_remote(i, pbar, name=f"Instance {i}") - for i in range(n_instances) - ] - else: - # threaded spawn - threads = [ - self._spawn_mechanical(i, ports[i], pbar, name=f"Instance {i}") - for i in range(n_instances) - ] - if wait: - [thread.join() for thread in threads] - - # check if all clients connected have connected - if len(self) != n_instances: # pragma: no cover - n_connected = len(self) - warnings.warn( - f"Only {n_connected} clients connected out of {n_instances} requested" - ) - if pbar is not None: - pbar.close() - - # monitor pool if requested - if restart_failed: - self._pool_monitor_thread = self._monitor_pool(name="Monitoring_Thread started") - - if not self._remote: - self._verify_unique_ports() - - def _verify_unique_ports(self): - if self._remote: # pragma: no cover - raise RuntimeError("PyPIM is used. Port information is not available.") - - if len(self.ports) != len(self): # pragma: no cover - raise RuntimeError("Mechanical pool has overlapping ports.") - - def map( - self, - func, - iterable=None, - clear_at_start=True, - progress_bar=True, - close_when_finished=False, - timeout=None, - wait=True, - ): - """Run a user-defined function on each Mechanical instance in the pool. - - Parameters - ---------- - func : function - Function with ``mechanical`` as the first argument. The subsequent - arguments should match the number of items in each iterable (if any). - iterable : list, tuple, optional - An iterable containing a set of arguments for the function. - The default is ``None``, in which case the function runs - once on each instance of Mechanical. - clear_at_start : bool, optional - Clear Mechanical at the start of execution. The default is - ``True``. Setting this to ``False`` might lead to instability. - progress_bar : bool, optional - Whether to show a progress bar when running the batch of input - files. The default is ``True``, but the progress bar is not shown - when ``wait=False``. - close_when_finished : bool, optional - Whether to close the instances when the function finishes running - on all instances in the pool. The default is ``False``. - timeout : float, optional - Maximum runtime in seconds for each iteration. The default is - ``None``, in which case there is no timeout. If you specify a - value, each iteration is allowed to run only this number of - seconds. Once this value is exceeded, the batch process is - stopped and treated as a failure. - wait : bool, optional - Whether block execution must wait until the batch process is - complete. The default is ``True``. - - Returns - ------- - list - A list containing the return values for the function. - Failed runs do not return an output. Because return values - are not necessarily in the same order as the iterable, - you might want to add some sort of tracker to the return - of your function. - - Examples - -------- - Run several input files while storing the final routine. Note - how the function to map must use ``mechanical`` as the first argument. - The function can have any number of additional arguments. - - >>> from ansys.mechanical.core import LocalMechanicalPool - >>> pool = LocalMechanicalPool(10) - >>> completed_indices = [] - >>> def function(mechanical, name, script): - # name, script = args - mechanical.clear() - output = mechanical.run_python_script(script) - return name, output - >>> inputs = [("first","2+3"), ("second", "3+4")] - >>> output = pool.map(function, inputs, progress_bar=False, wait=True) - [('first', '5'), ('second', '7')] - """ - # check if any instances are available - if not len(self): # pragma: no cover - # instances could still be spawning... - if not all(v is None for v in self._instances): - raise RuntimeError("No Mechanical instances available.") - - results = [] - - if iterable is not None: - jobs_count = len(iterable) - else: - jobs_count = len(self) - - pbar = None - if progress_bar: - if not _HAS_TQDM: # pragma: no cover - raise ModuleNotFoundError( - "To use the keyword argument 'progress_bar', you must have installed " - "the 'tqdm' package. To avoid this message, you can set 'progress_bar=False'." - ) - - pbar = tqdm(total=jobs_count, desc="Mechanical Running") - - @threaded_daemon - def func_wrapper(obj, func, clear_at_start, timeout, args=None, name=""): - """Expect obj to be an instance of Mechanical.""" - LOG.debug(name) - complete = [False] - - @threaded_daemon - def run(name_local=""): - LOG.debug(name_local) - - if clear_at_start: - obj.clear() - - if args is not None: - if isinstance(args, (tuple, list)): - results.append(func(obj, *args)) - else: - results.append(func(obj, args)) - else: - results.append(func(obj)) - - complete[0] = True - - run_thread = run(name_local=name) - - if timeout: # pragma: no cover - time_start = time.time() - while not complete[0]: - time.sleep(0.01) - if (time.time() - time_start) > timeout: - break - - if not complete[0]: - LOG.error(f"Stopped instance due to a timeout of {timeout} seconds.") - obj.exit() - else: - run_thread.join() - if not complete[0]: # pragma: no cover - LOG.error("Stopped instance because running failed.") - try: - obj.exit() - except Exception as e: - LOG.error(f"Unexpected error while exiting: {e}") - - obj.locked = False - if pbar: - pbar.update(1) - - threads = [] - if iterable is not None: - for args in iterable: - # grab the next available instance of mechanical - instance, i = self.next_available(return_index=True) - instance.locked = True - - threads.append( - func_wrapper( - instance, func, clear_at_start, timeout, args, name=f"Map_Thread{i}" - ) - ) - else: # simply apply to all - for instance in self._instances: - if instance: - threads.append( - func_wrapper(instance, func, clear_at_start, timeout, name=f"Map_Thread") - ) - - if close_when_finished: # pragma: no cover - # start closing any instances that are not in execution - while not all(v is None for v in self._instances): - # grab the next available instance of mechanical and close it - instance, i = self.next_available(return_index=True) - self._instances[i] = None - - try: - instance.exit() - except Exception as error: # pragma: no cover - LOG.error(f"Failed to close instance : str{error}.") - else: - # wait for all threads to complete - if wait: - [thread.join() for thread in threads] - - return results - - def run_batch( - self, - files, - clear_at_start=True, - progress_bar=True, - close_when_finished=False, - timeout=None, - wait=True, - ): - """Run a batch of input files on the Mechanical instances in the pool. - - Parameters - ---------- - files : list - List of input files. - clear_at_start : bool, optional - Whether to clear Mechanical when execution starts. The default is - ``True``. Setting this parameter to ``False`` might lead to - instability. - progress_bar : bool, optional - Whether to show a progress bar when running the batch of input - files. The default is ``True``, but the progress bar is not shown - when ``wait=False``. - close_when_finished : bool, optional - Whether to close the instances when running the batch - of input files is finished. The default is ``False``. - timeout : float, optional - Maximum runtime in seconds for each iteration. The default is - ``None``, in which case there is no timeout. If you specify a - value, each iteration is allowed to run only this number of - seconds. Once this value is exceeded, the batch process is stopped - and treated as a failure. - wait : bool, optional - Whether block execution must wait until the batch process is complete. - The default is ``True``. - - Returns - ------- - list - List of text outputs from Mechanical for each batch run. The outputs - are not necessarily listed in the order of the inputs. Failed runs do - not return an output. Because the return outputs are not - necessarily in the same order as ``iterable``, you might - want to add some sort of tracker or note within the input files. - - Examples - -------- - Run 20 verification files on the pool. - - >>> files = [f"test{index}.py" for index in range(1, 21)] - >>> outputs = pool.run_batch(files) - >>> len(outputs) - 20 - """ - # check all files exist before running - for filename in files: - if not os.path.isfile(filename): - raise FileNotFoundError("Unable to locate file %s" % filename) - - def run_file(mechanical, input_file): - if clear_at_start: - mechanical.clear() - return mechanical.run_python_script_from_file(input_file) - - return self.map( - run_file, - files, - progress_bar=progress_bar, - close_when_finished=close_when_finished, - timeout=timeout, - wait=wait, - ) - - def next_available(self, return_index=False): - """Wait until a Mechanical instance is available and return this instance. - - Parameters - ---------- - return_index : bool, optional - Whether to return the index along with the instance. The default - is ``False``. - - Returns - ------- - pymechanical.Mechanical - Instance of Mechanical. - - int - Index within the pool of Mechanical instances. This index - is not returned by default. - - Examples - -------- - >>> mechanical = pool.next_available() - >>> mechanical - Ansys Mechanical [Ansys Mechanical Enterprise] - Product Version:242 - Software build date: 06/03/2024 14:47:58 - """ - # loop until the next instance is available - while True: - for i, instance in enumerate(self._instances): - # if encounter placeholder - if not instance: # pragma: no cover - continue - - if not instance.locked and not instance._exited: - # any instance that is not running or exited - # should be available - if not instance.busy: - # double check that this instance is alive: - try: - instance._make_dummy_call() - except: # pragma: no cover - instance.exit() - continue - - if return_index: - return instance, i - else: - return instance - # review - not needed - # else: - # instance._exited = True - - def __del__(self): - """Clean up when complete.""" - print("pool:Automatic clean up.") - self.exit() - - def exit(self, block=False): - """Exit all Mechanical instances in the pool. - - Parameters - ---------- - block : bool, optional - Whether to wait until all processes close before exiting - all instances in the pool. The default is ``False``. - - Examples - -------- - >>> pool.exit() - """ - self._active = False # Stops any active instance restart - - @threaded - def threaded_exit(index, instance_local): - if instance_local: - try: - instance_local.exit() - except Exception as e: # pragma: no cover - LOG.error(f"Error while exiting instance {str(instance_local)}: {str(e)}") - self._instances[index] = None - LOG.debug(f"Exited instance: {str(instance_local)}") - - threads = [] - for i, instance in enumerate(self): - threads.append(threaded_exit(i, instance)) - - if block: - [thread.join() for thread in threads] - - def __len__(self): - """Get the number of instances in the pool.""" - count = 0 - for instance in self._instances: - if instance: - if not instance._exited: - count += 1 - return count - - def __getitem__(self, key): - """Get an instance by an index.""" - return self._instances[key] - - def __iter__(self): - """Iterate through active instances.""" - for instance in self._instances: - if instance: - yield instance - - @threaded_daemon - def _spawn_mechanical(self, index, port=None, pbar=None, name=""): - """Spawn a Mechanical instance at an index. - - Parameters - ---------- - index : int - Index to spawn the instance on. - port : int, optional - Port for the instance. The default is ``None``. - pbar : - The default is ``None``. - name : str, optional - Name for the instance. The default is ``""``. - """ - LOG.debug(name) - self._instances[index] = launch_mechanical(port=port, **self._spawn_kwargs) - # LOG.debug("Spawned instance %d. Name '%s'", index, name) - if pbar is not None: - pbar.update(1) - - @threaded_daemon - def _spawn_mechanical_remote(self, index, pbar=None, name=""): # pragma: no cover - """Spawn a Mechanical instance at an index. - - Parameters - ---------- - index : int - Index to spawn the instance on. - pbar : - The default is ``None``. - name : str, optional - Name for the instance. The default is ``""``. - - """ - LOG.debug(name) - self._instances[index] = launch_mechanical(**self._spawn_kwargs) - # LOG.debug("Spawned instance %d. Name '%s'", index, name) - if pbar is not None: - pbar.update(1) - - @threaded_daemon - def _monitor_pool(self, refresh=1.0, name=""): - """Check for instances within a pool that have exited (failed) and restart them. - - Parameters - ---------- - refresh : float, optional - The default is ``1.0``. - name : str, optional - Name for the instance. The default is ``""``. - """ - LOG.debug(name) - while self._active: - for index, instance in enumerate(self._instances): - # encountered placeholder - if not instance: # pragma: no cover - continue - if instance._exited: # pragma: no cover - try: - if self._remote: - LOG.debug( - f"Restarting a Mechanical remote instance for index : {index}." - ) - self._spawn_mechanical_remote(index, name=f"Instance {index}").join() - else: - # use the next port after the current available port - port = max(self.ports) + 1 - LOG.debug( - f"Restarting a Mechanical instance for index : " - f"{index} on port: {port}." - ) - self._spawn_mechanical( - index, port=port, name=f"Instance {index}" - ).join() - except Exception as e: - LOG.error(e, exc_info=True) - time.sleep(refresh) - - @property - def ports(self): - """Get a list of the ports that are used. - - Examples - -------- - Get the list of ports used by the pool of Mechanical instances. - - >>> pool.ports - [10001, 10002] - - """ - return [inst._port for inst in self if inst is not None] - - def __str__(self): - """Get the string representation of this object.""" - return "Mechanical pool with %d active instances" % len(self) +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""This module is for threaded implementations of the Mechanical interface.""" + +import os +import time +import warnings + +import ansys.platform.instancemanagement as pypim +from ansys.tools.path import version_from_path + +from ansys.mechanical.core.errors import VersionError +from ansys.mechanical.core.mechanical import ( + _HAS_TQDM, + LOG, + MECHANICAL_DEFAULT_PORT, + get_mechanical_path, + launch_mechanical, + port_in_use, +) +from ansys.mechanical.core.misc import threaded, threaded_daemon + +if _HAS_TQDM: + from tqdm import tqdm + + +def available_ports(n_ports, starting_port=MECHANICAL_DEFAULT_PORT): + """Get a list of a given number of available ports starting from a specified port number. + + Parameters + ---------- + n_ports : int + Number of available ports to return. + starting_port: int, option + Number of the port to start the search from. The default is + ``MECHANICAL_DEFAULT_PORT``. + """ + port = starting_port + ports = [] + while port < 65536 and len(ports) < n_ports: + if not port_in_use(port): + ports.append(port) + port += 1 + + if len(ports) < n_ports: + raise RuntimeError( + f"There are not {n_ports} available ports between {starting_port} and 65536." + ) + + return ports + + +class LocalMechanicalPool: + """Create a pool of Mechanical instances. + + Parameters + ---------- + n_instance : int + Number of Mechanical instances to create in the pool. + wait : bool, optional + Whether to wait for the pool to be initialized. The default is + ``True``. When ``False``, the pool starts in the background, in + which case all resources might not be immediately available. + starting_port : int, optional + Starting port for the instances. The default is ``10000``. + progress_bar : bool, optional + Whether to show a progress bar when starting the pool. The default + is ``True``, but the progress bar is not shown when ``wait=False``. + restart_failed : bool, optional + Whether to restart any failed instances in the pool. The default is + ``True``. + **kwargs : dict, optional + Additional keyword arguments. For a list of all keyword + arguments, use the :func:`ansys.mechanical.core.launch_mechanical` + function. If the ``exec_file`` keyword argument is found, it is used to + start instances. PyPIM is used to create instances if the following + conditions are met: + + - PyPIM is configured. + - ``version`` is specified. + - ``exec_file`` is not specified. + + + Examples + -------- + Create a pool of 10 Mechanical instances. + + >>> from ansys.mechanical.core import LocalMechanicalPool + >>> pool = LocalMechanicalPool(10) + Creating Pool: 100%|########| 10/10 [00:01<00:00, 1.43it/s] + + On Windows, create a pool while specifying the Mechanical executable file. + + >>> exec_file = 'C:/Program Files/ANSYS Inc/v251/aisol/bin/winx64/AnsysWBU.exe' + >>> pool = LocalMechanicalPool(10, exec_file=exec_file) + Creating Pool: 100%|########| 10/10 [00:01<00:00, 1.43it/s] + + On Linux, create a pool while specifying the Mechanical executable file. + + >>> exec_file = '/ansys_inc/v251/aisol/.workbench' + >>> pool = LocalMechanicalPool(10, exec_file=exec_file) + Creating Pool: 100%|########| 10/10 [00:01<00:00, 1.43it/s] + + In the PyPIM environment, create a pool. + + >>> pool = LocalMechanicalPool(10, version="251") + Creating Pool: 100%|########| 10/10 [00:01<00:00, 1.43it/s] + + """ + + def __init__( + self, + n_instances, + wait=True, + port=MECHANICAL_DEFAULT_PORT, + progress_bar=True, + restart_failed=True, + **kwargs, + ): + """Initialize several Mechanical instances. + + Parameters + ---------- + n_instance : int + Number of Mechanical instances to initialize. + wait : bool, optional + Whether to wait for the instances to be initialized. The default is + ``True``. When ``False``, the instances start in the background, in + which case all resources might not be immediately available. + port : int, optional + Port for the first Mechanical instance. The default is + ``MECHANICAL_DEFAULT_PORT``. + progress_bar : bool, optional + Whether to display a progress bar when starting the instances. The default + is ``True``, but the progress bar is not shown when ``wait=False``. + restart_failed : bool, optional + Whether to restart any failed instances. The default is ``True``. + **kwargs : dict, optional + Additional keyword arguments. For a list of all additional keyword + arguments, see the :func:`ansys.mechanical.core.launch_mechanical` + function. If the ``exec_file`` keyword argument is found, it is used to + start instances. Instances are created using PyPIM if the following + conditions are met: + + - PyPIM is configured. + - Version is specified/ + - ``exec_file`` is not specified. + """ + self._instances = [] + self._spawn_kwargs = kwargs + self._remote = False + + # Verify that Mechanical is 2023R2 or newer + exec_file = None + if "exec_file" in kwargs: + exec_file = kwargs["exec_file"] + else: + if pypim.is_configured(): # pragma: no cover + if "version" in kwargs: + version = kwargs["version"] + self._remote = True + else: + raise ValueError("Pypim is configured, but version is not passed.") + else: # get default executable + exec_file = get_mechanical_path() + if exec_file is None: # pragma: no cover + raise FileNotFoundError( + "Path to Mechanical executable file is invalid or cache cannot be loaded. " + "Enter a path manually by specifying a value for the " + "'exec_file' parameter." + ) + + if not self._remote: # pragma: no cover + if version_from_path("mechanical", exec_file) < 232: + raise VersionError("A local Mechanical pool requires Mechanical 2023 R2 or later.") + + ports = None + + if not self._remote: + # grab available ports + ports = available_ports(n_instances, port) + + self._instances = [] + self._active = True # used by pool monitor + + n_instances = int(n_instances) + if n_instances < 2: + raise ValueError("You must request at least two instances to create a pool.") + + pbar = None + if wait and progress_bar: + if not _HAS_TQDM: # pragma: no cover + raise ModuleNotFoundError( + f"To use the keyword argument 'progress_bar', you must have installed " + f"the 'tqdm' package. To avoid this message, you can set 'progress_bar=False'." + ) + + pbar = tqdm(total=n_instances, desc="Creating Pool") + + # initialize a list of dummy instances + self._instances = [None for _ in range(n_instances)] + + if self._remote: # pragma: no cover + threads = [ + self._spawn_mechanical_remote(i, pbar, name=f"Instance {i}") + for i in range(n_instances) + ] + else: + # threaded spawn + threads = [ + self._spawn_mechanical(i, ports[i], pbar, name=f"Instance {i}") + for i in range(n_instances) + ] + if wait: + [thread.join() for thread in threads] + + # check if all clients connected have connected + if len(self) != n_instances: # pragma: no cover + n_connected = len(self) + warnings.warn( + f"Only {n_connected} clients connected out of {n_instances} requested" + ) + if pbar is not None: + pbar.close() + + # monitor pool if requested + if restart_failed: + self._pool_monitor_thread = self._monitor_pool(name="Monitoring_Thread started") + + if not self._remote: + self._verify_unique_ports() + + def _verify_unique_ports(self): + if self._remote: # pragma: no cover + raise RuntimeError("PyPIM is used. Port information is not available.") + + if len(self.ports) != len(self): # pragma: no cover + raise RuntimeError("Mechanical pool has overlapping ports.") + + def map( + self, + func, + iterable=None, + clear_at_start=True, + progress_bar=True, + close_when_finished=False, + timeout=None, + wait=True, + ): + """Run a user-defined function on each Mechanical instance in the pool. + + Parameters + ---------- + func : function + Function with ``mechanical`` as the first argument. The subsequent + arguments should match the number of items in each iterable (if any). + iterable : list, tuple, optional + An iterable containing a set of arguments for the function. + The default is ``None``, in which case the function runs + once on each instance of Mechanical. + clear_at_start : bool, optional + Clear Mechanical at the start of execution. The default is + ``True``. Setting this to ``False`` might lead to instability. + progress_bar : bool, optional + Whether to show a progress bar when running the batch of input + files. The default is ``True``, but the progress bar is not shown + when ``wait=False``. + close_when_finished : bool, optional + Whether to close the instances when the function finishes running + on all instances in the pool. The default is ``False``. + timeout : float, optional + Maximum runtime in seconds for each iteration. The default is + ``None``, in which case there is no timeout. If you specify a + value, each iteration is allowed to run only this number of + seconds. Once this value is exceeded, the batch process is + stopped and treated as a failure. + wait : bool, optional + Whether block execution must wait until the batch process is + complete. The default is ``True``. + + Returns + ------- + list + A list containing the return values for the function. + Failed runs do not return an output. Because return values + are not necessarily in the same order as the iterable, + you might want to add some sort of tracker to the return + of your function. + + Examples + -------- + Run several input files while storing the final routine. Note + how the function to map must use ``mechanical`` as the first argument. + The function can have any number of additional arguments. + + >>> from ansys.mechanical.core import LocalMechanicalPool + >>> pool = LocalMechanicalPool(10) + >>> completed_indices = [] + >>> def function(mechanical, name, script): + # name, script = args + mechanical.clear() + output = mechanical.run_python_script(script) + return name, output + >>> inputs = [("first","2+3"), ("second", "3+4")] + >>> output = pool.map(function, inputs, progress_bar=False, wait=True) + [('first', '5'), ('second', '7')] + """ + # check if any instances are available + if not len(self): # pragma: no cover + # instances could still be spawning... + if not all(v is None for v in self._instances): + raise RuntimeError("No Mechanical instances available.") + + results = [] + + if iterable is not None: + jobs_count = len(iterable) + else: + jobs_count = len(self) + + pbar = None + if progress_bar: + if not _HAS_TQDM: # pragma: no cover + raise ModuleNotFoundError( + "To use the keyword argument 'progress_bar', you must have installed " + "the 'tqdm' package. To avoid this message, you can set 'progress_bar=False'." + ) + + pbar = tqdm(total=jobs_count, desc="Mechanical Running") + + @threaded_daemon + def func_wrapper(obj, func, clear_at_start, timeout, args=None, name=""): + """Expect obj to be an instance of Mechanical.""" + LOG.debug(name) + complete = [False] + + @threaded_daemon + def run(name_local=""): + LOG.debug(name_local) + + if clear_at_start: + obj.clear() + + if args is not None: + if isinstance(args, (tuple, list)): + results.append(func(obj, *args)) + else: + results.append(func(obj, args)) + else: + results.append(func(obj)) + + complete[0] = True + + run_thread = run(name_local=name) + + if timeout: # pragma: no cover + time_start = time.time() + while not complete[0]: + time.sleep(0.01) + if (time.time() - time_start) > timeout: + break + + if not complete[0]: + LOG.error(f"Stopped instance due to a timeout of {timeout} seconds.") + obj.exit() + else: + run_thread.join() + if not complete[0]: # pragma: no cover + LOG.error("Stopped instance because running failed.") + try: + obj.exit() + except Exception as e: + LOG.error(f"Unexpected error while exiting: {e}") + + obj.locked = False + if pbar: + pbar.update(1) + + threads = [] + if iterable is not None: + for args in iterable: + # grab the next available instance of mechanical + instance, i = self.next_available(return_index=True) + instance.locked = True + + threads.append( + func_wrapper( + instance, func, clear_at_start, timeout, args, name=f"Map_Thread{i}" + ) + ) + else: # simply apply to all + for instance in self._instances: + if instance: + threads.append( + func_wrapper(instance, func, clear_at_start, timeout, name=f"Map_Thread") + ) + + if close_when_finished: # pragma: no cover + # start closing any instances that are not in execution + while not all(v is None for v in self._instances): + # grab the next available instance of mechanical and close it + instance, i = self.next_available(return_index=True) + self._instances[i] = None + + try: + instance.exit() + except Exception as error: # pragma: no cover + LOG.error(f"Failed to close instance : str{error}.") + else: + # wait for all threads to complete + if wait: + [thread.join() for thread in threads] + + return results + + def run_batch( + self, + files, + clear_at_start=True, + progress_bar=True, + close_when_finished=False, + timeout=None, + wait=True, + ): + """Run a batch of input files on the Mechanical instances in the pool. + + Parameters + ---------- + files : list + List of input files. + clear_at_start : bool, optional + Whether to clear Mechanical when execution starts. The default is + ``True``. Setting this parameter to ``False`` might lead to + instability. + progress_bar : bool, optional + Whether to show a progress bar when running the batch of input + files. The default is ``True``, but the progress bar is not shown + when ``wait=False``. + close_when_finished : bool, optional + Whether to close the instances when running the batch + of input files is finished. The default is ``False``. + timeout : float, optional + Maximum runtime in seconds for each iteration. The default is + ``None``, in which case there is no timeout. If you specify a + value, each iteration is allowed to run only this number of + seconds. Once this value is exceeded, the batch process is stopped + and treated as a failure. + wait : bool, optional + Whether block execution must wait until the batch process is complete. + The default is ``True``. + + Returns + ------- + list + List of text outputs from Mechanical for each batch run. The outputs + are not necessarily listed in the order of the inputs. Failed runs do + not return an output. Because the return outputs are not + necessarily in the same order as ``iterable``, you might + want to add some sort of tracker or note within the input files. + + Examples + -------- + Run 20 verification files on the pool. + + >>> files = [f"test{index}.py" for index in range(1, 21)] + >>> outputs = pool.run_batch(files) + >>> len(outputs) + 20 + """ + # check all files exist before running + for filename in files: + if not os.path.isfile(filename): + raise FileNotFoundError("Unable to locate file %s" % filename) + + def run_file(mechanical, input_file): + if clear_at_start: + mechanical.clear() + return mechanical.run_python_script_from_file(input_file) + + return self.map( + run_file, + files, + progress_bar=progress_bar, + close_when_finished=close_when_finished, + timeout=timeout, + wait=wait, + ) + + def next_available(self, return_index=False): + """Wait until a Mechanical instance is available and return this instance. + + Parameters + ---------- + return_index : bool, optional + Whether to return the index along with the instance. The default + is ``False``. + + Returns + ------- + pymechanical.Mechanical + Instance of Mechanical. + + int + Index within the pool of Mechanical instances. This index + is not returned by default. + + Examples + -------- + >>> mechanical = pool.next_available() + >>> mechanical + Ansys Mechanical [Ansys Mechanical Enterprise] + Product Version:251 + Software build date: 11/27/2024 09:34:44 + """ + # loop until the next instance is available + while True: + for i, instance in enumerate(self._instances): + # if encounter placeholder + if not instance: # pragma: no cover + continue + + if not instance.locked and not instance._exited: + # any instance that is not running or exited + # should be available + if not instance.busy: + # double check that this instance is alive: + try: + instance._make_dummy_call() + except: # pragma: no cover + instance.exit() + continue + + if return_index: + return instance, i + else: + return instance + # review - not needed + # else: + # instance._exited = True + + def __del__(self): + """Clean up when complete.""" + print("pool:Automatic clean up.") + self.exit() + + def exit(self, block=False): + """Exit all Mechanical instances in the pool. + + Parameters + ---------- + block : bool, optional + Whether to wait until all processes close before exiting + all instances in the pool. The default is ``False``. + + Examples + -------- + >>> pool.exit() + """ + self._active = False # Stops any active instance restart + + @threaded + def threaded_exit(index, instance_local): + if instance_local: + try: + instance_local.exit() + except Exception as e: # pragma: no cover + LOG.error(f"Error while exiting instance {str(instance_local)}: {str(e)}") + self._instances[index] = None + LOG.debug(f"Exited instance: {str(instance_local)}") + + threads = [] + for i, instance in enumerate(self): + threads.append(threaded_exit(i, instance)) + + if block: + [thread.join() for thread in threads] + + def __len__(self): + """Get the number of instances in the pool.""" + count = 0 + for instance in self._instances: + if instance: + if not instance._exited: + count += 1 + return count + + def __getitem__(self, key): + """Get an instance by an index.""" + return self._instances[key] + + def __iter__(self): + """Iterate through active instances.""" + for instance in self._instances: + if instance: + yield instance + + @threaded_daemon + def _spawn_mechanical(self, index, port=None, pbar=None, name=""): + """Spawn a Mechanical instance at an index. + + Parameters + ---------- + index : int + Index to spawn the instance on. + port : int, optional + Port for the instance. The default is ``None``. + pbar : + The default is ``None``. + name : str, optional + Name for the instance. The default is ``""``. + """ + LOG.debug(name) + self._instances[index] = launch_mechanical(port=port, **self._spawn_kwargs) + # LOG.debug("Spawned instance %d. Name '%s'", index, name) + if pbar is not None: + pbar.update(1) + + @threaded_daemon + def _spawn_mechanical_remote(self, index, pbar=None, name=""): # pragma: no cover + """Spawn a Mechanical instance at an index. + + Parameters + ---------- + index : int + Index to spawn the instance on. + pbar : + The default is ``None``. + name : str, optional + Name for the instance. The default is ``""``. + + """ + LOG.debug(name) + self._instances[index] = launch_mechanical(**self._spawn_kwargs) + # LOG.debug("Spawned instance %d. Name '%s'", index, name) + if pbar is not None: + pbar.update(1) + + @threaded_daemon + def _monitor_pool(self, refresh=1.0, name=""): + """Check for instances within a pool that have exited (failed) and restart them. + + Parameters + ---------- + refresh : float, optional + The default is ``1.0``. + name : str, optional + Name for the instance. The default is ``""``. + """ + LOG.debug(name) + while self._active: + for index, instance in enumerate(self._instances): + # encountered placeholder + if not instance: # pragma: no cover + continue + if instance._exited: # pragma: no cover + try: + if self._remote: + LOG.debug( + f"Restarting a Mechanical remote instance for index : {index}." + ) + self._spawn_mechanical_remote(index, name=f"Instance {index}").join() + else: + # use the next port after the current available port + port = max(self.ports) + 1 + LOG.debug( + f"Restarting a Mechanical instance for index : " + f"{index} on port: {port}." + ) + self._spawn_mechanical( + index, port=port, name=f"Instance {index}" + ).join() + except Exception as e: + LOG.error(e, exc_info=True) + time.sleep(refresh) + + @property + def ports(self): + """Get a list of the ports that are used. + + Examples + -------- + Get the list of ports used by the pool of Mechanical instances. + + >>> pool.ports + [10001, 10002] + + """ + return [inst._port for inst in self if inst is not None] + + def __str__(self): + """Get the string representation of this object.""" + return "Mechanical pool with %d active instances" % len(self) diff --git a/src/ansys/mechanical/core/run.py b/src/ansys/mechanical/core/run.py index ced846726..70bb3569a 100644 --- a/src/ansys/mechanical/core/run.py +++ b/src/ansys/mechanical/core/run.py @@ -1,321 +1,321 @@ -# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. -# SPDX-License-Identifier: MIT -# -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -"""Convenience CLI to run mechanical.""" - -import asyncio -from asyncio.subprocess import PIPE -import os -import sys -import typing -import warnings - -import ansys.tools.path as atp -import click - -from ansys.mechanical.core.embedding.appdata import UniqueUserProfile -from ansys.mechanical.core.feature_flags import get_command_line_arguments, get_feature_flag_names - -DRY_RUN = False -"""Dry run constant.""" - -# TODO - add logging options (reuse env var based logging initialization) -# TODO - add timeout - - -async def _read_and_display(cmd, env, do_display: bool): - """Read command's stdout and stderr and display them as they are processed.""" - # start process - process = await asyncio.create_subprocess_exec(*cmd, stdout=PIPE, stderr=PIPE, env=env) - # read child's stdout/stderr concurrently - stdout, stderr = [], [] # stderr, stdout buffers - tasks = { - asyncio.Task(process.stdout.readline()): (stdout, process.stdout, sys.stdout.buffer), - asyncio.Task(process.stderr.readline()): (stderr, process.stderr, sys.stderr.buffer), - } - while tasks: - done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) - if not done: - raise RuntimeError("Subprocess read failed: No tasks completed.") - for future in done: - buf, stream, display = tasks.pop(future) - line = future.result() - if line: # not EOF - buf.append(line) # save for later - if do_display: - display.write(line) # display in terminal - # schedule to read the next line - tasks[asyncio.Task(stream.readline())] = buf, stream, display - - # wait for the process to exit - rc = await process.wait() - return rc, process, b"".join(stdout), b"".join(stderr) - - -def _run(args, env, check=False, display=False): - if os.name == "nt": - loop = asyncio.ProactorEventLoop() # for subprocess' pipes on Windows - asyncio.set_event_loop(loop) - else: - loop = asyncio.get_event_loop() - try: - rc, process, *output = loop.run_until_complete(_read_and_display(args, env, display)) - if rc and check: - sys.exit("child failed with '{}' exit code".format(rc)) - finally: - if os.name == "nt": - loop.close() - return process, output - - -def _cli_impl( - project_file: str = None, - port: int = 0, - debug: bool = False, - input_script: str = None, - script_args: str = None, - exe: str = None, - version: int = None, - graphical: bool = False, - show_welcome_screen: bool = False, - private_appdata: bool = False, - exit: bool = False, - features: str = None, -): - if project_file and input_script: - raise Exception("Cannot open a project file *and* run a script.") - - if (not graphical) and project_file: - raise Exception("Cannot open a project file in batch mode.") - - if port: - if project_file: - raise Exception("Cannot open in server mode with a project file.") - if input_script: - raise Exception("Cannot open in server mode with an input script.") - - if not input_script and script_args: - raise Exception("Cannot add script arguments without an input script.") - - if script_args: - if '"' in script_args: - raise Exception( - "Cannot have double quotes around individual arguments in the --script-args string." - ) - - # If the input_script and port are missing in batch mode, raise an exception - if (not graphical) and (input_script is None) and (not port): - raise Exception("An input script, -i, or port, --port, are required in batch mode.") - - args = [exe, "-DSApplet"] - if (not graphical) or (not show_welcome_screen): - args.append("-AppModeMech") - - if version < 232: - args.append("-nosplash") - args.append("-notabctrl") - - if not graphical: - args.append("-b") - - env: typing.Dict[str, str] = os.environ.copy() - if debug: - env["WBDEBUG_STOP"] = "1" - - if port: - args.append("-grpc") - args.append(str(port)) - - if project_file: - args.append("-file") - args.append(project_file) - - if input_script: - args.append("-script") - args.append(input_script) - - if script_args: - args.append("-ScriptArgs") - args.append(f'"{script_args}"') - - if (not graphical) and input_script: - exit = True - if version < 241: - warnings.warn( - "Please ensure ExtAPI.Application.Close() is at the end of your script. " - "Without this command, Batch mode will not terminate.", - stacklevel=2, - ) - - if exit and input_script and version >= 241: - args.append("-x") - - profile: UniqueUserProfile = None - if private_appdata: - new_profile_name = f"Mechanical-{os.getpid()}" - profile = UniqueUserProfile(new_profile_name, dry_run=DRY_RUN) - profile.update_environment(env) - - if not DRY_RUN: - version_name = atp.SUPPORTED_ANSYS_VERSIONS[version] - if graphical: - mode = "Graphical" - else: - mode = "Batch" - print(f"Starting Ansys Mechanical version {version_name} in {mode} mode...") - if port: - # TODO - Mechanical doesn't write anything to the stdout in grpc mode - # when logging is off.. Ideally we let Mechanical write it, so - # the user only sees the message when the server is ready. - print(f"Serving on port {port}") - - if features is not None: - args.extend(get_command_line_arguments(features.split(";"))) - - if DRY_RUN: - return args, env - else: - _run(args, env, False, True) - - if private_appdata: - profile.cleanup() - - -@click.command() -@click.help_option("--help", "-h") -@click.option( - "-p", - "--project-file", - default=None, - help="Opens Mechanical project file (.mechdb). Cannot be mixed with -i", -) -@click.option( - "--private-appdata", - default=None, - is_flag=True, - help="Make the appdata folder private.\ - This enables you to run parallel instances of Mechanical.", -) -@click.option( - "--port", - type=int, - help="Start mechanical in server mode with the given port number", -) -@click.option( - "--features", - type=str, - default=None, - help=f"Beta feature flags to set, as a semicolon delimited list.\ - Options: {get_feature_flag_names()}", -) -@click.option( - "-i", - "--input-script", - default=None, - help="Name of the input Python script. Cannot be mixed with -p", -) -@click.option( - "--script-args", - default=None, - help='Arguments to pass into the --input-script, -i. \ -Write the arguments as a string, with each argument \ -separated by a comma. For example, --script-args "arg1,arg2" \ -This can only be used with the --input-script argument.', -) -@click.option( - "--exit", - is_flag=True, - default=None, - help="Exit the application after running an input script. \ -You can only use this command with --input-script argument (-i). \ -The command defaults to true you are not running the application in graphical mode. \ -The ``exit`` command is only supported in version 2024 R1 or later.", -) -@click.option( - "-s", - "--show-welcome-screen", - is_flag=True, - default=False, - help="Show the welcome screen, where you can select the file to open.\ - Only affects graphical mode", -) -@click.option( - "--debug", - is_flag=True, - default=False, - help="Show a debug dialog right when the process starts.", -) -@click.option( - "-r", - "--revision", - default=None, - type=int, - help='Ansys Revision number, e.g. "242" or "241". If none is specified\ -, uses the default from ansys-tools-path', -) -@click.option( - "-g", - "--graphical", - is_flag=True, - default=False, - help="Graphical mode", -) -def cli( - project_file: str, - port: int, - debug: bool, - input_script: str, - script_args: str, - revision: int, - graphical: bool, - show_welcome_screen: bool, - private_appdata: bool, - exit: bool, - features: str, -): - """CLI tool to run mechanical. - - USAGE: - - The following example demonstrates the main use of this tool: - - $ ansys-mechanical -r 242 -g - - Starting Ansys Mechanical version 2024R2 in graphical mode... - """ - exe = atp.get_mechanical_path(allow_input=False, version=revision) - version = atp.version_from_path("mechanical", exe) - - return _cli_impl( - project_file, - port, - debug, - input_script, - script_args, - exe, - version, - graphical, - show_welcome_screen, - private_appdata, - exit, - features, - ) +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Convenience CLI to run mechanical.""" + +import asyncio +from asyncio.subprocess import PIPE +import os +import sys +import typing +import warnings + +import ansys.tools.path as atp +import click + +from ansys.mechanical.core.embedding.appdata import UniqueUserProfile +from ansys.mechanical.core.feature_flags import get_command_line_arguments, get_feature_flag_names + +DRY_RUN = False +"""Dry run constant.""" + +# TODO - add logging options (reuse env var based logging initialization) +# TODO - add timeout + + +async def _read_and_display(cmd, env, do_display: bool): + """Read command's stdout and stderr and display them as they are processed.""" + # start process + process = await asyncio.create_subprocess_exec(*cmd, stdout=PIPE, stderr=PIPE, env=env) + # read child's stdout/stderr concurrently + stdout, stderr = [], [] # stderr, stdout buffers + tasks = { + asyncio.Task(process.stdout.readline()): (stdout, process.stdout, sys.stdout.buffer), + asyncio.Task(process.stderr.readline()): (stderr, process.stderr, sys.stderr.buffer), + } + while tasks: + done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) + if not done: + raise RuntimeError("Subprocess read failed: No tasks completed.") + for future in done: + buf, stream, display = tasks.pop(future) + line = future.result() + if line: # not EOF + buf.append(line) # save for later + if do_display: + display.write(line) # display in terminal + # schedule to read the next line + tasks[asyncio.Task(stream.readline())] = buf, stream, display + + # wait for the process to exit + rc = await process.wait() + return rc, process, b"".join(stdout), b"".join(stderr) + + +def _run(args, env, check=False, display=False): + if os.name == "nt": + loop = asyncio.ProactorEventLoop() # for subprocess' pipes on Windows + asyncio.set_event_loop(loop) + else: + loop = asyncio.get_event_loop() + try: + rc, process, *output = loop.run_until_complete(_read_and_display(args, env, display)) + if rc and check: + sys.exit("child failed with '{}' exit code".format(rc)) + finally: + if os.name == "nt": + loop.close() + return process, output + + +def _cli_impl( + project_file: str = None, + port: int = 0, + debug: bool = False, + input_script: str = None, + script_args: str = None, + exe: str = None, + version: int = None, + graphical: bool = False, + show_welcome_screen: bool = False, + private_appdata: bool = False, + exit: bool = False, + features: str = None, +): + if project_file and input_script: + raise Exception("Cannot open a project file *and* run a script.") + + if (not graphical) and project_file: + raise Exception("Cannot open a project file in batch mode.") + + if port: + if project_file: + raise Exception("Cannot open in server mode with a project file.") + if input_script: + raise Exception("Cannot open in server mode with an input script.") + + if not input_script and script_args: + raise Exception("Cannot add script arguments without an input script.") + + if script_args: + if '"' in script_args: + raise Exception( + "Cannot have double quotes around individual arguments in the --script-args string." + ) + + # If the input_script and port are missing in batch mode, raise an exception + if (not graphical) and (input_script is None) and (not port): + raise Exception("An input script, -i, or port, --port, are required in batch mode.") + + args = [exe, "-DSApplet"] + if (not graphical) or (not show_welcome_screen): + args.append("-AppModeMech") + + if version < 232: + args.append("-nosplash") + args.append("-notabctrl") + + if not graphical: + args.append("-b") + + env: typing.Dict[str, str] = os.environ.copy() + if debug: + env["WBDEBUG_STOP"] = "1" + + if port: + args.append("-grpc") + args.append(str(port)) + + if project_file: + args.append("-file") + args.append(project_file) + + if input_script: + args.append("-script") + args.append(input_script) + + if script_args: + args.append("-ScriptArgs") + args.append(f'"{script_args}"') + + if (not graphical) and input_script: + exit = True + if version < 241: + warnings.warn( + "Please ensure ExtAPI.Application.Close() is at the end of your script. " + "Without this command, Batch mode will not terminate.", + stacklevel=2, + ) + + if exit and input_script and version >= 241: + args.append("-x") + + profile: UniqueUserProfile = None + if private_appdata: + new_profile_name = f"Mechanical-{os.getpid()}" + profile = UniqueUserProfile(new_profile_name, dry_run=DRY_RUN) + profile.update_environment(env) + + if not DRY_RUN: + version_name = atp.SUPPORTED_ANSYS_VERSIONS[version] + if graphical: + mode = "Graphical" + else: + mode = "Batch" + print(f"Starting Ansys Mechanical version {version_name} in {mode} mode...") + if port: + # TODO - Mechanical doesn't write anything to the stdout in grpc mode + # when logging is off.. Ideally we let Mechanical write it, so + # the user only sees the message when the server is ready. + print(f"Serving on port {port}") + + if features is not None: + args.extend(get_command_line_arguments(features.split(";"))) + + if DRY_RUN: + return args, env + else: + _run(args, env, False, True) + + if private_appdata: + profile.cleanup() + + +@click.command() +@click.help_option("--help", "-h") +@click.option( + "-p", + "--project-file", + default=None, + help="Opens Mechanical project file (.mechdb). Cannot be mixed with -i", +) +@click.option( + "--private-appdata", + default=None, + is_flag=True, + help="Make the appdata folder private.\ + This enables you to run parallel instances of Mechanical.", +) +@click.option( + "--port", + type=int, + help="Start mechanical in server mode with the given port number", +) +@click.option( + "--features", + type=str, + default=None, + help=f"Beta feature flags to set, as a semicolon delimited list.\ + Options: {get_feature_flag_names()}", +) +@click.option( + "-i", + "--input-script", + default=None, + help="Name of the input Python script. Cannot be mixed with -p", +) +@click.option( + "--script-args", + default=None, + help='Arguments to pass into the --input-script, -i. \ +Write the arguments as a string, with each argument \ +separated by a comma. For example, --script-args "arg1,arg2" \ +This can only be used with the --input-script argument.', +) +@click.option( + "--exit", + is_flag=True, + default=None, + help="Exit the application after running an input script. \ +You can only use this command with --input-script argument (-i). \ +The command defaults to true you are not running the application in graphical mode. \ +The ``exit`` command is only supported in version 2024 R1 or later.", +) +@click.option( + "-s", + "--show-welcome-screen", + is_flag=True, + default=False, + help="Show the welcome screen, where you can select the file to open.\ + Only affects graphical mode", +) +@click.option( + "--debug", + is_flag=True, + default=False, + help="Show a debug dialog right when the process starts.", +) +@click.option( + "-r", + "--revision", + default=None, + type=int, + help='Ansys Revision number, e.g. "251" or "242". If none is specified\ +, uses the default from ansys-tools-path', +) +@click.option( + "-g", + "--graphical", + is_flag=True, + default=False, + help="Graphical mode", +) +def cli( + project_file: str, + port: int, + debug: bool, + input_script: str, + script_args: str, + revision: int, + graphical: bool, + show_welcome_screen: bool, + private_appdata: bool, + exit: bool, + features: str, +): + """CLI tool to run mechanical. + + USAGE: + + The following example demonstrates the main use of this tool: + + $ ansys-mechanical -r 251 -g + + Starting Ansys Mechanical version 2025R1 in graphical mode... + """ + exe = atp.get_mechanical_path(allow_input=False, version=revision) + version = atp.version_from_path("mechanical", exe) + + return _cli_impl( + project_file, + port, + debug, + input_script, + script_args, + exe, + version, + graphical, + show_welcome_screen, + private_appdata, + exit, + features, + ) diff --git a/tests/conftest.py b/tests/conftest.py index cd4565b9a..657615392 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -378,7 +378,7 @@ def pytest_addoption(parser): mechanical_path = atp.get_mechanical_path(False) if mechanical_path is None: - parser.addoption("--ansys-version", default="242") + parser.addoption("--ansys-version", default="251") else: mechanical_version = atp.version_from_path("mechanical", mechanical_path) parser.addoption("--ansys-version", default=str(mechanical_version)) @@ -400,7 +400,7 @@ def pytest_collection_modifyitems(config, items): item.add_marker(skip_versions) # Skip tests that are outside of the provided version range. For example, - # @pytest.mark.version_range(241,242) + # @pytest.mark.version_range(241,251) if "version_range" in item.keywords: revns = [mark.args for mark in item.iter_markers(name="version_range")][0] ansys_version = int(config.getoption("--ansys-version")) diff --git a/tests/test_cli.py b/tests/test_cli.py index 9ce1f7000..71dd0a5f9 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,421 +1,440 @@ -# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. -# SPDX-License-Identifier: MIT -# -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -import os -from pathlib import Path -import subprocess -import sys - -import pytest - -from ansys.mechanical.core.ide_config import _cli_impl as ideconfig_cli_impl -from ansys.mechanical.core.ide_config import get_stubs_location, get_stubs_versions -from ansys.mechanical.core.run import _cli_impl - -STUBS_LOC = get_stubs_location() -STUBS_REVNS = get_stubs_versions(STUBS_LOC) -MIN_STUBS_REVN = min(STUBS_REVNS) -MAX_STUBS_REVN = max(STUBS_REVNS) - - -@pytest.mark.cli -def test_cli_default(disable_cli): - args, env = _cli_impl(exe="AnsysWBU.exe", version=241, port=11) - assert os.environ == env - assert "-AppModeMech" in args - assert "-b" in args - assert "-DSApplet" in args - assert "AnsysWBU.exe" in args - - -@pytest.mark.cli -def test_cli_debug(disable_cli): - _, env = _cli_impl(exe="AnsysWBU.exe", version=241, debug=True, port=11) - assert "WBDEBUG_STOP" in env - - -@pytest.mark.cli -def test_cli_graphical(disable_cli): - args, _ = _cli_impl(exe="AnsysWBU.exe", version=241, graphical=True) - assert "-b" not in args - - -@pytest.mark.cli -def test_cli_appdata(disable_cli): - _, env = _cli_impl(exe="AnsysWBU.exe", version=241, private_appdata=True, port=11) - var_to_compare = "TEMP" if os.name == "nt" else "HOME" - assert os.environ[var_to_compare] != env[var_to_compare] - - -@pytest.mark.cli -def test_cli_errors(disable_cli): - # can't mix project file and input script - with pytest.raises(Exception): - _cli_impl( - exe="AnsysWBU.exe", - version=241, - project_file="foo.mechdb", - input_script="foo.py", - graphical=True, - ) - # project file only works in graphical mode - with pytest.raises(Exception): - _cli_impl(exe="AnsysWBU.exe", version=241, project_file="foo.mechdb") - # can't mix port and project file - with pytest.raises(Exception): - _cli_impl(exe="AnsysWBU.exe", version=241, project_file="foo.mechdb", port=11) - # can't mix port and input script - with pytest.raises(Exception): - _cli_impl(exe="AnsysWBU.exe", version=241, input_script="foo.py", port=11) - - -@pytest.mark.cli -def test_cli_appmode(disable_cli): - args, _ = _cli_impl(exe="AnsysWBU.exe", version=241, show_welcome_screen=True, graphical=True) - assert "-AppModeMech" not in args - - -@pytest.mark.cli -def test_cli_231(disable_cli): - args, _ = _cli_impl(exe="AnsysWBU.exe", version=231, port=11) - assert "-nosplash" in args - assert "-notabctrl" in args - - -@pytest.mark.cli -def test_cli_port(disable_cli): - args, _ = _cli_impl(exe="AnsysWBU.exe", version=241, port=11) - assert "-grpc" in args - assert "11" in args - - -@pytest.mark.cli -def test_cli_project(disable_cli): - args, _ = _cli_impl(exe="AnsysWBU.exe", version=241, project_file="foo.mechdb", graphical=True) - assert "-file" in args - assert "foo.mechdb" in args - - -@pytest.mark.cli -def test_cli_script(disable_cli): - args, _ = _cli_impl(exe="AnsysWBU.exe", version=241, input_script="foo.py", graphical=True) - assert "-script" in args - assert "foo.py" in args - - -@pytest.mark.cli -def test_cli_scriptargs(disable_cli): - args, _ = _cli_impl( - exe="AnsysWBU.exe", - version=241, - input_script="foo.py", - script_args="arg1,arg2,arg3", - graphical=True, - ) - assert "-ScriptArgs" in args - assert '"arg1,arg2,arg3"' in args - assert "-script" in args - assert "foo.py" in args - - -@pytest.mark.cli -def test_cli_scriptargs_no_script(disable_cli): - with pytest.raises(Exception): - _cli_impl( - exe="AnsysWBU.exe", - version=241, - script_args="arg1,arg2,arg3", - graphical=True, - ) - - -@pytest.mark.cli -def test_cli_scriptargs_singlequote(disable_cli): - args, _ = _cli_impl( - exe="AnsysWBU.exe", - version=241, - input_script="foo.py", - script_args="arg1,arg2,'arg3'", - graphical=True, - ) - assert "-ScriptArgs" in args - assert "\"arg1,arg2,'arg3'\"" in args - assert "-script" in args - assert "foo.py" in args - - -@pytest.mark.cli -def test_cli_scriptargs_doublequote(disable_cli): - with pytest.raises(Exception): - _cli_impl( - exe="AnsysWBU.exe", - version=241, - input_script="foo.py", - script_args='arg1,"arg2",arg3', - graphical=True, - ) - - -@pytest.mark.cli -def test_cli_features(disable_cli): - with pytest.warns(UserWarning): - args, _ = _cli_impl(exe="AnsysWBU.exe", version=241, features="a;b;c", port=11) - assert "-featureflags" in args - assert "a;b;c" in args - args, _ = _cli_impl(exe="AnsysWBU.exe", version=241, features="MultistageHarmonic", port=11) - assert "Mechanical.MultistageHarmonic" in args - - -@pytest.mark.cli -def test_cli_exit(disable_cli): - # Regardless of version, `exit` does nothing on its own - args, _ = _cli_impl(exe="AnsysWBU.exe", version=232, exit=True, port=11) - assert "-x" not in args - - args, _ = _cli_impl(exe="AnsysWBU.exe", version=241, exit=True, port=11) - assert "-x" not in args - - # On versions earlier than 2024R1, `exit` throws a warning but does nothing - with pytest.warns(UserWarning): - args, _ = _cli_impl(exe="AnsysWBU.exe", version=232, exit=True, input_script="foo.py") - assert "-x" not in args - - # In UI mode, exit must be manually specified - args, _ = _cli_impl(exe="AnsysWBU.exe", version=241, input_script="foo.py", graphical=True) - assert "-x" not in args - - # In batch mode, exit is implied - args, _ = _cli_impl(exe="AnsysWBU.exe", version=241, input_script="foo.py") - assert "-x" in args - - # In batch mode, exit can be explicitly passed - args, _ = _cli_impl(exe="AnsysWBU.exe", version=241, exit=True, input_script="foo.py") - assert "-x" in args - - # In batch mode, exit can not be disabled - args, _ = _cli_impl(exe="AnsysWBU.exe", version=241, exit=False, input_script="foo.py") - assert "-x" in args - - -@pytest.mark.cli -def test_cli_batch_required_args(disable_cli): - # ansys-mechanical -r 241 => exception - with pytest.raises(Exception): - _cli_impl(exe="AnsysWBU.exe", version=241) - - # ansys-mechanical -r 241 -g => no exception - try: - _cli_impl(exe="AnsysWBU.exe", version=241, graphical=True) - except Exception as e: - assert False, f"cli raised an exception: {e}" - - # ansys-mechanical -r 241 -i input.py => no exception - try: - _cli_impl(exe="AnsysWBU.exe", version=241, input_script="input.py") - except Exception as e: - assert False, f"cli raised an exception: {e}" - - # ansys-mechanical -r 241 -port 11 => no exception - try: - _cli_impl(exe="AnsysWBU.exe", version=241, port=11) - except Exception as e: - assert False, f"cli raised an exception: {e}" - - -def get_settings_location() -> str: - """Get the location of settings.json for user settings. - - Returns - ------- - str - The path to the settings.json file for users on Windows and Linux. - """ - if "win" in sys.platform: - settings_json = Path(os.environ.get("APPDATA")) / "Code" / "User" / "settings.json" - elif "lin" in sys.platform: - settings_json = Path(os.environ.get("HOME")) / ".config" / "Code" / "User" / "settings.json" - - return settings_json - - -@pytest.mark.cli -def test_ideconfig_cli_ide_exception(capfd, pytestconfig): - """Test IDE configuration raises an exception for anything but vscode.""" - revision = int(pytestconfig.getoption("ansys_version")) - with pytest.raises(Exception): - ideconfig_cli_impl( - ide="pycharm", - target="user", - revision=revision, - ) - - -def test_ideconfig_cli_version_exception(pytestconfig): - """Test the IDE configuration raises an exception when the version is out of bounds.""" - revision = int(pytestconfig.getoption("ansys_version")) - stubs_location = get_stubs_location() - stubs_revns = get_stubs_versions(stubs_location) - - # If revision number is greater than the maximum stubs revision number - # assert an exception is raised - if revision > max(stubs_revns): - with pytest.raises(Exception): - ideconfig_cli_impl( - ide="vscode", - target="user", - revision=revision, - ) - - -@pytest.mark.cli -@pytest.mark.version_range(MIN_STUBS_REVN, MAX_STUBS_REVN) -def test_ideconfig_cli_user_settings(capfd, pytestconfig): - """Test the IDE configuration prints correct information for user settings.""" - # Get the revision number - revision = int(pytestconfig.getoption("ansys_version")) - stubs_location = get_stubs_location() - - # Run the IDE configuration command for the user settings type - ideconfig_cli_impl( - ide="vscode", - target="user", - revision=revision, - ) - - # Get output of the IDE configuration command - out, err = capfd.readouterr() - out = out.replace("\\\\", "\\") - - # Get the path to the settings.json file based on operating system env vars - settings_json = get_settings_location() - - assert f"Update {settings_json} with the following information" in out - assert str(stubs_location) in out - - -@pytest.mark.cli -@pytest.mark.version_range(MIN_STUBS_REVN, MAX_STUBS_REVN) -def test_ideconfig_cli_workspace_settings(capfd, pytestconfig): - """Test the IDE configuration prints correct information for workplace settings.""" - # Set the revision number - revision = int(pytestconfig.getoption("ansys_version")) - stubs_location = get_stubs_location() - - # Run the IDE configuration command - ideconfig_cli_impl( - ide="vscode", - target="workspace", - revision=revision, - ) - - # Get output of the IDE configuration command - out, err = capfd.readouterr() - out = out.replace("\\\\", "\\") - - # Get the path to the settings.json file based on the current directory & .vscode folder - settings_json = Path.cwd() / ".vscode" / "settings.json" - - # Assert the correct settings.json file and stubs location is in the output - assert f"Update {settings_json} with the following information" in out - assert str(stubs_location) in out - assert "Please ensure the .vscode folder is in the root of your project or repository" in out - - -@pytest.mark.cli -@pytest.mark.python_env -@pytest.mark.version_range(MIN_STUBS_REVN, MAX_STUBS_REVN) -def test_ideconfig_cli_venv(test_env, run_subprocess, rootdir, pytestconfig): - """Test the IDE configuration location when a virtual environment is active.""" - # Set the revision number - revision = pytestconfig.getoption("ansys_version") - - # Install pymechanical - subprocess.check_call( - [test_env.python, "-m", "pip", "install", "-e", "."], - cwd=rootdir, - env=test_env.env, - ) - - # Get the virtual environment location - subprocess_output = run_subprocess( - [test_env.python, "-c", "'import sys; print(sys.prefix)'"], - env=test_env.env, - ) - # Decode stdout and fix extra backslashes in paths - venv_loc = subprocess_output[1].decode().replace("\\\\", "\\") - - # Run ansys-mechanical-ideconfig in the test virtual environment - subprocess_output_ideconfig = run_subprocess( - [ - "ansys-mechanical-ideconfig", - "--ide", - "vscode", - "--target", - "user", - "--revision", - str(revision), - ], - env=test_env.env, - ) - # Decode stdout and fix extra backslashes in paths - stdout = subprocess_output_ideconfig[1].decode().replace("\\\\", "\\") - - # Assert virtual environment is in the stdout - assert venv_loc in stdout - - -@pytest.mark.cli -@pytest.mark.python_env -@pytest.mark.version_range(MIN_STUBS_REVN, MAX_STUBS_REVN) -def test_ideconfig_cli_default(test_env, run_subprocess, rootdir, pytestconfig): - """Test the IDE configuration location when no arguments are supplied.""" - # Get the revision number - revision = pytestconfig.getoption("ansys_version") - # Set part of the settings.json path - settings_json_fragment = Path("Code") / "User" / "settings.json" - - # Install pymechanical - subprocess.check_call( - [test_env.python, "-m", "pip", "install", "-e", "."], - cwd=rootdir, - env=test_env.env, - ) - - # Get the virtual environment location - subprocess_output = run_subprocess( - [test_env.python, "-c", "'import sys; print(sys.prefix)'"], - env=test_env.env, - ) - # Decode stdout and fix extra backslashes in paths - venv_loc = subprocess_output[1].decode().replace("\\\\", "\\") - - # Run ansys-mechanical-ideconfig in the test virtual environment - subprocess_output_ideconfig = run_subprocess( - ["ansys-mechanical-ideconfig"], - env=test_env.env, - ) - # Decode stdout and fix extra backslashes in paths - stdout = subprocess_output_ideconfig[1].decode().replace("\\\\", "\\") - - assert revision in stdout - assert str(settings_json_fragment) in stdout - assert venv_loc in stdout +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os +from pathlib import Path +import subprocess +import sys + +import pytest + +from ansys.mechanical.core.ide_config import _cli_impl as ideconfig_cli_impl +from ansys.mechanical.core.ide_config import get_stubs_location, get_stubs_versions +from ansys.mechanical.core.run import _cli_impl + +STUBS_LOC = get_stubs_location() +STUBS_REVNS = get_stubs_versions(STUBS_LOC) +MIN_STUBS_REVN = min(STUBS_REVNS) +MAX_STUBS_REVN = max(STUBS_REVNS) + + +@pytest.mark.cli +def test_cli_default(disable_cli, pytestconfig): + version = int(pytestconfig.getoption("ansys_version")) + args, env = _cli_impl(exe="AnsysWBU.exe", version=version, port=11) + assert os.environ == env + assert "-AppModeMech" in args + assert "-b" in args + assert "-DSApplet" in args + assert "AnsysWBU.exe" in args + + +@pytest.mark.cli +def test_cli_debug(disable_cli, pytestconfig): + version = int(pytestconfig.getoption("ansys_version")) + _, env = _cli_impl(exe="AnsysWBU.exe", version=version, debug=True, port=11) + assert "WBDEBUG_STOP" in env + + +@pytest.mark.cli +def test_cli_graphical(disable_cli, pytestconfig): + version = int(pytestconfig.getoption("ansys_version")) + args, _ = _cli_impl(exe="AnsysWBU.exe", version=version, graphical=True) + assert "-b" not in args + + +@pytest.mark.cli +def test_cli_appdata(disable_cli, pytestconfig): + version = int(pytestconfig.getoption("ansys_version")) + _, env = _cli_impl(exe="AnsysWBU.exe", version=version, private_appdata=True, port=11) + var_to_compare = "TEMP" if os.name == "nt" else "HOME" + assert os.environ[var_to_compare] != env[var_to_compare] + + +@pytest.mark.cli +def test_cli_errors(disable_cli, pytestconfig): + version = int(pytestconfig.getoption("ansys_version")) + # can't mix project file and input script + with pytest.raises(Exception): + _cli_impl( + exe="AnsysWBU.exe", + version=version, + project_file="foo.mechdb", + input_script="foo.py", + graphical=True, + ) + # project file only works in graphical mode + with pytest.raises(Exception): + _cli_impl(exe="AnsysWBU.exe", version=version, project_file="foo.mechdb") + # can't mix port and project file + with pytest.raises(Exception): + _cli_impl(exe="AnsysWBU.exe", version=version, project_file="foo.mechdb", port=11) + # can't mix port and input script + with pytest.raises(Exception): + _cli_impl(exe="AnsysWBU.exe", version=version, input_script="foo.py", port=11) + + +@pytest.mark.cli +def test_cli_appmode(disable_cli, pytestconfig): + version = int(pytestconfig.getoption("ansys_version")) + args, _ = _cli_impl( + exe="AnsysWBU.exe", version=version, show_welcome_screen=True, graphical=True + ) + assert "-AppModeMech" not in args + + +@pytest.mark.cli +def test_cli_231(disable_cli): + args, _ = _cli_impl(exe="AnsysWBU.exe", version=231, port=11) + assert "-nosplash" in args + assert "-notabctrl" in args + + +@pytest.mark.cli +def test_cli_port(disable_cli, pytestconfig): + version = int(pytestconfig.getoption("ansys_version")) + args, _ = _cli_impl(exe="AnsysWBU.exe", version=version, port=11) + assert "-grpc" in args + assert "11" in args + + +@pytest.mark.cli +def test_cli_project(disable_cli, pytestconfig): + version = int(pytestconfig.getoption("ansys_version")) + args, _ = _cli_impl( + exe="AnsysWBU.exe", version=version, project_file="foo.mechdb", graphical=True + ) + assert "-file" in args + assert "foo.mechdb" in args + + +@pytest.mark.cli +def test_cli_script(disable_cli, pytestconfig): + version = int(pytestconfig.getoption("ansys_version")) + args, _ = _cli_impl(exe="AnsysWBU.exe", version=version, input_script="foo.py", graphical=True) + assert "-script" in args + assert "foo.py" in args + + +@pytest.mark.cli +def test_cli_scriptargs(disable_cli, pytestconfig): + version = int(pytestconfig.getoption("ansys_version")) + args, _ = _cli_impl( + exe="AnsysWBU.exe", + version=version, + input_script="foo.py", + script_args="arg1,arg2,arg3", + graphical=True, + ) + assert "-ScriptArgs" in args + assert '"arg1,arg2,arg3"' in args + assert "-script" in args + assert "foo.py" in args + + +@pytest.mark.cli +def test_cli_scriptargs_no_script(disable_cli, pytestconfig): + version = int(pytestconfig.getoption("ansys_version")) + with pytest.raises(Exception): + _cli_impl( + exe="AnsysWBU.exe", + version=version, + script_args="arg1,arg2,arg3", + graphical=True, + ) + + +@pytest.mark.cli +def test_cli_scriptargs_singlequote(disable_cli, pytestconfig): + version = int(pytestconfig.getoption("ansys_version")) + args, _ = _cli_impl( + exe="AnsysWBU.exe", + version=version, + input_script="foo.py", + script_args="arg1,arg2,'arg3'", + graphical=True, + ) + assert "-ScriptArgs" in args + assert "\"arg1,arg2,'arg3'\"" in args + assert "-script" in args + assert "foo.py" in args + + +@pytest.mark.cli +def test_cli_scriptargs_doublequote(disable_cli, pytestconfig): + version = int(pytestconfig.getoption("ansys_version")) + with pytest.raises(Exception): + _cli_impl( + exe="AnsysWBU.exe", + version=version, + input_script="foo.py", + script_args='arg1,"arg2",arg3', + graphical=True, + ) + + +@pytest.mark.cli +def test_cli_features(disable_cli, pytestconfig): + version = int(pytestconfig.getoption("ansys_version")) + with pytest.warns(UserWarning): + args, _ = _cli_impl(exe="AnsysWBU.exe", version=version, features="a;b;c", port=11) + assert "-featureflags" in args + assert "a;b;c" in args + args, _ = _cli_impl(exe="AnsysWBU.exe", version=version, features="MultistageHarmonic", port=11) + assert "Mechanical.MultistageHarmonic" in args + + +@pytest.mark.cli +def test_cli_exit(disable_cli, pytestconfig): + version = int(pytestconfig.getoption("ansys_version")) + # Regardless of version, `exit` does nothing on its own + args, _ = _cli_impl(exe="AnsysWBU.exe", version=232, exit=True, port=11) + assert "-x" not in args + + args, _ = _cli_impl(exe="AnsysWBU.exe", version=version, exit=True, port=11) + assert "-x" not in args + + # On versions earlier than 2024R1, `exit` throws a warning but does nothing + with pytest.warns(UserWarning): + args, _ = _cli_impl(exe="AnsysWBU.exe", version=232, exit=True, input_script="foo.py") + assert "-x" not in args + + # In UI mode, exit must be manually specified + args, _ = _cli_impl(exe="AnsysWBU.exe", version=version, input_script="foo.py", graphical=True) + assert "-x" not in args + + # In batch mode, exit is implied + args, _ = _cli_impl(exe="AnsysWBU.exe", version=version, input_script="foo.py") + assert "-x" in args + + # In batch mode, exit can be explicitly passed + args, _ = _cli_impl(exe="AnsysWBU.exe", version=version, exit=True, input_script="foo.py") + assert "-x" in args + + # In batch mode, exit can not be disabled + args, _ = _cli_impl(exe="AnsysWBU.exe", version=version, exit=False, input_script="foo.py") + assert "-x" in args + + +@pytest.mark.cli +def test_cli_batch_required_args(disable_cli): + # ansys-mechanical -r 241 => exception + with pytest.raises(Exception): + _cli_impl(exe="AnsysWBU.exe", version=241) + + # ansys-mechanical -r 241 -g => no exception + try: + _cli_impl(exe="AnsysWBU.exe", version=241, graphical=True) + except Exception as e: + assert False, f"cli raised an exception: {e}" + + # ansys-mechanical -r 241 -i input.py => no exception + try: + _cli_impl(exe="AnsysWBU.exe", version=241, input_script="input.py") + except Exception as e: + assert False, f"cli raised an exception: {e}" + + # ansys-mechanical -r 241 -port 11 => no exception + try: + _cli_impl(exe="AnsysWBU.exe", version=241, port=11) + except Exception as e: + assert False, f"cli raised an exception: {e}" + + +def get_settings_location() -> str: + """Get the location of settings.json for user settings. + + Returns + ------- + str + The path to the settings.json file for users on Windows and Linux. + """ + if "win" in sys.platform: + settings_json = Path(os.environ.get("APPDATA")) / "Code" / "User" / "settings.json" + elif "lin" in sys.platform: + settings_json = Path(os.environ.get("HOME")) / ".config" / "Code" / "User" / "settings.json" + + return settings_json + + +@pytest.mark.cli +def test_ideconfig_cli_ide_exception(capfd, pytestconfig): + """Test IDE configuration raises an exception for anything but vscode.""" + revision = int(pytestconfig.getoption("ansys_version")) + with pytest.raises(Exception): + ideconfig_cli_impl( + ide="pycharm", + target="user", + revision=revision, + ) + + +def test_ideconfig_cli_version_exception(pytestconfig): + """Test the IDE configuration raises an exception when the version is out of bounds.""" + revision = int(pytestconfig.getoption("ansys_version")) + stubs_location = get_stubs_location() + stubs_revns = get_stubs_versions(stubs_location) + + # If revision number is greater than the maximum stubs revision number + # assert an exception is raised + if revision > max(stubs_revns): + with pytest.raises(Exception): + ideconfig_cli_impl( + ide="vscode", + target="user", + revision=revision, + ) + + +@pytest.mark.cli +@pytest.mark.version_range(MIN_STUBS_REVN, MAX_STUBS_REVN) +def test_ideconfig_cli_user_settings(capfd, pytestconfig): + """Test the IDE configuration prints correct information for user settings.""" + # Get the revision number + revision = int(pytestconfig.getoption("ansys_version")) + stubs_location = get_stubs_location() + + # Run the IDE configuration command for the user settings type + ideconfig_cli_impl( + ide="vscode", + target="user", + revision=revision, + ) + + # Get output of the IDE configuration command + out, err = capfd.readouterr() + out = out.replace("\\\\", "\\") + + # Get the path to the settings.json file based on operating system env vars + settings_json = get_settings_location() + + assert f"Update {settings_json} with the following information" in out + assert str(stubs_location) in out + + +@pytest.mark.cli +@pytest.mark.version_range(MIN_STUBS_REVN, MAX_STUBS_REVN) +def test_ideconfig_cli_workspace_settings(capfd, pytestconfig): + """Test the IDE configuration prints correct information for workplace settings.""" + # Set the revision number + revision = int(pytestconfig.getoption("ansys_version")) + stubs_location = get_stubs_location() + + # Run the IDE configuration command + ideconfig_cli_impl( + ide="vscode", + target="workspace", + revision=revision, + ) + + # Get output of the IDE configuration command + out, err = capfd.readouterr() + out = out.replace("\\\\", "\\") + + # Get the path to the settings.json file based on the current directory & .vscode folder + settings_json = Path.cwd() / ".vscode" / "settings.json" + + # Assert the correct settings.json file and stubs location is in the output + assert f"Update {settings_json} with the following information" in out + assert str(stubs_location) in out + assert "Please ensure the .vscode folder is in the root of your project or repository" in out + + +@pytest.mark.cli +@pytest.mark.python_env +@pytest.mark.version_range(MIN_STUBS_REVN, MAX_STUBS_REVN) +def test_ideconfig_cli_venv(test_env, run_subprocess, rootdir, pytestconfig): + """Test the IDE configuration location when a virtual environment is active.""" + # Set the revision number + revision = pytestconfig.getoption("ansys_version") + + # Install pymechanical + subprocess.check_call( + [test_env.python, "-m", "pip", "install", "-e", "."], + cwd=rootdir, + env=test_env.env, + ) + + # Get the virtual environment location + subprocess_output = run_subprocess( + [test_env.python, "-c", "'import sys; print(sys.prefix)'"], + env=test_env.env, + ) + # Decode stdout and fix extra backslashes in paths + venv_loc = subprocess_output[1].decode().replace("\\\\", "\\") + + # Run ansys-mechanical-ideconfig in the test virtual environment + subprocess_output_ideconfig = run_subprocess( + [ + "ansys-mechanical-ideconfig", + "--ide", + "vscode", + "--target", + "user", + "--revision", + str(revision), + ], + env=test_env.env, + ) + # Decode stdout and fix extra backslashes in paths + stdout = subprocess_output_ideconfig[1].decode().replace("\\\\", "\\") + + # Assert virtual environment is in the stdout + assert venv_loc in stdout + + +@pytest.mark.cli +@pytest.mark.python_env +@pytest.mark.version_range(MIN_STUBS_REVN, MAX_STUBS_REVN) +def test_ideconfig_cli_default(test_env, run_subprocess, rootdir, pytestconfig): + """Test the IDE configuration location when no arguments are supplied.""" + # Get the revision number + revision = pytestconfig.getoption("ansys_version") + # Set part of the settings.json path + settings_json_fragment = Path("Code") / "User" / "settings.json" + + # Install pymechanical + subprocess.check_call( + [test_env.python, "-m", "pip", "install", "-e", "."], + cwd=rootdir, + env=test_env.env, + ) + + # Get the virtual environment location + subprocess_output = run_subprocess( + [test_env.python, "-c", "'import sys; print(sys.prefix)'"], + env=test_env.env, + ) + # Decode stdout and fix extra backslashes in paths + venv_loc = subprocess_output[1].decode().replace("\\\\", "\\") + + # Run ansys-mechanical-ideconfig in the test virtual environment + subprocess_output_ideconfig = run_subprocess( + ["ansys-mechanical-ideconfig"], + env=test_env.env, + ) + # Decode stdout and fix extra backslashes in paths + stdout = subprocess_output_ideconfig[1].decode().replace("\\\\", "\\") + + assert revision in stdout + assert str(settings_json_fragment) in stdout + assert venv_loc in stdout diff --git a/tests/test_mechanical.py b/tests/test_mechanical.py index 5ef1fcc67..d4db05b43 100644 --- a/tests/test_mechanical.py +++ b/tests/test_mechanical.py @@ -1,558 +1,558 @@ -# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. -# SPDX-License-Identifier: MIT -# -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -import json -import os -import pathlib -import re - -import ansys.tools.path -import grpc -import pytest - -import ansys.mechanical.core as pymechanical -import ansys.mechanical.core.errors as errors -import ansys.mechanical.core.misc as misc -import conftest - - -@pytest.mark.remote_session_connect -def test_run_python_script_success(mechanical): - result = mechanical.run_python_script("2+3") - assert result == "5" - - -@pytest.mark.remote_session_connect -def test_run_python_script_success_return_empty(mechanical): - result = mechanical.run_python_script("ExtAPI.DataModel.Project") - if misc.is_windows(): - assert result == "" - else: - assert result == "Ansys.ACT.Automation.Mechanical.Project" - - -@pytest.mark.remote_session_connect -def test_run_python_script_error(mechanical): - with pytest.raises(grpc.RpcError) as exc_info: - mechanical.run_python_script("import test") - - assert exc_info.value.details() == "No module named test" - - -@pytest.mark.remote_session_connect -def test_run_python_from_file_success(mechanical): - current_working_directory = os.getcwd() - script_path = os.path.join( - current_working_directory, "tests", "scripts", "run_python_success.py" - ) - print("running python script : ", script_path) - result = mechanical.run_python_script_from_file(script_path) - - assert result == "test" - - -# @pytest.mark.remote_session_connect -# def test_run_python_from_file_log_messages(mechanical): -# current_working_directory = os.getcwd() -# script_path = os.path.join(current_working_directory, "tests", "scripts", "log_message.py") -# print("running python script : ", script_path) -# -# print("logging_not enabled") -# result = mechanical.run_python_script_from_file(script_path) -# -# print("logging_enabled") -# result = mechanical.run_python_script_from_file( -# script_path, enable_logging=True, log_level="DEBUG", progress_interval=1000 -# ) -# -# result = mechanical.run_python_script_from_file( -# script_path, enable_logging=True, log_level="INFO", progress_interval=1000 -# ) -# -# result = mechanical.run_python_script_from_file( -# script_path, enable_logging=True, log_level="WARNING", progress_interval=1000 -# ) -# -# result = mechanical.run_python_script_from_file( -# script_path, enable_logging=True, log_level="ERROR", progress_interval=1000 -# ) -# -# result = mechanical.run_python_script_from_file( -# script_path, enable_logging=True, log_level="CRITICAL", progress_interval=1000 -# ) -# -# assert result == "log_test" - - -@pytest.mark.remote_session_connect -def test_run_python_script_from_file_error(mechanical): - with pytest.raises(grpc.RpcError) as exc_info: - current_working_directory = os.getcwd() - script_path = os.path.join( - current_working_directory, "tests", "scripts", "run_python_error.py" - ) - print("running python script : ", script_path) - mechanical.run_python_script_from_file(script_path) - - assert exc_info.value.details() == "name 'get_myname' is not defined" - - -@pytest.mark.remote_session_connect -@pytest.mark.parametrize("file_name", [r"hsec.x_t"]) -def test_upload(mechanical, file_name, assets): - mechanical.run_python_script("ExtAPI.DataModel.Project.New()") - directory = mechanical.run_python_script("ExtAPI.DataModel.Project.ProjectDirectory") - print(directory) - - file_path = os.path.join(assets, file_name) - mechanical.upload( - file_name=file_path, file_location_destination=directory, chunk_size=1024 * 1024 - ) - - base_name = os.path.basename(file_path) - combined_path = os.path.join(directory, base_name) - file_path_modified = combined_path.replace("\\", "\\\\") - # we are working with iron python 2.7 on mechanical side - # use python 2.7 style formatting - # path = '%s' % file_path_modified - script = 'import os\nos.path.exists("%s")' % file_path_modified - print(script) - result = mechanical.run_python_script(script) - assert bool(result) - - -@pytest.mark.remote_session_connect -# we are using only a small test file -# change the chunk_size for that -# ideally this will be 64*1024, 1024*1024, etc. -@pytest.mark.parametrize("chunk_size", [10, 50, 100]) -def test_upload_with_different_chunk_size(mechanical, chunk_size, assets): - file_path = os.path.join(assets, "hsec.x_t") - mechanical.run_python_script("ExtAPI.DataModel.Project.New()") - directory = mechanical.run_python_script("ExtAPI.DataModel.Project.ProjectDirectory") - mechanical.upload( - file_name=file_path, file_location_destination=directory, chunk_size=chunk_size - ) - - -def get_solve_out_path(mechanical): - solve_out_path = "" - for file_path in mechanical.list_files(): - if file_path.find("solve.out") != -1: - solve_out_path = file_path - break - - return solve_out_path - - -def write_file_contents_to_console(path): - with open(path, "rt") as file: - for line in file: - print(line, end="") - - -def disable_distributed_solve(mechanical): - script = ( - 'ExtAPI.Application.SolveConfigurations["My Computer"].' - "SolveProcessSettings.DistributeSolution = False" - ) - mechanical.run_python_script(script) - - -def enable_distributed_solve(mechanical): - script = ( - 'ExtAPI.Application.SolveConfigurations["My Computer"].' - "SolveProcessSettings.DistributeSolution = True" - ) - mechanical.run_python_script(script) - - -def solve_and_return_results(mechanical): - current_working_directory = os.getcwd() - file_path = os.path.join(current_working_directory, "tests", "assets", "hsec.x_t") - - mechanical.clear() - directory = mechanical.project_directory - mechanical.upload( - file_name=file_path, file_location_destination=directory, chunk_size=1024 * 1024 - ) - - python_script = os.path.join(current_working_directory, "tests", "scripts", "api.py") - - text_file = open(python_script, "r") - # read whole file to a string - data = text_file.read() - # close file - text_file.close() - - file_path_string = "\n" - - # let us append the scripts to run - func_to_call = """ -import os -directory = ExtAPI.DataModel.Project.ProjectDirectory -file_path_modified=os.path.join(directory,'hsec.x_t') -attach_geometry(file_path_modified) -generate_mesh() -add_static_structural_analysis_bc_results() -solve_model() -return_total_deformation() - """ - python_script = data + file_path_string + func_to_call - - result = mechanical.run_python_script( - python_script, enable_logging=True, log_level="INFO", progress_interval=1000 - ) - - # if solve fails, solve.out contains enough information - solve_out_path = get_solve_out_path(mechanical) - - if solve_out_path != "": - print(f"downloading {solve_out_path} from server") - print(f"downloading to {current_working_directory}") - solve_out_local_path_list = mechanical.download( - solve_out_path, target_dir=current_working_directory - ) - solve_out_local_path = solve_out_local_path_list[0] - print(solve_out_local_path) - - write_file_contents_to_console(solve_out_local_path) - - # done with solve.out - remove it - os.remove(solve_out_local_path) - - return result - - -def verify_project_download(mechanical, tmpdir): - files = mechanical.list_files() - number_of_files = len(files) - - print("files available: ") - for file in files: - print(file) - assert number_of_files > 0 - - # download the project - project_directory = mechanical.project_directory - print(f"project directory: {project_directory}") - - target_dir = os.path.join(tmpdir, "mechanical_project") - # add a trailing path separator - target_dir = os.path.join(target_dir, "") - print(f"creating target directory {target_dir}") - if not os.path.exists(target_dir): - os.mkdir(target_dir) - - out_files = mechanical.download_project(target_dir=target_dir) - print("downloaded files:") - for file in out_files: - print(file) - assert os.path.exists(file) and os.path.getsize(file) > 0 - - files = mechanical.list_files() - assert len(files) == len(out_files) - - target_dir = os.path.join(tmpdir, "mechanical_project2") - # add a trailing path separator - target_dir = os.path.join(target_dir, "") - print(f"creating target directory {target_dir}") - if not os.path.exists(target_dir): - os.mkdir(target_dir) - - # project not saved. - # no mechdb available. - extensions = ["mechdb"] - with pytest.raises(ValueError): - mechanical.download_project(extensions=extensions, target_dir=target_dir) - - extensions = ["xml", "rst"] - out_files = mechanical.download_project(extensions=extensions, target_dir=target_dir) - print(f"downloaded files for extensions: {extensions}") - for file in out_files: - print(file) - assert os.path.exists(file) and os.path.getsize(file) > 0 - extension = pathlib.Path(file).suffix - extension_without_dot = extension[1:] - assert extension_without_dot in extensions - - -@pytest.mark.remote_session_connect -# @pytest.mark.wip -# @pytest.mark.skip(reason="avoid long running") -def test_upload_attach_mesh_solve_use_api_non_distributed_solve(mechanical, tmpdir): - # default is distributed solve - # let's disable the distributed solve and then solve - # enable the distributed solve back - - # this test could run under a container with 1 cpu - # let us disable distributed solve - disable_distributed_solve(mechanical) - - result = solve_and_return_results(mechanical) - - # revert back to distributed solve - enable_distributed_solve(mechanical) - - dict_result = json.loads(result) - - min_value = float(dict_result["Minimum"].split(" ")[0]) - max_value = float(dict_result["Maximum"].split(" ")[0]) - avg_value = float(dict_result["Average"].split(" ")[0]) - - print(f"min_value = {min_value} max_value = {max_value} avg_value = {avg_value}") - - result = mechanical.run_python_script("ExtAPI.DataModel.Project.Model.Analyses[0].ObjectState") - assert "5" == result.lower() - - verify_project_download(mechanical, tmpdir) - - -@pytest.mark.remote_session_connect -def test_upload_attach_mesh_solve_use_api_distributed_solve(mechanical, tmpdir): - # default is distributed solve - - result = solve_and_return_results(mechanical) - - dict_result = json.loads(result) - - min_value = float(dict_result["Minimum"].split(" ")[0]) - max_value = float(dict_result["Maximum"].split(" ")[0]) - avg_value = float(dict_result["Average"].split(" ")[0]) - - print(f"min_value = {min_value} max_value = {max_value} avg_value = {avg_value}") - - result = mechanical.run_python_script("ExtAPI.DataModel.Project.Model.Analyses[0].ObjectState") - assert "5" == result.lower() - - verify_project_download(mechanical, tmpdir) - - -def verify_download(mechanical, tmpdir, file_name, chunk_size): - directory = mechanical.run_python_script("ExtAPI.DataModel.Project.ProjectDirectory") - print(directory) - - current_working_directory = os.getcwd() - file_path = os.path.join(current_working_directory, "tests", "assets", file_name) - mechanical.upload( - file_name=file_path, file_location_destination=directory, chunk_size=1024 * 1024 - ) - - print(f"using the temporary directory: {tmpdir}") - file_path = os.path.join(directory, file_name) - local_directory = tmpdir.strpath - - # test with different download chunk_size - local_path_list = mechanical.download( - files=file_path, target_dir=local_directory, chunk_size=chunk_size - ) - print("downloaded files:") - for local_path in local_path_list: - print(f" downloaded file: {local_path}") - assert os.path.exists(local_path) and os.path.getsize(local_path) > 0 - - -@pytest.mark.remote_session_connect -# @pytest.mark.wip -@pytest.mark.parametrize("file_name", ["hsec.x_t"]) -def test_download_file(mechanical, tmpdir, file_name): - verify_download(mechanical, tmpdir, file_name, 1024 * 1024) - - -@pytest.mark.remote_session_connect -# we are using only a small test file -# change the chunk_size for that -# ideally this will be 64*1024, 1024*1024, etc. -@pytest.mark.parametrize("chunk_size", [10, 50, 100]) -def test_download_file_different_chunk_size1(mechanical, tmpdir, chunk_size): - file_name = "hsec.x_t" - - verify_download(mechanical, tmpdir, file_name, chunk_size) - - -@pytest.mark.remote_session_launch -def test_launch_meshing_mode(mechanical_meshing): - result = mechanical_meshing.run_python_script("2+3") - assert result == "5" - - -@pytest.mark.remote_session_launch -def test_launch_result_mode(mechanical_result): - result = mechanical_result.run_python_script("2+3") - assert result == "5" - - -@pytest.mark.remote_session_launch -def test_close_all_Local_instances(tmpdir): - list_ports = [] - mechanical = conftest.launch_mechanical_instance(cleanup_on_exit=False) - print(mechanical.name) - list_ports.append(mechanical._port) - - # connect to the launched instance - mechanical2 = conftest.connect_to_mechanical_instance(mechanical._port, clear_on_connect=True) - print(mechanical2.name) - - test_upload_attach_mesh_solve_use_api_non_distributed_solve(mechanical2, tmpdir) - - # use the settings and launch another Mechanical instance - mechanical.launch(cleanup_on_exit=False) - print(mechanical.name) - list_ports.append(mechanical._port) - - pymechanical.close_all_local_instances(list_ports, use_thread=False) - for value in list_ports: - assert value not in pymechanical.LOCAL_PORTS - - -@pytest.mark.remote_session_launch -def test_find_mechanical_path(): - if pymechanical.mechanical.get_start_instance(): - path = ansys.tools.path.get_mechanical_path() - version = ansys.tools.path.version_from_path("mechanical", path) - - if misc.is_windows(): - assert "AnsysWBU.exe" in path - else: - assert ".workbench" in path - - assert re.match(r"\d{3}", str(version)) and version >= 232 - - -@pytest.mark.remote_session_launch -def test_change_default_mechanical_path(): - if pymechanical.mechanical.get_start_instance(): - path = ansys.tools.path.get_mechanical_path() - version = ansys.tools.path.version_from_path("mechanical", path) - - pymechanical.change_default_mechanical_path(path) - - path_new = ansys.tools.path.get_mechanical_path() - version_new = ansys.tools.path.version_from_path("mechanical", path) - - assert path_new == path - assert version_new == version - - -@pytest.mark.remote_session_launch -def test_version_from_path(): - windows_path = "C:\\Program Files\\ANSYS Inc\\v242\\aisol\\bin\\winx64\\AnsysWBU.exe" - version = ansys.tools.path.version_from_path("mechanical", windows_path) - assert version == 242 - - linux_path = "/usr/ansys_inc/v242/aisol/.workbench" - version = ansys.tools.path.version_from_path("mechanical", linux_path) - assert version == 242 - - with pytest.raises(RuntimeError): - # doesn't contain version - path = "C:\\Program Files\\ANSYS Inc\\aisol\\bin\\winx64\\AnsysWBU.exe" - ansys.tools.path.version_from_path("mechanical", path) - - -@pytest.mark.remote_session_launch -def test_valid_port(): - # no error thrown when everything is ok. - pymechanical.mechanical.check_valid_port(10000, 1000, 60000) - - with pytest.raises(ValueError): - pymechanical.mechanical.check_valid_port("10000") - - with pytest.raises(ValueError): - pymechanical.mechanical.check_valid_port(100, 1000, 60000) - - -@pytest.mark.remote_session_launch -def test_server_log_level(): - server_log_level = pymechanical.mechanical.Mechanical.convert_to_server_log_level("DEBUG") - assert 1 == server_log_level - - server_log_level = pymechanical.mechanical.Mechanical.convert_to_server_log_level("INFO") - assert 2 == server_log_level - - server_log_level = pymechanical.mechanical.Mechanical.convert_to_server_log_level("WARNING") - assert 3 == server_log_level - - server_log_level = pymechanical.mechanical.Mechanical.convert_to_server_log_level("ERROR") - assert 4 == server_log_level - - server_log_level = pymechanical.mechanical.Mechanical.convert_to_server_log_level("CRITICAL") - assert 5 == server_log_level - - with pytest.raises(ValueError): - pymechanical.mechanical.Mechanical.convert_to_server_log_level("NON_EXITING_LEVEL") - - -@pytest.mark.remote_session_launch -def test_launch_mechanical_non_existent_path(): - cwd = os.getcwd() - - if misc.is_windows(): - exec_file = os.path.join(cwd, "test", "AnsysWBU.exe") - else: - exec_file = os.path.join(cwd, "test", ".workbench") - - with pytest.raises(FileNotFoundError): - pymechanical.launch_mechanical(exec_file=exec_file) - - -@pytest.mark.remote_session_launch -def test_launch_grpc_not_supported_version(): - cwd = os.getcwd() - - if misc.is_windows(): - exec_file = os.path.join(cwd, "ANSYS Inc", "v230", "aisol", "bin", "win64", "AnsysWBU.exe") - else: - exec_file = os.path.join(cwd, "ansys_inc", "v230", "aisol", ".workbench") - - with pytest.raises(errors.VersionError): - pymechanical.mechanical.launch_grpc(exec_file=exec_file) - - -# def test_call_before_launch_or_connect(): -# import ansys.mechanical.core as pymechanical -# from ansys.mechanical.core.errors import MechanicalExitedError -# -# # we are not checking any valid value passed to each call, -# # we just verify an exception being raised. -# -# mechanical1 = pymechanical.launch_mechanical(start_instance=True) -# mechanical1.exit() -# -# error = "Mechanical has already exited." -# -# with pytest.raises(MechanicalExitedError, match=error): -# mechanical1.run_python_script("2+5") -# -# with pytest.raises(MechanicalExitedError, match=error): -# mechanical1.run_python_script_from_file("test.py") -# -# # currently we exit silently -# # with pytest.raises(ValueError, match=error): -# # mechanical.exit(force_exit=True) -# -# with pytest.raises(MechanicalExitedError, match=error): -# mechanical1.upload(file_name="test.x_t", file_location_destination="some_destination", -# chunk_size=1024) -# -# with pytest.raises(MechanicalExitedError, match=error): -# mechanical1.download(files="test.x_t", target_dir="some_local_directory", chunk_size=1024) +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import json +import os +import pathlib +import re + +import ansys.tools.path +import grpc +import pytest + +import ansys.mechanical.core as pymechanical +import ansys.mechanical.core.errors as errors +import ansys.mechanical.core.misc as misc +import conftest + + +@pytest.mark.remote_session_connect +def test_run_python_script_success(mechanical): + result = mechanical.run_python_script("2+3") + assert result == "5" + + +@pytest.mark.remote_session_connect +def test_run_python_script_success_return_empty(mechanical): + result = mechanical.run_python_script("ExtAPI.DataModel.Project") + if misc.is_windows(): + assert result == "" + else: + assert result == "Ansys.ACT.Automation.Mechanical.Project" + + +@pytest.mark.remote_session_connect +def test_run_python_script_error(mechanical): + with pytest.raises(grpc.RpcError) as exc_info: + mechanical.run_python_script("import test") + + assert exc_info.value.details() == "No module named test" + + +@pytest.mark.remote_session_connect +def test_run_python_from_file_success(mechanical): + current_working_directory = os.getcwd() + script_path = os.path.join( + current_working_directory, "tests", "scripts", "run_python_success.py" + ) + print("running python script : ", script_path) + result = mechanical.run_python_script_from_file(script_path) + + assert result == "test" + + +# @pytest.mark.remote_session_connect +# def test_run_python_from_file_log_messages(mechanical): +# current_working_directory = os.getcwd() +# script_path = os.path.join(current_working_directory, "tests", "scripts", "log_message.py") +# print("running python script : ", script_path) +# +# print("logging_not enabled") +# result = mechanical.run_python_script_from_file(script_path) +# +# print("logging_enabled") +# result = mechanical.run_python_script_from_file( +# script_path, enable_logging=True, log_level="DEBUG", progress_interval=1000 +# ) +# +# result = mechanical.run_python_script_from_file( +# script_path, enable_logging=True, log_level="INFO", progress_interval=1000 +# ) +# +# result = mechanical.run_python_script_from_file( +# script_path, enable_logging=True, log_level="WARNING", progress_interval=1000 +# ) +# +# result = mechanical.run_python_script_from_file( +# script_path, enable_logging=True, log_level="ERROR", progress_interval=1000 +# ) +# +# result = mechanical.run_python_script_from_file( +# script_path, enable_logging=True, log_level="CRITICAL", progress_interval=1000 +# ) +# +# assert result == "log_test" + + +@pytest.mark.remote_session_connect +def test_run_python_script_from_file_error(mechanical): + with pytest.raises(grpc.RpcError) as exc_info: + current_working_directory = os.getcwd() + script_path = os.path.join( + current_working_directory, "tests", "scripts", "run_python_error.py" + ) + print("running python script : ", script_path) + mechanical.run_python_script_from_file(script_path) + + assert exc_info.value.details() == "name 'get_myname' is not defined" + + +@pytest.mark.remote_session_connect +@pytest.mark.parametrize("file_name", [r"hsec.x_t"]) +def test_upload(mechanical, file_name, assets): + mechanical.run_python_script("ExtAPI.DataModel.Project.New()") + directory = mechanical.run_python_script("ExtAPI.DataModel.Project.ProjectDirectory") + print(directory) + + file_path = os.path.join(assets, file_name) + mechanical.upload( + file_name=file_path, file_location_destination=directory, chunk_size=1024 * 1024 + ) + + base_name = os.path.basename(file_path) + combined_path = os.path.join(directory, base_name) + file_path_modified = combined_path.replace("\\", "\\\\") + # we are working with iron python 2.7 on mechanical side + # use python 2.7 style formatting + # path = '%s' % file_path_modified + script = 'import os\nos.path.exists("%s")' % file_path_modified + print(script) + result = mechanical.run_python_script(script) + assert bool(result) + + +@pytest.mark.remote_session_connect +# we are using only a small test file +# change the chunk_size for that +# ideally this will be 64*1024, 1024*1024, etc. +@pytest.mark.parametrize("chunk_size", [10, 50, 100]) +def test_upload_with_different_chunk_size(mechanical, chunk_size, assets): + file_path = os.path.join(assets, "hsec.x_t") + mechanical.run_python_script("ExtAPI.DataModel.Project.New()") + directory = mechanical.run_python_script("ExtAPI.DataModel.Project.ProjectDirectory") + mechanical.upload( + file_name=file_path, file_location_destination=directory, chunk_size=chunk_size + ) + + +def get_solve_out_path(mechanical): + solve_out_path = "" + for file_path in mechanical.list_files(): + if file_path.find("solve.out") != -1: + solve_out_path = file_path + break + + return solve_out_path + + +def write_file_contents_to_console(path): + with open(path, "rt") as file: + for line in file: + print(line, end="") + + +def disable_distributed_solve(mechanical): + script = ( + 'ExtAPI.Application.SolveConfigurations["My Computer"].' + "SolveProcessSettings.DistributeSolution = False" + ) + mechanical.run_python_script(script) + + +def enable_distributed_solve(mechanical): + script = ( + 'ExtAPI.Application.SolveConfigurations["My Computer"].' + "SolveProcessSettings.DistributeSolution = True" + ) + mechanical.run_python_script(script) + + +def solve_and_return_results(mechanical): + current_working_directory = os.getcwd() + file_path = os.path.join(current_working_directory, "tests", "assets", "hsec.x_t") + + mechanical.clear() + directory = mechanical.project_directory + mechanical.upload( + file_name=file_path, file_location_destination=directory, chunk_size=1024 * 1024 + ) + + python_script = os.path.join(current_working_directory, "tests", "scripts", "api.py") + + text_file = open(python_script, "r") + # read whole file to a string + data = text_file.read() + # close file + text_file.close() + + file_path_string = "\n" + + # let us append the scripts to run + func_to_call = """ +import os +directory = ExtAPI.DataModel.Project.ProjectDirectory +file_path_modified=os.path.join(directory,'hsec.x_t') +attach_geometry(file_path_modified) +generate_mesh() +add_static_structural_analysis_bc_results() +solve_model() +return_total_deformation() + """ + python_script = data + file_path_string + func_to_call + + result = mechanical.run_python_script( + python_script, enable_logging=True, log_level="INFO", progress_interval=1000 + ) + + # if solve fails, solve.out contains enough information + solve_out_path = get_solve_out_path(mechanical) + + if solve_out_path != "": + print(f"downloading {solve_out_path} from server") + print(f"downloading to {current_working_directory}") + solve_out_local_path_list = mechanical.download( + solve_out_path, target_dir=current_working_directory + ) + solve_out_local_path = solve_out_local_path_list[0] + print(solve_out_local_path) + + write_file_contents_to_console(solve_out_local_path) + + # done with solve.out - remove it + os.remove(solve_out_local_path) + + return result + + +def verify_project_download(mechanical, tmpdir): + files = mechanical.list_files() + number_of_files = len(files) + + print("files available: ") + for file in files: + print(file) + assert number_of_files > 0 + + # download the project + project_directory = mechanical.project_directory + print(f"project directory: {project_directory}") + + target_dir = os.path.join(tmpdir, "mechanical_project") + # add a trailing path separator + target_dir = os.path.join(target_dir, "") + print(f"creating target directory {target_dir}") + if not os.path.exists(target_dir): + os.mkdir(target_dir) + + out_files = mechanical.download_project(target_dir=target_dir) + print("downloaded files:") + for file in out_files: + print(file) + assert os.path.exists(file) and os.path.getsize(file) > 0 + + files = mechanical.list_files() + assert len(files) == len(out_files) + + target_dir = os.path.join(tmpdir, "mechanical_project2") + # add a trailing path separator + target_dir = os.path.join(target_dir, "") + print(f"creating target directory {target_dir}") + if not os.path.exists(target_dir): + os.mkdir(target_dir) + + # project not saved. + # no mechdb available. + extensions = ["mechdb"] + with pytest.raises(ValueError): + mechanical.download_project(extensions=extensions, target_dir=target_dir) + + extensions = ["xml", "rst"] + out_files = mechanical.download_project(extensions=extensions, target_dir=target_dir) + print(f"downloaded files for extensions: {extensions}") + for file in out_files: + print(file) + assert os.path.exists(file) and os.path.getsize(file) > 0 + extension = pathlib.Path(file).suffix + extension_without_dot = extension[1:] + assert extension_without_dot in extensions + + +@pytest.mark.remote_session_connect +# @pytest.mark.wip +# @pytest.mark.skip(reason="avoid long running") +def test_upload_attach_mesh_solve_use_api_non_distributed_solve(mechanical, tmpdir): + # default is distributed solve + # let's disable the distributed solve and then solve + # enable the distributed solve back + + # this test could run under a container with 1 cpu + # let us disable distributed solve + disable_distributed_solve(mechanical) + + result = solve_and_return_results(mechanical) + + # revert back to distributed solve + enable_distributed_solve(mechanical) + + dict_result = json.loads(result) + + min_value = float(dict_result["Minimum"].split(" ")[0]) + max_value = float(dict_result["Maximum"].split(" ")[0]) + avg_value = float(dict_result["Average"].split(" ")[0]) + + print(f"min_value = {min_value} max_value = {max_value} avg_value = {avg_value}") + + result = mechanical.run_python_script("ExtAPI.DataModel.Project.Model.Analyses[0].ObjectState") + assert "5" == result.lower() + + verify_project_download(mechanical, tmpdir) + + +@pytest.mark.remote_session_connect +def test_upload_attach_mesh_solve_use_api_distributed_solve(mechanical, tmpdir): + # default is distributed solve + + result = solve_and_return_results(mechanical) + + dict_result = json.loads(result) + + min_value = float(dict_result["Minimum"].split(" ")[0]) + max_value = float(dict_result["Maximum"].split(" ")[0]) + avg_value = float(dict_result["Average"].split(" ")[0]) + + print(f"min_value = {min_value} max_value = {max_value} avg_value = {avg_value}") + + result = mechanical.run_python_script("ExtAPI.DataModel.Project.Model.Analyses[0].ObjectState") + assert "5" == result.lower() + + verify_project_download(mechanical, tmpdir) + + +def verify_download(mechanical, tmpdir, file_name, chunk_size): + directory = mechanical.run_python_script("ExtAPI.DataModel.Project.ProjectDirectory") + print(directory) + + current_working_directory = os.getcwd() + file_path = os.path.join(current_working_directory, "tests", "assets", file_name) + mechanical.upload( + file_name=file_path, file_location_destination=directory, chunk_size=1024 * 1024 + ) + + print(f"using the temporary directory: {tmpdir}") + file_path = os.path.join(directory, file_name) + local_directory = tmpdir.strpath + + # test with different download chunk_size + local_path_list = mechanical.download( + files=file_path, target_dir=local_directory, chunk_size=chunk_size + ) + print("downloaded files:") + for local_path in local_path_list: + print(f" downloaded file: {local_path}") + assert os.path.exists(local_path) and os.path.getsize(local_path) > 0 + + +@pytest.mark.remote_session_connect +# @pytest.mark.wip +@pytest.mark.parametrize("file_name", ["hsec.x_t"]) +def test_download_file(mechanical, tmpdir, file_name): + verify_download(mechanical, tmpdir, file_name, 1024 * 1024) + + +@pytest.mark.remote_session_connect +# we are using only a small test file +# change the chunk_size for that +# ideally this will be 64*1024, 1024*1024, etc. +@pytest.mark.parametrize("chunk_size", [10, 50, 100]) +def test_download_file_different_chunk_size1(mechanical, tmpdir, chunk_size): + file_name = "hsec.x_t" + + verify_download(mechanical, tmpdir, file_name, chunk_size) + + +@pytest.mark.remote_session_launch +def test_launch_meshing_mode(mechanical_meshing): + result = mechanical_meshing.run_python_script("2+3") + assert result == "5" + + +@pytest.mark.remote_session_launch +def test_launch_result_mode(mechanical_result): + result = mechanical_result.run_python_script("2+3") + assert result == "5" + + +@pytest.mark.remote_session_launch +def test_close_all_Local_instances(tmpdir): + list_ports = [] + mechanical = conftest.launch_mechanical_instance(cleanup_on_exit=False) + print(mechanical.name) + list_ports.append(mechanical._port) + + # connect to the launched instance + mechanical2 = conftest.connect_to_mechanical_instance(mechanical._port, clear_on_connect=True) + print(mechanical2.name) + + test_upload_attach_mesh_solve_use_api_non_distributed_solve(mechanical2, tmpdir) + + # use the settings and launch another Mechanical instance + mechanical.launch(cleanup_on_exit=False) + print(mechanical.name) + list_ports.append(mechanical._port) + + pymechanical.close_all_local_instances(list_ports, use_thread=False) + for value in list_ports: + assert value not in pymechanical.LOCAL_PORTS + + +@pytest.mark.remote_session_launch +def test_find_mechanical_path(): + if pymechanical.mechanical.get_start_instance(): + path = ansys.tools.path.get_mechanical_path() + version = ansys.tools.path.version_from_path("mechanical", path) + + if misc.is_windows(): + assert "AnsysWBU.exe" in path + else: + assert ".workbench" in path + + assert re.match(r"\d{3}", str(version)) and version >= 232 + + +@pytest.mark.remote_session_launch +def test_change_default_mechanical_path(): + if pymechanical.mechanical.get_start_instance(): + path = ansys.tools.path.get_mechanical_path() + version = ansys.tools.path.version_from_path("mechanical", path) + + pymechanical.change_default_mechanical_path(path) + + path_new = ansys.tools.path.get_mechanical_path() + version_new = ansys.tools.path.version_from_path("mechanical", path) + + assert path_new == path + assert version_new == version + + +@pytest.mark.remote_session_launch +def test_version_from_path(): + windows_path = "C:\\Program Files\\ANSYS Inc\\v251\\aisol\\bin\\winx64\\AnsysWBU.exe" + version = ansys.tools.path.version_from_path("mechanical", windows_path) + assert version == 251 + + linux_path = "/usr/ansys_inc/v251/aisol/.workbench" + version = ansys.tools.path.version_from_path("mechanical", linux_path) + assert version == 251 + + with pytest.raises(RuntimeError): + # doesn't contain version + path = "C:\\Program Files\\ANSYS Inc\\aisol\\bin\\winx64\\AnsysWBU.exe" + ansys.tools.path.version_from_path("mechanical", path) + + +@pytest.mark.remote_session_launch +def test_valid_port(): + # no error thrown when everything is ok. + pymechanical.mechanical.check_valid_port(10000, 1000, 60000) + + with pytest.raises(ValueError): + pymechanical.mechanical.check_valid_port("10000") + + with pytest.raises(ValueError): + pymechanical.mechanical.check_valid_port(100, 1000, 60000) + + +@pytest.mark.remote_session_launch +def test_server_log_level(): + server_log_level = pymechanical.mechanical.Mechanical.convert_to_server_log_level("DEBUG") + assert 1 == server_log_level + + server_log_level = pymechanical.mechanical.Mechanical.convert_to_server_log_level("INFO") + assert 2 == server_log_level + + server_log_level = pymechanical.mechanical.Mechanical.convert_to_server_log_level("WARNING") + assert 3 == server_log_level + + server_log_level = pymechanical.mechanical.Mechanical.convert_to_server_log_level("ERROR") + assert 4 == server_log_level + + server_log_level = pymechanical.mechanical.Mechanical.convert_to_server_log_level("CRITICAL") + assert 5 == server_log_level + + with pytest.raises(ValueError): + pymechanical.mechanical.Mechanical.convert_to_server_log_level("NON_EXITING_LEVEL") + + +@pytest.mark.remote_session_launch +def test_launch_mechanical_non_existent_path(): + cwd = os.getcwd() + + if misc.is_windows(): + exec_file = os.path.join(cwd, "test", "AnsysWBU.exe") + else: + exec_file = os.path.join(cwd, "test", ".workbench") + + with pytest.raises(FileNotFoundError): + pymechanical.launch_mechanical(exec_file=exec_file) + + +@pytest.mark.remote_session_launch +def test_launch_grpc_not_supported_version(): + cwd = os.getcwd() + + if misc.is_windows(): + exec_file = os.path.join(cwd, "ANSYS Inc", "v230", "aisol", "bin", "win64", "AnsysWBU.exe") + else: + exec_file = os.path.join(cwd, "ansys_inc", "v230", "aisol", ".workbench") + + with pytest.raises(errors.VersionError): + pymechanical.mechanical.launch_grpc(exec_file=exec_file) + + +# def test_call_before_launch_or_connect(): +# import ansys.mechanical.core as pymechanical +# from ansys.mechanical.core.errors import MechanicalExitedError +# +# # we are not checking any valid value passed to each call, +# # we just verify an exception being raised. +# +# mechanical1 = pymechanical.launch_mechanical(start_instance=True) +# mechanical1.exit() +# +# error = "Mechanical has already exited." +# +# with pytest.raises(MechanicalExitedError, match=error): +# mechanical1.run_python_script("2+5") +# +# with pytest.raises(MechanicalExitedError, match=error): +# mechanical1.run_python_script_from_file("test.py") +# +# # currently we exit silently +# # with pytest.raises(ValueError, match=error): +# # mechanical.exit(force_exit=True) +# +# with pytest.raises(MechanicalExitedError, match=error): +# mechanical1.upload(file_name="test.x_t", file_location_destination="some_destination", +# chunk_size=1024) +# +# with pytest.raises(MechanicalExitedError, match=error): +# mechanical1.download(files="test.x_t", target_dir="some_local_directory", chunk_size=1024) From 810bf042423785c5ecdb10352fc41294bab01dc9 Mon Sep 17 00:00:00 2001 From: PyAnsys CI Bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Thu, 16 Jan 2025 14:46:15 +0100 Subject: [PATCH 43/44] chore: update CHANGELOG for v0.11.12 (#1050) --- doc/changelog.d/1031.maintenance.md | 1 - doc/changelog.d/1036.maintenance.md | 1 - doc/changelog.d/1037.added.md | 1 - doc/changelog.d/1038.added.md | 1 - doc/changelog.d/1039.maintenance.md | 1 - doc/changelog.d/1042.added.md | 1 - doc/changelog.d/1044.maintenance.md | 1 - doc/changelog.d/1045.maintenance.md | 1 - doc/changelog.d/1048.maintenance.md | 1 - doc/changelog.d/1050.maintenance.md | 1 + doc/source/changelog.rst | 21 +++++++++++++++++++++ 11 files changed, 22 insertions(+), 9 deletions(-) delete mode 100644 doc/changelog.d/1031.maintenance.md delete mode 100644 doc/changelog.d/1036.maintenance.md delete mode 100644 doc/changelog.d/1037.added.md delete mode 100644 doc/changelog.d/1038.added.md delete mode 100644 doc/changelog.d/1039.maintenance.md delete mode 100644 doc/changelog.d/1042.added.md delete mode 100644 doc/changelog.d/1044.maintenance.md delete mode 100644 doc/changelog.d/1045.maintenance.md delete mode 100644 doc/changelog.d/1048.maintenance.md create mode 100644 doc/changelog.d/1050.maintenance.md diff --git a/doc/changelog.d/1031.maintenance.md b/doc/changelog.d/1031.maintenance.md deleted file mode 100644 index 822e820a0..000000000 --- a/doc/changelog.d/1031.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -update CHANGELOG for v0.11.11 \ No newline at end of file diff --git a/doc/changelog.d/1036.maintenance.md b/doc/changelog.d/1036.maintenance.md deleted file mode 100644 index 46cb383cb..000000000 --- a/doc/changelog.d/1036.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -Bump the doc group with 2 updates \ No newline at end of file diff --git a/doc/changelog.d/1037.added.md b/doc/changelog.d/1037.added.md deleted file mode 100644 index 741cac2d8..000000000 --- a/doc/changelog.d/1037.added.md +++ /dev/null @@ -1 +0,0 @@ -Update enum and globals \ No newline at end of file diff --git a/doc/changelog.d/1038.added.md b/doc/changelog.d/1038.added.md deleted file mode 100644 index 093e54d4f..000000000 --- a/doc/changelog.d/1038.added.md +++ /dev/null @@ -1 +0,0 @@ -add poster method that raises an exception \ No newline at end of file diff --git a/doc/changelog.d/1039.maintenance.md b/doc/changelog.d/1039.maintenance.md deleted file mode 100644 index c655a0905..000000000 --- a/doc/changelog.d/1039.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -pre-commit automatic update \ No newline at end of file diff --git a/doc/changelog.d/1042.added.md b/doc/changelog.d/1042.added.md deleted file mode 100644 index 191f1e201..000000000 --- a/doc/changelog.d/1042.added.md +++ /dev/null @@ -1 +0,0 @@ -docker and ci/cd change for 25R1 \ No newline at end of file diff --git a/doc/changelog.d/1044.maintenance.md b/doc/changelog.d/1044.maintenance.md deleted file mode 100644 index 6567a4e14..000000000 --- a/doc/changelog.d/1044.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -Bump `ansys-mechanical-stubs` from 0.1.5 to 0.1.6 \ No newline at end of file diff --git a/doc/changelog.d/1045.maintenance.md b/doc/changelog.d/1045.maintenance.md deleted file mode 100644 index 10865a473..000000000 --- a/doc/changelog.d/1045.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -Update default product version to 25R1 \ No newline at end of file diff --git a/doc/changelog.d/1048.maintenance.md b/doc/changelog.d/1048.maintenance.md deleted file mode 100644 index bd87b206c..000000000 --- a/doc/changelog.d/1048.maintenance.md +++ /dev/null @@ -1 +0,0 @@ -Bump `ansys-mechanical-env` version from `0.1.8` to `0.1.9` \ No newline at end of file diff --git a/doc/changelog.d/1050.maintenance.md b/doc/changelog.d/1050.maintenance.md new file mode 100644 index 000000000..adc7db049 --- /dev/null +++ b/doc/changelog.d/1050.maintenance.md @@ -0,0 +1 @@ +update CHANGELOG for v0.11.12 \ No newline at end of file diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 4db155c06..16da79bd5 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -9,6 +9,27 @@ This document contains the release notes for the project. .. towncrier release notes start +`0.11.12 `_ - 2025-01-16 +===================================================================================== + +Added +^^^^^ + +- Update enum and globals `#1037 `_ +- add poster method that raises an exception `#1038 `_ +- docker and ci/cd change for 25R1 `#1042 `_ + + +Maintenance +^^^^^^^^^^^ + +- update CHANGELOG for v0.11.11 `#1031 `_ +- Bump the doc group with 2 updates `#1036 `_ +- pre-commit automatic update `#1039 `_ +- Bump `ansys-mechanical-stubs` from 0.1.5 to 0.1.6 `#1044 `_ +- Update default product version to 25R1 `#1045 `_ +- Bump `ansys-mechanical-env` version from `0.1.8` to `0.1.9` `#1048 `_ + `0.11.11 `_ - 2025-01-08 ===================================================================================== From 05c268f22a6bb55c5c667b41d1df8d94930f7ce6 Mon Sep 17 00:00:00 2001 From: Dipin <26918585+dipinknair@users.noreply.github.com> Date: Thu, 16 Jan 2025 08:52:11 -0600 Subject: [PATCH 44/44] FEAT: Add CPython feature flag for `ansys-mechanical` cli (#1049) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/1049.added.md | 1 + .../getting_started/running_mechanical.rst | 566 +++++++++--------- src/ansys/mechanical/core/feature_flags.py | 1 + tests/test_cli.py | 10 +- 4 files changed, 294 insertions(+), 284 deletions(-) create mode 100644 doc/changelog.d/1049.added.md diff --git a/doc/changelog.d/1049.added.md b/doc/changelog.d/1049.added.md new file mode 100644 index 000000000..521e72b9f --- /dev/null +++ b/doc/changelog.d/1049.added.md @@ -0,0 +1 @@ +Add CPython feature flag for `ansys-mechanical` cli \ No newline at end of file diff --git a/doc/source/getting_started/running_mechanical.rst b/doc/source/getting_started/running_mechanical.rst index 9abf6ca47..f9c4d2ad0 100644 --- a/doc/source/getting_started/running_mechanical.rst +++ b/doc/source/getting_started/running_mechanical.rst @@ -1,283 +1,283 @@ -.. _using_standard_install: - -Launching PyMechanical -====================== - -The ``ansys-mechanical-core`` package requires either a local or -remote instance of Mechanical to communicate with. This page describes -how Mechanical is installed from the Ansys standard installer and -describes how you launch and interface with Mechanical from Python. - -Install Mechanical ------------------- - -Mechanical is installed by default from the Ansys standard installer. -When you run the standard installer, look under the **Structural Mechanics** -heading to verify that the **Mechanical Products** checkbox is selected. -Although options in the standard installer might change, this image provides -a reference: - -.. figure:: ../images/unified_install_2023R1.jpg - :width: 400pt - -Launch a remote Mechanical session ----------------------------------- - -You can use PyMechanical to launch a Mechanical session on the local machine -Python is running on. Alternatively, you can run Mechanical's command line -directly on any machine to start Mechanical in server mode and then use its -IP address to manually connect to it from Python. - -Launch Mechanical on the local machine using Python -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When Mechanical is installed locally on your machine, you can use the -`launch_mechanical() <../api/ansys/mechanical/core/mechanical/index.html#mechanical.launch_mechanical>`_ -method to launch and automatically connect to Mechanical. While this method provides the -easiest and fastest way to launch Mechanical, it only works with a local Mechanical installation. - -Launch Mechanical locally with this code: - -.. code:: pycon - - >>> from ansys.mechanical.core import launch_mechanical - >>> mechanical = launch_mechanical() - >>> mechanical - - Ansys Mechanical [Ansys Mechanical Enterprise] - Product Version:251 - Software build date: 11/27/2024 09:34:44 - -Launch Mechanical from the command line -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The `ansys-mechanical` utility is installed automatically with PyMechanical, -and can be used to run Mechanical from the command line. To obtain help on -usage, type the following command: - -.. code:: console - - $ ansys-mechanical --help - - Usage: ansys-mechanical [OPTIONS] - - CLI tool to run mechanical. - - USAGE: - - The following example demonstrates the main use of this tool: - - $ ansys-mechanical -r 251 -g - - Starting Ansys Mechanical version 2025R1 in graphical mode... - - Options: - -h, --help Show this message and exit. - -p, --project-file TEXT Opens Mechanical project file (.mechdb). Cannot - be mixed with -i - --private-appdata Make the appdata folder private. This enables you - to run parallel instances of Mechanical. - --port INTEGER Start mechanical in server mode with the given - port number - -i, --input-script TEXT Name of the input Python script. Cannot be mixed - with -p - --features TEXT Beta feature flags to set, as a semicolon - delimited list. Options: ['MultistageHarmonic', - 'ThermalShells'] - --exit Exit the application after running an input - script. You can only use this command with - --input-script argument (-i). The command - defaults to true you are not running the - application in graphical mode. The ``exit`` - command is only supported in version 2024 R1 or - later. - -s, --show-welcome-screen Show the welcome screen. You use this screen to - open a file. This argument only affects the - application when in graphical mode. - --debug Show a debug dialog window at the start of the - process. - -r, --revision INTEGER Ansys Revision number, e.g. "232", "241", "242" or "251". - If none is specified, uses the default from ansys- - tools-path - -g, --graphical Graphical mode - - ... - -You can launch Mechanical in server mode from the command line and then -manually connect to the server. Use the `port` argument to select the port. - -.. code:: - - ansys-mechanical --port 10000 - -Connect to a Mechanical session -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can connect to a Mechanical session from the same host or from an external host. - -Assuming that Mechanical is running locally at the default IP address (``127.0.0.1``) on the -default port (``10000``), you would use this code to connect to it with this code: - -.. code:: python - - from ansys.mechanical.core import Mechanical - - mechanical = Mechanical() - -Alternatively, you can use the -`connect_to_mechanical() <../api/ansys/mechanical/core/mechanical/index.html#mechanical.connect_to_mechanical>`_ -for same functionality. - -.. code:: python - - from ansys.mechanical.core import connect_to_mechanical - - mechanical = connect_to_mechanical() - - -Now assume that a remote instance of Mechanical has been started in server mode. To connect to -the computer on your local area network that is running Mechanical, you can use either -an IP address and port or a hostname and port. - -**IP address and port** - -Assume that Mechanical is running remotely at IP address ``192.168.0.1`` on port ``10000``. - -You would connect to it with this code: - -.. code:: python - - mechanical = Mechanical("192.168.0.1", port=10000) - -or - -.. code:: python - - mechanical = connect_to_mechanical("192.168.0.1", port=10000) - -**Hostname and port** - -Assume that Mechanical is running remotely at hostname ``myremotemachine`` on port ``10000``. - -You would connect to it with this code: - -.. code:: python - - mechanical = Mechanical("myremotemachine", port=10000) - -or - -.. code:: python - - mechanical = connect_to_mechanical("myremotemachine", port=10000) - -Launching issues ----------------- - -For any number of reasons, launching Mechanical can fail. Some approaches -follow for debugging launch failures. - -Manually set the location of the executable file -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you have a non-standard installation of Mechanical, PyMechanical might -not be able to find your installation. In this case, you should manually -set the location of your Mechanical executable file as the first parameter -for the `launch_mechanical()`_ method. - -**On Windows** - -.. code:: python - - from ansys.mechanical.core import launch_mechanical - - exec_loc = "C:/Program Files/ANSYS Inc/v251/aisol/bin/winx64/AnsysWBU.exe" - mechanical = launch_mechanical(exec_loc) - -**On Linux** - -.. code:: python - - from ansys.mechanical.core import launch_mechanical - - exec_loc = "/usr/ansys_inc/v251/aisol/.workbench" - mechanical = launch_mechanical(exec_loc) - -If, when using the `launch_mechanical()`_ -method, Mechanical still fails to launch or hangs while launching, pass the -``verbose_mechanical=True`` parameter. This prints the output of Mechanical in the Python console. -You can then use this output to debug why Mechanical isn't launching. - -.. Note:: - - On Windows, output is limited because of the way Mechanical launches. - -Debug from the command line -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You may need to run the ``launch`` command from the command line to debug why Mechanical is not launching. -running the launch command from the command line. - -Open a terminal and run the following command: - -.. code:: console - - ansys-mechanical -g --port 10000 - -If the preceding command for your operating system doesn't launch Mechanical, you might have -a variety of issues, including: - -- License server setup -- Running behind a VPN -- Missing dependencies - -Embed a Mechanical instance ---------------------------- - -The instructions for embedding a Mechanical instance are different on -Windows and Linux. While the Python code is the same in both cases, -Linux requires some additional environment variables. - -Python code -~~~~~~~~~~~ - -.. code:: pycon - - >>> from ansys.mechanical.core import App - >>> mechanical = App() - >>> mechanical - Ansys Mechanical [Ansys Mechanical Enterprise] - Product Version:251 - Software build date: 11/27/2024 09:34:44 - -Additional information for Linux -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Starting with 2023 R2, it is possible to embed an instance of Mechanical on Linux. -However, because of differences in how Mechanical works on Linux, you cannot simply -run Python as usual. On Linux, certain environment variables must be set for the Python -process before it starts. You can set up these environment variables using the ``mechanical-env`` -script which is part of PyMechanical - -.. code:: shell - - $ mechanical-env python - -Licensing issues ----------------- - -`PADT `_ has an `Ansys `_ -product section. Posts about licensing are common. - -If you are responsible for maintaining an Ansys license or have a personal installation -of Ansys, you likely can access the -`Licensing `_ -section of the Ansys Help, where you can view or download the *Ansys, Inc. Licensing Guide* for -comprehensive licensing information. - -VPN issues ----------- - -Sometimes, Mechanical has issues starting when VPN software is running. For more information, -access the `Mechanical Users Guide`_ -in the Ansys Help. +.. _using_standard_install: + +Launching PyMechanical +====================== + +The ``ansys-mechanical-core`` package requires either a local or +remote instance of Mechanical to communicate with. This page describes +how Mechanical is installed from the Ansys standard installer and +describes how you launch and interface with Mechanical from Python. + +Install Mechanical +------------------ + +Mechanical is installed by default from the Ansys standard installer. +When you run the standard installer, look under the **Structural Mechanics** +heading to verify that the **Mechanical Products** checkbox is selected. +Although options in the standard installer might change, this image provides +a reference: + +.. figure:: ../images/unified_install_2023R1.jpg + :width: 400pt + +Launch a remote Mechanical session +---------------------------------- + +You can use PyMechanical to launch a Mechanical session on the local machine +Python is running on. Alternatively, you can run Mechanical's command line +directly on any machine to start Mechanical in server mode and then use its +IP address to manually connect to it from Python. + +Launch Mechanical on the local machine using Python +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When Mechanical is installed locally on your machine, you can use the +`launch_mechanical() <../api/ansys/mechanical/core/mechanical/index.html#mechanical.launch_mechanical>`_ +method to launch and automatically connect to Mechanical. While this method provides the +easiest and fastest way to launch Mechanical, it only works with a local Mechanical installation. + +Launch Mechanical locally with this code: + +.. code:: pycon + + >>> from ansys.mechanical.core import launch_mechanical + >>> mechanical = launch_mechanical() + >>> mechanical + + Ansys Mechanical [Ansys Mechanical Enterprise] + Product Version:251 + Software build date: 11/27/2024 09:34:44 + +Launch Mechanical from the command line +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The `ansys-mechanical` utility is installed automatically with PyMechanical, +and can be used to run Mechanical from the command line. To obtain help on +usage, type the following command: + +.. code:: console + + $ ansys-mechanical --help + + Usage: ansys-mechanical [OPTIONS] + + CLI tool to run mechanical. + + USAGE: + + The following example demonstrates the main use of this tool: + + $ ansys-mechanical -r 251 -g + + Starting Ansys Mechanical version 2025R1 in graphical mode... + + Options: + -h, --help Show this message and exit. + -p, --project-file TEXT Opens Mechanical project file (.mechdb). Cannot + be mixed with -i + --private-appdata Make the appdata folder private. This enables you + to run parallel instances of Mechanical. + --port INTEGER Start mechanical in server mode with the given + port number + -i, --input-script TEXT Name of the input Python script. Cannot be mixed + with -p + --features TEXT Beta feature flags to set, as a semicolon + delimited list. Options: ['MultistageHarmonic', + 'ThermalShells', 'CPython'] + --exit Exit the application after running an input + script. You can only use this command with + --input-script argument (-i). The command + defaults to true you are not running the + application in graphical mode. The ``exit`` + command is only supported in version 2024 R1 or + later. + -s, --show-welcome-screen Show the welcome screen. You use this screen to + open a file. This argument only affects the + application when in graphical mode. + --debug Show a debug dialog window at the start of the + process. + -r, --revision INTEGER Ansys Revision number, e.g. "232", "241", "242" or "251". + If none is specified, uses the default from ansys- + tools-path + -g, --graphical Graphical mode + + ... + +You can launch Mechanical in server mode from the command line and then +manually connect to the server. Use the `port` argument to select the port. + +.. code:: + + ansys-mechanical --port 10000 + +Connect to a Mechanical session +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can connect to a Mechanical session from the same host or from an external host. + +Assuming that Mechanical is running locally at the default IP address (``127.0.0.1``) on the +default port (``10000``), you would use this code to connect to it with this code: + +.. code:: python + + from ansys.mechanical.core import Mechanical + + mechanical = Mechanical() + +Alternatively, you can use the +`connect_to_mechanical() <../api/ansys/mechanical/core/mechanical/index.html#mechanical.connect_to_mechanical>`_ +for same functionality. + +.. code:: python + + from ansys.mechanical.core import connect_to_mechanical + + mechanical = connect_to_mechanical() + + +Now assume that a remote instance of Mechanical has been started in server mode. To connect to +the computer on your local area network that is running Mechanical, you can use either +an IP address and port or a hostname and port. + +**IP address and port** + +Assume that Mechanical is running remotely at IP address ``192.168.0.1`` on port ``10000``. + +You would connect to it with this code: + +.. code:: python + + mechanical = Mechanical("192.168.0.1", port=10000) + +or + +.. code:: python + + mechanical = connect_to_mechanical("192.168.0.1", port=10000) + +**Hostname and port** + +Assume that Mechanical is running remotely at hostname ``myremotemachine`` on port ``10000``. + +You would connect to it with this code: + +.. code:: python + + mechanical = Mechanical("myremotemachine", port=10000) + +or + +.. code:: python + + mechanical = connect_to_mechanical("myremotemachine", port=10000) + +Launching issues +---------------- + +For any number of reasons, launching Mechanical can fail. Some approaches +follow for debugging launch failures. + +Manually set the location of the executable file +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you have a non-standard installation of Mechanical, PyMechanical might +not be able to find your installation. In this case, you should manually +set the location of your Mechanical executable file as the first parameter +for the `launch_mechanical()`_ method. + +**On Windows** + +.. code:: python + + from ansys.mechanical.core import launch_mechanical + + exec_loc = "C:/Program Files/ANSYS Inc/v251/aisol/bin/winx64/AnsysWBU.exe" + mechanical = launch_mechanical(exec_loc) + +**On Linux** + +.. code:: python + + from ansys.mechanical.core import launch_mechanical + + exec_loc = "/usr/ansys_inc/v251/aisol/.workbench" + mechanical = launch_mechanical(exec_loc) + +If, when using the `launch_mechanical()`_ +method, Mechanical still fails to launch or hangs while launching, pass the +``verbose_mechanical=True`` parameter. This prints the output of Mechanical in the Python console. +You can then use this output to debug why Mechanical isn't launching. + +.. Note:: + + On Windows, output is limited because of the way Mechanical launches. + +Debug from the command line +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You may need to run the ``launch`` command from the command line to debug why Mechanical is not launching. +running the launch command from the command line. + +Open a terminal and run the following command: + +.. code:: console + + ansys-mechanical -g --port 10000 + +If the preceding command for your operating system doesn't launch Mechanical, you might have +a variety of issues, including: + +- License server setup +- Running behind a VPN +- Missing dependencies + +Embed a Mechanical instance +--------------------------- + +The instructions for embedding a Mechanical instance are different on +Windows and Linux. While the Python code is the same in both cases, +Linux requires some additional environment variables. + +Python code +~~~~~~~~~~~ + +.. code:: pycon + + >>> from ansys.mechanical.core import App + >>> mechanical = App() + >>> mechanical + Ansys Mechanical [Ansys Mechanical Enterprise] + Product Version:251 + Software build date: 11/27/2024 09:34:44 + +Additional information for Linux +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Starting with 2023 R2, it is possible to embed an instance of Mechanical on Linux. +However, because of differences in how Mechanical works on Linux, you cannot simply +run Python as usual. On Linux, certain environment variables must be set for the Python +process before it starts. You can set up these environment variables using the ``mechanical-env`` +script which is part of PyMechanical + +.. code:: shell + + $ mechanical-env python + +Licensing issues +---------------- + +`PADT `_ has an `Ansys `_ +product section. Posts about licensing are common. + +If you are responsible for maintaining an Ansys license or have a personal installation +of Ansys, you likely can access the +`Licensing `_ +section of the Ansys Help, where you can view or download the *Ansys, Inc. Licensing Guide* for +comprehensive licensing information. + +VPN issues +---------- + +Sometimes, Mechanical has issues starting when VPN software is running. For more information, +access the `Mechanical Users Guide`_ +in the Ansys Help. diff --git a/src/ansys/mechanical/core/feature_flags.py b/src/ansys/mechanical/core/feature_flags.py index 813911be9..bc1c7e2da 100644 --- a/src/ansys/mechanical/core/feature_flags.py +++ b/src/ansys/mechanical/core/feature_flags.py @@ -31,6 +31,7 @@ class FeatureFlags: ThermalShells = "Mechanical.ThermalShells" MultistageHarmonic = "Mechanical.MultistageHarmonic" + CPython = "Mechanical.CPython.Capability" def get_feature_flag_names() -> typing.List[str]: diff --git a/tests/test_cli.py b/tests/test_cli.py index 71dd0a5f9..5a90d53b1 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -199,8 +199,16 @@ def test_cli_features(disable_cli, pytestconfig): args, _ = _cli_impl(exe="AnsysWBU.exe", version=version, features="a;b;c", port=11) assert "-featureflags" in args assert "a;b;c" in args - args, _ = _cli_impl(exe="AnsysWBU.exe", version=version, features="MultistageHarmonic", port=11) + args, _ = _cli_impl( + exe="AnsysWBU.exe", + version=version, + features="ThermalShells;MultistageHarmonic;CPython", + port=11, + ) + args = [arg for arg in args if arg.startswith("Mechanical")][0] + assert "Mechanical.ThermalShells" in args assert "Mechanical.MultistageHarmonic" in args + assert "Mechanical.CPython.Capability" in args @pytest.mark.cli