diff --git a/.github/workflows/build_documentation.yml b/.github/workflows/build_documentation.yml deleted file mode 100644 index a0e60ecf30a..00000000000 --- a/.github/workflows/build_documentation.yml +++ /dev/null @@ -1,111 +0,0 @@ -name: Documentation Build - -on: [pull_request, workflow_dispatch] - -env: - # Following env vars when changed will "reset" the mentioned cache, - # by changing the cache file name. It is rendered as ...-v%RESET_XXX%-... - # You should go up in number, if you go down (or repeat a previous value) - # you might end up reusing a previous cache if it haven't been deleted already. - # It applies 7 days retention policy by default. - RESET_EXAMPLES_CACHE: 3 - RESET_DOC_BUILD_CACHE: 3 - RESET_AUTOSUMMARY_CACHE: 3 - - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - docs-style: - name: "Check documentation style" - runs-on: ubuntu-latest - steps: - - name: "Check documentation style" - uses: ansys/actions/doc-style@v5 - with: - token: ${{ secrets.GITHUB_TOKEN }} - vale-config: "doc/.vale.ini" - vale-version: "2.29.6" - - docs_build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.10" - - - name: Update pip - run: | - pip install --upgrade pip - - - name: Install pyaedt - run: | - pip install .[doc] - - - name: Verify pyaedt can be imported - run: python -c "import pyaedt" - - - name: Retrieve PyAEDT version - id: version - run: | - echo "PYAEDT_VERSION=$(python -c 'from pyaedt import __version__; print(__version__)')" >> $GITHUB_OUTPUT - echo "PyAEDT version is: $(python -c "from pyaedt import __version__; print(__version__)")" - - # - name: Cache docs build directory - # uses: actions/cache@v3 - # with: - # path: doc/build - # key: doc-build-v${{ env.RESET_DOC_BUILD_CACHE }}-${{ steps.version.outputs.PYAEDT_VERSION }}-${{ github.sha }} - # restore-keys: | - # doc-build-v${{ env.RESET_DOC_BUILD_CACHE }}-${{ steps.version.outputs.PYAEDT_VERSION }} - # - name: Cache autosummary - # uses: actions/cache@v3 - # with: - # path: doc/source/**/_autosummary/*.rst - # key: autosummary-v${{ env.RESET_AUTOSUMMARY_CACHE }}-${{ steps.version.outputs.PYAEDT_VERSION }}-${{ github.sha }} - # restore-keys: | - # autosummary-v${{ env.RESET_AUTOSUMMARY_CACHE }}-${{ steps.version.outputs.PYAEDT_VERSION }} - - - name: Install doc build requirements - run: | - sudo apt update - sudo apt install graphviz texlive-latex-extra latexmk texlive-xetex texlive-fonts-extra -y - - # run doc build, without creating the examples directory - # note that we have to add the examples file here since it won't - # be created as gallery is disabled on linux. - - name: Documentation Build (HTML) - run: | - make -C doc clean - mkdir doc/source/examples -p - echo $'Examples\n========' > doc/source/examples/index.rst - make -C doc phtml-no-examples SPHINXOPTS="-j auto -w build_errors.txt -N" - - # Verify that sphinx generates no warnings - - name: Check for warnings - run: | - python doc/print_errors.py - - - name: Upload Documentation - uses: actions/upload-artifact@v3 - with: - name: Documentation - path: doc/_build/html - retention-days: 7 - - - name: Documentation Build (PDF) - run: | - make -C doc pdf-no-examples - - - name: Upload documentation PDF artifact - uses: actions/upload-artifact@v3 - with: - name: Documentation-pdf - path: doc/_build/latex/*.pdf - retention-days: 7 diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml new file mode 100644 index 00000000000..e6c7f71fcf3 --- /dev/null +++ b/.github/workflows/ci_cd.yml @@ -0,0 +1,573 @@ +name: GitHub CI CD +on: + pull_request: + workflow_dispatch: + push: + tags: + - "*" + branches: + - main + +env: + ANSYSLMD_LICENSE_FILE: ${{ format('1055@{0}', secrets.LICENSE_SERVER) }} + MAIN_PYTHON_VERSION: '3.10' + PACKAGE_NAME: 'PyAEDT' + DOCUMENTATION_CNAME: 'aedt.docs.pyansys.com' + MEILISEARCH_API_KEY: ${{ secrets.MEILISEARCH_API_KEY }} + MEILISEARCH_HOST_URL: ${{ vars.MEILISEARCH_HOST_URL }} + MEILISEARCH_PUBLIC_API_KEY: ${{ secrets.MEILISEARCH_PUBLIC_API_KEY }} + ON_CI: True + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + + pr-title: + if: github.event_name == 'pull_request' + name: Check the title of the pull request + runs-on: ubuntu-latest + steps: + - name: Check commit name + uses: ansys/actions/commit-style@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + use-upper-case: true + + # TODO: Update to ansys/actions/doc-style@v6 + doc-style: + name: Documentation style check + runs-on: ubuntu-latest + steps: + - name: Check documentation style + uses: ansys/actions/doc-style@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + vale-config: "doc/.vale.ini" + vale-version: "2.29.6" + + smoke-tests: + name: Build wheelhouse and smoke tests + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + steps: + - name: Build wheelhouse and perform smoke test + uses: ansys/actions/build-wheelhouse@v4 + with: + library-name: ${{ env.PACKAGE_NAME }} + operating-system: ${{ matrix.os }} + python-version: ${{ matrix.python-version }} + target: 'all' + + - name: Import python package + run: | + python -c "import pyaedt; from pyaedt import __version__" + + # TODO: Update to ansys/actions/doc-build@v6 once we remove examples + doc-build: + name: Documentation build without examples + runs-on: ubuntu-latest + needs: [doc-style] + steps: + - name: Install Git and checkout project + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.MAIN_PYTHON_VERSION }} + + - name: Update pip + run: | + pip install --upgrade pip + + - name: Install pyaedt and documentation dependencies + run: | + pip install .[doc-no-examples] + + - name: Retrieve PyAEDT version + id: version + run: | + echo "PYAEDT_VERSION=$(python -c 'from pyaedt import __version__; print(__version__)')" >> $GITHUB_OUTPUT + echo "PyAEDT version is: $(python -c "from pyaedt import __version__; print(__version__)")" + + - name: Install doc build requirements + run: | + sudo apt update + sudo apt install graphviz texlive-latex-extra latexmk texlive-xetex texlive-fonts-extra -y + + # TODO: Update this step once pyaedt-examples is ready + - name: Build HTML documentation without examples + run: | + make -C doc clean + make -C doc html-no-examples + + # Verify that sphinx generates no warnings + - name: Check for warnings + run: | + python doc/print_errors.py + + - name: Upload HTML documentation without examples artifact + uses: actions/upload-artifact@v3 + with: + name: documentation-no-examples-html + path: doc/_build/html + retention-days: 7 + + - name: Build PDF documentation without examples + run: | + make -C doc pdf-no-examples + + - name: Upload PDF documentation without examples artifact + uses: actions/upload-artifact@v3 + with: + name: documentation-no-examples-pdf + path: doc/_build/latex/PyAEDT-Documentation-*.pdf + retention-days: 7 + +# # ================================================================================================= +# # vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv RUNNING ON SELF-HOSTED RUNNER vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv +# # ================================================================================================= + + doc-build-with-examples: + name: Documentation build with examples + if: github.event_name == 'push' && contains(github.ref, 'refs/tags') + runs-on: [ self-hosted, Windows, pyaedt ] + needs: [doc-style] + timeout-minutes: 720 + steps: + - name: Install Git and checkout project + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.MAIN_PYTHON_VERSION }} + + - name: Create virtual environment + run: | + python -m venv .venv + .venv\Scripts\Activate.ps1 + python -m pip install pip -U + python -m pip install wheel setuptools -U + python -c "import sys; print(sys.executable)" + + - name: Install pyaedt and documentation dependencies + run: | + .venv\Scripts\Activate.ps1 + pip install .[doc] + + - name: Retrieve PyAEDT version + id: version + run: | + .venv\Scripts\Activate.ps1 + echo "PYAEDT_VERSION=$(python -c 'from pyaedt import __version__; print(__version__)')" >> $GITHUB_OUTPUT + echo "PyAEDT version is: $(python -c "from pyaedt import __version__; print(__version__)")" + + - name: Install CI dependencies (e.g. vtk-osmesa) + run: | + .venv\Scripts\Activate.ps1 + # Uninstall conflicting dependencies + pip uninstall --yes vtk + pip install --extra-index-url https://wheels.vtk.org vtk-osmesa==9.2.20230527.dev0 + + # TODO: Update this step once pyaedt-examples is ready + # NOTE: Use environment variable to keep the doctree and avoid redundant build for PDF pages + - name: Build HTML documentation with examples + env: + SPHINXBUILD_KEEP_DOCTREEDIR: "1" + run: | + .venv\Scripts\Activate.ps1 + .\doc\make.bat clean + .\doc\make.bat html + + # TODO: Keeping this commented as reminder of https://github.com/ansys/pyaedt/issues/4296 + # # Verify that sphinx generates no warnings + # - name: Check for warnings + # run: | + # .venv\Scripts\Activate.ps1 + # python doc/print_errors.py + + # Use environment variable to remove the doctree after the build of PDF pages + - name: Build PDF documentation with examples + env: + SPHINXBUILD_KEEP_DOCTREEDIR: "0" + run: | + .venv\Scripts\Activate.ps1 + .\doc\make.bat pdf + + - name: Add assets to HTML docs + run: | + zip -r documentation-html.zip ./doc/_build/html + mv documentation-html.zip ./doc/_build/html/_static/assets/download/ + cp doc/_build/latex/PyAEDT-Documentation-*.pdf ./doc/_build/html/_static/assets/download/pyaedt.pdf + + - name: Upload HTML documentation with examples artifact + uses: actions/upload-artifact@v3 + with: + name: documentation-html + path: doc/_build/html + retention-days: 7 + + - name: Upload PDF documentation without examples artifact + uses: actions/upload-artifact@v3 + with: + name: documentation-pdf + path: doc/_build/latex/PyAEDT-Documentation-*.pdf + retention-days: 7 + +# # ================================================================================================= +# # vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv RUNNING ON SELF-HOSTED RUNNER vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv +# # ================================================================================================= + + test-solvers-windows: + name: Testing solvers and coverage (Windows) + needs: [smoke-tests] + runs-on: [ self-hosted, Windows, pyaedt ] + steps: + - name: Install Git and checkout project + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.MAIN_PYTHON_VERSION }} + + - name: Create virtual environment + run: | + python -m venv .venv + .venv\Scripts\Activate.ps1 + python -m pip install pip -U + python -m pip install wheel setuptools -U + python -c "import sys; print(sys.executable)" + + - name: Install pyaedt and tests dependencies + run: | + .venv\Scripts\Activate.ps1 + pip install .[tests] + pip install pytest-azurepipelines + + - name: Install CI dependencies (e.g. vtk-osmesa) + run: | + .venv\Scripts\Activate.ps1 + # Uninstall conflicting dependencies + pip uninstall --yes vtk + pip install --extra-index-url https://wheels.vtk.org vtk-osmesa==9.2.20230527.dev0 + + - name: Run tests on _unittest_solvers + env: + PYTHONMALLOC: malloc + run: | + .venv\Scripts\Activate.ps1 + pytest --durations=50 -v --cov=pyaedt --cov-report=xml --cov-report=html --junitxml=junit/test-results.xml _unittest_solvers + + - uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + name: codecov-system-solver-tests + file: ./coverage.xml + flags: system,solver + + - name: Upload pytest test results + uses: actions/upload-artifact@v3 + with: + name: pytest-solver-results + path: junit/test-results.xml + if: ${{ always() }} + +# # ================================================================================================= +# # vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv RUNNING ON SELF-HOSTED RUNNER vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv +# # ================================================================================================= + + # TODO: Si if we can use ansys/actions + test-solvers-linux: + name: Testing solvers and coverage (Linux) + needs: [smoke-tests] + runs-on: [ self-hosted, Linux, pyaedt ] + env: + ANSYSEM_ROOT241: '/opt/AnsysEM/v241/Linux64' + ANS_NODEPCHECK: '1' + steps: + - name: Install Git and checkout project + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.MAIN_PYTHON_VERSION }} + + - name: Create virtual environment + run: | + export LD_LIBRARY_PATH=${{ env.ANSYSEM_ROOT241 }}/common/mono/Linux64/lib64:${{ env.ANSYSEM_ROOT241 }}/Delcross:$LD_LIBRARY_PATH + python -m venv .venv + source .venv/bin/activate + python -m pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org pip -U + python -m pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org wheel setuptools -U + python -c "import sys; print(sys.executable)" + + - name: Install pyaedt and tests dependencies + run: | + export LD_LIBRARY_PATH=${{ env.ANSYSEM_ROOT241 }}/common/mono/Linux64/lib64:${{ env.ANSYSEM_ROOT241 }}/Delcross:$LD_LIBRARY_PATH + source .venv/bin/activate + pip install .[tests] + pip install pytest-azurepipelines + + - name: Run tests on _unittest_solvers + run: | + export LD_LIBRARY_PATH=${{ env.ANSYSEM_ROOT241 }}/common/mono/Linux64/lib64:${{ env.ANSYSEM_ROOT241 }}/Delcross:$LD_LIBRARY_PATH + source .venv/bin/activate + pytest --durations=50 -v --cov=pyaedt --cov-report=xml --cov-report=html --junitxml=junit/test-results.xml _unittest_solvers + + - uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + name: codecov-system-solver-tests + file: ./coverage.xml + flags: system,solver + + - name: Upload pytest test results + uses: actions/upload-artifact@v3 + with: + name: pytest-solver-results + path: junit/test-results.xml + if: ${{ always() }} + +# # ================================================================================================= +# # vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv RUNNING ON SELF-HOSTED RUNNER vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv +# # ================================================================================================= + + test-windows: + name: Testing and coverage (Windows) + needs: [smoke-tests] + runs-on: [ self-hosted, Windows, pyaedt ] + steps: + - name: Install Git and checkout project + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.MAIN_PYTHON_VERSION }} + + - name: Create virtual environment + run: | + python -m venv .venv + .venv\Scripts\Activate.ps1 + python -m pip install pip -U + python -m pip install wheel setuptools -U + python -c "import sys; print(sys.executable)" + + - name: Install pyaedt and tests dependencies + run: | + .venv\Scripts\Activate.ps1 + pip install .[tests] + pip install pytest-azurepipelines + + - name: Install CI dependencies (e.g. vtk-osmesa) + run: | + .venv\Scripts\Activate.ps1 + # Uninstall conflicting dependencies + pip uninstall --yes vtk + pip install --extra-index-url https://wheels.vtk.org vtk-osmesa==9.2.20230527.dev0 + + - name: Run tests on _unittest + uses: nick-fields/retry@v3 + env: + PYTHONMALLOC: malloc + with: + max_attempts: 2 + retry_on: error + timeout_minutes: 50 + command: | + .venv\Scripts\Activate.ps1 + pytest -n 4 --dist loadfile --durations=50 -v --cov=pyaedt --cov-report=xml --cov-report=html --junitxml=junit/test-results.xml _unittest + + - uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + name: codecov-system-tests + file: ./coverage.xml + flags: system + + - name: Upload pytest test results + uses: actions/upload-artifact@v3 + with: + name: pytest-results + path: junit/test-results.xml + if: ${{ always() }} + +# # ================================================================================================= +# # vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv RUNNING ON SELF-HOSTED RUNNER vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv +# # ================================================================================================= + + # TODO: Si if we can use ansys/actions + test-linux: + name: Testing and coverage (Linux) + needs: [smoke-tests] + runs-on: [ self-hosted, Linux, pyaedt ] + env: + ANSYSEM_ROOT241: '/opt/AnsysEM/v241/Linux64' + ANS_NODEPCHECK: '1' + steps: + - name: Install Git and checkout project + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.MAIN_PYTHON_VERSION }} + + - name: Create virtual environment + run: | + export LD_LIBRARY_PATH=${{ env.ANSYSEM_ROOT241 }}/common/mono/Linux64/lib64:${{ env.ANSYSEM_ROOT241 }}/Delcross:$LD_LIBRARY_PATH + python -m venv .venv + source .venv/bin/activate + python -m pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org pip -U + python -m pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org wheel setuptools -U + python -c "import sys; print(sys.executable)" + + - name: Install pyaedt and tests dependencies + run: | + export LD_LIBRARY_PATH=${{ env.ANSYSEM_ROOT241 }}/common/mono/Linux64/lib64:${{ env.ANSYSEM_ROOT241 }}/Delcross:$LD_LIBRARY_PATH + source .venv/bin/activate + pip install .[tests] + pip install pytest-azurepipelines + + - name: Install CI dependencies (e.g. vtk-osmesa) + run: | + source .venv/bin/activate + # Uninstall conflicting dependencies + pip uninstall --yes vtk + pip install --extra-index-url https://wheels.vtk.org vtk-osmesa==9.2.20230527.dev0 + + - name: Run tests on _unittest + uses: nick-fields/retry@v3 + with: + max_attempts: 2 + retry_on: error + timeout_minutes: 50 + command: | + export LD_LIBRARY_PATH=${{ env.ANSYSEM_ROOT241 }}/common/mono/Linux64/lib64:${{ env.ANSYSEM_ROOT241 }}/Delcross:$LD_LIBRARY_PATH + source .venv/bin/activate + pytest -n 4 --dist loadfile --durations=50 -v --cov=pyaedt --cov-report=xml --cov-report=html --junitxml=junit/test-results.xml _unittest + + - uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + name: codecov-system-solver-tests + file: ./coverage.xml + flags: system,solver + + - name: Upload pytest test results + uses: actions/upload-artifact@v3 + with: + name: pytest-solver-results + path: junit/test-results.xml + if: ${{ always() }} + +# # ================================================================================================= +# # vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv RUNNING ON SELF-HOSTED RUNNER vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv +# # ================================================================================================= + + test-ironpython-windows: + name: Testing IronPython and coverage (Windows) + needs: [smoke-tests] + runs-on: [ self-hosted, Windows, pyaedt ] + steps: + - uses: actions/checkout@v4 + + - name: Run Ironpython tests + timeout-minutes: 5 + run: | + $processA = start-process 'cmd' -ArgumentList '/c .\_unittest_ironpython\run_unittests_batchmode.cmd' -PassThru + $processA.WaitForExit() + + - name: Get log content + run: | + get-content .\_unittest_ironpython\pyaedt_unit_test_ironpython.log + + - name: Check for errors + run: | + $test_errors_failures = Select-String -Path .\_unittest_ironpython\pyaedt_unit_test_ironpython.log -Pattern "TextTestResult errors=" + if ($test_errors_failures -ne $null) + { + exit 1 + } + + package: + name: Package library + needs: [test-windows, test-solvers-windows, test-ironpython-windows, test-linux, test-solvers-linux, doc-build] + runs-on: ubuntu-latest + steps: + - name: Build library source and wheel artifacts + uses: ansys/actions/build-library@v4 + with: + library-name: ${{ env.PACKAGE_NAME }} + python-version: ${{ env.MAIN_PYTHON_VERSION }} + + # TODO: Si if we can fix the PDF issue and leverage classic ansys/release-github + release: + name: Release project + if: github.event_name == 'push' && contains(github.ref, 'refs/tags') + needs: [package, doc-build-with-examples] + runs-on: ubuntu-latest + steps: + - name: Release to the public PyPI repository + uses: ansys/actions/release-pypi-public@v4 + with: + library-name: ${{ env.PACKAGE_NAME }} + twine-username: "__token__" + twine-token: ${{ secrets.PYPI_TOKEN }} + + - name: Release to GitHub + uses: ansys/actions/release-github@v4 + with: + library-name: ${{ env.PACKAGE_NAME }} + + upload-release-doc: + name: Upload release documentation + if: github.event_name == 'push' && contains(github.ref, 'refs/tags') + runs-on: ubuntu-latest + needs: [release] + steps: + - name: Deploy the stable documentation + uses: ansys/actions/doc-deploy-stable@v4 + with: + cname: ${{ env.DOCUMENTATION_CNAME }} + token: ${{ secrets.GITHUB_TOKEN }} + doc-artifact-name: 'documentation-html' + + doc-index-stable: + name: Deploy stable docs index + if: github.event_name == 'push' && contains(github.ref, 'refs/tags') + runs-on: ubuntu-latest + needs: upload-release-doc + steps: + - name: Install Git and clone project + uses: actions/checkout@v4 + + - name: Install the package requirements + run: pip install -e . + + - name: Get the version to PyMeilisearch + run: | + VERSION=$(python -c "from pyaedt import __version__; print('.'.join(__version__.split('.')[:2]))") + VERSION_MEILI=$(python -c "from pyaedt import __version__; print('-'.join(__version__.split('.')[:2]))") + echo "Calculated VERSION: $VERSION" + echo "Calculated VERSION_MEILI: $VERSION_MEILI" + echo "VERSION=$VERSION" >> $GITHUB_ENV + echo "VERSION_MEILI=$VERSION_MEILI" >> $GITHUB_ENV + + - name: Deploy the latest documentation index + uses: ansys/actions/doc-deploy-index@v4 + with: + cname: ${{ env.DOCUMENTATION_CNAME }}/version/${{ env.VERSION }} + index-name: pyaedt-v${{ env.VERSION_MEILI }} + host-url: ${{ env.MEILISEARCH_HOST_URL }} + api-key: ${{ env.MEILISEARCH_API_KEY }} + python-version: ${{ env.MAIN_PYTHON_VERSION }} diff --git a/.github/workflows/cpython_linux.yml b/.github/workflows/cpython_linux.yml deleted file mode 100644 index 5d9313e5765..00000000000 --- a/.github/workflows/cpython_linux.yml +++ /dev/null @@ -1,92 +0,0 @@ -name: Linux_CPython_UnitTests - -env: - ANSYSLMD_LICENSE_FILE: ${{ format('1055@{0}', secrets.LICENSE_SERVER) }} - python.version: '3.10' - python.venv: 'testvenv' - # Following env vars when changed will "reset" the mentioned cache, - # by changing the cache file name. It is rendered as ...-v%RESET_XXX%-... - # You should go up in number, if you go down (or repeat a previous value) - # you might end up reusing a previous cache if it haven't been deleted already. - # It applies 7 days retention policy by default. - RESET_PIP_CACHE: 0 - PACKAGE_NAME: PyAEDT - - -on: - workflow_dispatch: - inputs: - logLevel: - description: 'Log level' - required: true - default: 'warning' - tags: - description: 'Linux CPython daily' - schedule: # UTC at 0100 - - cron: '0 1 * * *' - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - test: - runs-on: [Linux, pyaedt] - strategy: - matrix: - python-version: [ '3.10' ] - steps: - - uses: actions/checkout@v3 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - architecture: 'x86' - - - name: 'Install pyaedt' - run: | - python -m venv .pyaedt_test_env - export ANSYSEM_ROOT241=/apps/AnsysEM/v241/Linux64 - export LD_LIBRARY_PATH=$ANSYSEM_ROOT241/common/mono/Linux64/lib64:$LD_LIBRARY_PATH - source .pyaedt_test_env/bin/activate - python -m pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org pip -U - python -m pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org wheel setuptools -U - python -c "import sys; print(sys.executable)" - pip install .[tests] - pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org pytest-azurepipelines - python -c "import pyaedt; print('Imported pyaedt')" - - - name: 'Unit testing' - uses: nick-fields/retry@v3 - with: - max_attempts: 3 - retry_on: error - timeout_minutes: 60 - command: | - export ANS_NODEPCHECK=1 - export ANSYSEM_ROOT241=/apps/AnsysEM/v241/Linux64 - export LD_LIBRARY_PATH=$ANSYSEM_ROOT241/common/mono/Linux64/lib64:$LD_LIBRARY_PATH - source .pyaedt_test_env/bin/activate - pytest --tx 6*popen --durations=50 --dist loadfile -v _unittest - - - name: 'Unit testing Solvers' - continue-on-error: true - uses: nick-fields/retry@v3 - with: - max_attempts: 3 - retry_on: error - timeout_minutes: 60 - command: | - export ANS_NODEPCHECK=1 - export ANSYSEM_ROOT241=/apps/AnsysEM/v241/Linux64 - export LD_LIBRARY_PATH=$ANSYSEM_ROOT241/common/mono/Linux64/lib64:$LD_LIBRARY_PATH - source .pyaedt_test_env/bin/activate - pytest --tx 2*popen --durations=50 --dist loadfile -v _unittest_solvers - - - name: Upload pytest test results - uses: actions/upload-artifact@v4 - with: - name: pytest-results - path: junit/test-results.xml - if: ${{ always() }} diff --git a/.github/workflows/full_documentation.yml b/.github/workflows/full_documentation.yml deleted file mode 100644 index 0a114fa3635..00000000000 --- a/.github/workflows/full_documentation.yml +++ /dev/null @@ -1,155 +0,0 @@ -# This is a basic workflow to help you get started with Actions - -name: FullDocumentation - -env: - ANSYSLMD_LICENSE_FILE: ${{ format('1055@{0}', secrets.LICENSE_SERVER) }} - python.version: '3.10' - python.venv: 'testvenv' - DOCUMENTATION_CNAME: 'aedt.docs.pyansys.com' - MEILISEARCH_API_KEY: ${{ secrets.MEILISEARCH_API_KEY }} - MEILISEARCH_HOST_URL: https://backend.search.pyansys.com - MEILISEARCH_PUBLIC_API_KEY: ${{ secrets.MEILISEARCH_PUBLIC_API_KEY }} -# Controls when the workflow will run -on: - # Triggers the workflow on push or pull request events but only for the main branch - push: - tags: - - v* - workflow_dispatch: - inputs: - logLevel: - description: 'Log level' - required: true - default: 'warning' - tags: - description: 'Test scenario tags' - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - full_documentation: - # The type of runner that the job will run on - name: full_documentation - runs-on: [Windows, self-hosted, pyaedt] - timeout-minutes: 720 - strategy: - matrix: - python-version: ['3.10'] - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - - uses: actions/checkout@v4 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: 'Create virtual env' - run: | - python -m venv testenv - testenv\Scripts\Activate.ps1 - python -m pip install pip -U - python -m pip install wheel setuptools -U - python -c "import sys; print(sys.executable)" - - - name: 'Install pyaedt' - run: | - testenv\Scripts\Activate.ps1 - pip install .[doc] - Copy-Item -Path "C:\actions-runner\opengl32.dll" -Destination "testenv\Lib\site-packages\vtkmodules" -Force - - - name: Retrieve PyAEDT version - id: version - run: | - testenv\Scripts\Activate.ps1 - echo "PYAEDT_VERSION=$(python -c 'from pyaedt import __version__; print(__version__)')" >> $GITHUB_OUTPUT - echo "PyAEDT version is: $(python -c "from pyaedt import __version__; print(__version__)")" - - - name: Create HTML Documentations - run: | - testenv\Scripts\Activate.ps1 - sphinx-build -j auto --color -b html -a doc/source doc/_build/html - -# - name: Create PDF Documentations -# run: | -# testenv\Scripts\Activate.ps1 -# .\doc\make.bat pdf - - - name: Upload HTML documentation artifact - uses: actions/upload-artifact@v3 - with: - name: documentation-html - path: doc/_build/html - retention-days: 7 - -# - name: Upload PDF documentation artifact -# uses: actions/upload-artifact@v4 -# with: -# name: documentation-pdf -# path: doc/_build/pdf -# retention-days: 7 - -# - name: Release -# uses: softprops/action-gh-release@v1 -# if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') -# with: -# generate_release_notes: true -# files: | -# doc/_build/pdf - - doc-deploy-stable: - name: Deploy stable documentation - runs-on: ubuntu-latest - needs: full_documentation - if: github.event_name == 'push' && contains(github.ref, 'refs/tags') - steps: - - name: Deploy the stable documentation - uses: ansys/actions/doc-deploy-stable@v4 - with: - cname: ${{ env.DOCUMENTATION_CNAME }} - token: ${{ secrets.GITHUB_TOKEN }} - python-version: ${{ matrix.python-version }} - - - doc-index-stable: - name: "Deploy stable docs index" - if: github.event_name == 'push' && contains(github.ref, 'refs/tags') - runs-on: ubuntu-latest - needs: doc-deploy-stable - - steps: - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: ${{ env.MAIN_PYTHON_VERSION }} - - - uses: actions/checkout@v4 - - - uses: actions/download-artifact@v3 - - - name: Display structure of downloaded files - run: ls -R - - - name: Install the package requirements - run: pip install -e . - - - name: Get the version to PyMeilisearch - run: | - VERSION=$(python -c "from pyaedt import __version__; print('.'.join(__version__.split('.')[:2]))") - VERSION_MEILI=$(python -c "from pyaedt import __version__; print('-'.join(__version__.split('.')[:2]))") - echo "Calculated VERSION: $VERSION" - echo "Calculated VERSION_MEILI: $VERSION_MEILI" - echo "VERSION=$VERSION" >> $GITHUB_ENV - echo "VERSION_MEILI=$VERSION_MEILI" >> $GITHUB_ENV - - - name: "Deploy the stable documentation index for PyAEDT API" - uses: ansys/actions/doc-deploy-index@v4 - with: - cname: ${{ env.DOCUMENTATION_CNAME }}/version/${{ env.VERSION }} - index-name: pyaedt-v${{ env.VERSION_MEILI }} - host-url: ${{ vars.MEILISEARCH_HOST_URL }} - api-key: ${{ env.MEILISEARCH_API_KEY }} diff --git a/.github/workflows/ironpython.yml b/.github/workflows/ironpython.yml deleted file mode 100644 index 0fb334beaaa..00000000000 --- a/.github/workflows/ironpython.yml +++ /dev/null @@ -1,42 +0,0 @@ -# This is a basic workflow to help you get started with Actions - -name: CI_Ironpython - -env: - ANSYSLMD_LICENSE_FILE: ${{ format('1055@{0}', secrets.LICENSE_SERVER) }} - -# Controls when the workflow will run -on: - # Triggers the workflow on push or pull request events but only for the main branch - pull_request: - branches: [ main ] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - # This workflow contains a single job called "build" - build: - # The type of runner that the job will run on - runs-on: [Windows, self-hosted, pyaedt] - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - - uses: actions/checkout@v4 - - - name: 'Run Unit Tests in Ironpython' - timeout-minutes: 60 - run: | - $processA = start-process 'cmd' -ArgumentList '/c .\_unittest_ironpython\run_unittests_batchmode.cmd' -PassThru - $processA.WaitForExit() - get-content .\_unittest_ironpython\pyaedt_unit_test_ironpython.log - $test_errors_failures = Select-String -Path .\_unittest_ironpython\pyaedt_unit_test_ironpython.log -Pattern "TextTestResult errors=" - if ($test_errors_failures -ne $null) - { - exit 1 - } - else - { - exit 0 - } diff --git a/.github/workflows/nightly-docs.yml b/.github/workflows/nightly-docs.yml index a3c8aec00c0..9c9ccb30c6b 100644 --- a/.github/workflows/nightly-docs.yml +++ b/.github/workflows/nightly-docs.yml @@ -7,6 +7,7 @@ on: env: ANSYSLMD_LICENSE_FILE: ${{ format('1055@{0}', secrets.LICENSE_SERVER) }} + MAIN_PYTHON_VERSION: '3.10' DOCUMENTATION_CNAME: 'aedt.docs.pyansys.com' MEILISEARCH_API_KEY: ${{ secrets.MEILISEARCH_API_KEY }} MEILISEARCH_PUBLIC_API_KEY: ${{ secrets.MEILISEARCH_PUBLIC_API_KEY }} @@ -16,43 +17,50 @@ concurrency: cancel-in-progress: true jobs: - docs_build: + doc-build: + name: Documentation build without examples runs-on: ubuntu-latest - + needs: [doc-style] steps: - - uses: actions/checkout@v4 + - name: Install Git and checkout project + uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: ${{ env.MAIN_PYTHON_VERSION }} - - name: Install pyaedt + - name: Update pip run: | - pip install . + pip install --upgrade pip - - name: Install doc build requirements + - name: Install pyaedt and documentation dependencies + run: | + pip install .[doc-no-examples] + + - name: Retrieve PyAEDT version + id: version run: | - pip install .[doc] + echo "PYAEDT_VERSION=$(python -c 'from pyaedt import __version__; print(__version__)')" >> $GITHUB_OUTPUT + echo "PyAEDT version is: $(python -c "from pyaedt import __version__; print(__version__)")" - name: Install doc build requirements run: | sudo apt update sudo apt install graphviz texlive-latex-extra latexmk texlive-xetex texlive-fonts-extra -y - - name: Documentation Build (HTML) + # TODO: Update this step once pyaedt-examples is ready + - name: Build HTML documentation without examples run: | make -C doc clean - mkdir doc/source/examples -p - echo $'Examples\n========' > doc/source/examples/index.rst - make -C doc phtml-no-examples SPHINXOPTS="-j auto -w build_errors.txt -N" + make -C doc html-no-examples # Verify that sphinx generates no warnings - name: Check for warnings run: | python doc/print_errors.py - - name: Documentation Build (PDF) + - name: Build PDF documentation without examples run: | make -C doc pdf-no-examples @@ -62,84 +70,42 @@ jobs: mv documentation-html.zip ./doc/_build/html/_static/assets/download/ cp doc/_build/latex/PyAEDT-Documentation-*.pdf ./doc/_build/html/_static/assets/download/pyaedt.pdf - - name: Upload documentation HTML artifact + - name: Upload HTML documentation without examples artifact uses: actions/upload-artifact@v3 with: - name: documentation-html + name: documentation-no-examples-html path: doc/_build/html retention-days: 7 - - name: Upload documentation PDF artifact + - name: Upload PDF documentation without examples artifact uses: actions/upload-artifact@v3 with: - name: Documentation-pdf - path: doc/_build/latex/*.pdf + name: documentation-pdf + path: doc/_build/latex/PyAEDT-Documentation-*.pdf retention-days: 7 - docs_upload: - needs: docs_build + upload-dev-doc: + name: Upload dev documentation runs-on: ubuntu-latest + needs: [doc-build] steps: - name: Upload development documentation uses: ansys/actions/doc-deploy-dev@v4 with: cname: ${{ env.DOCUMENTATION_CNAME }} token: ${{ secrets.GITHUB_TOKEN }} + doc-artifact-name: 'documentation-no-examples-html' doc-index-dev: - name: "Deploy dev docs index" + name: Deploy dev index docs runs-on: ubuntu-latest - needs: docs_upload + needs: upload-dev-doc steps: - - uses: actions/checkout@v4 - - - uses: actions/download-artifact@v3 - - - name: Display structure of downloaded files - run: ls -R - - - name: "Deploy the dev documentation index for PyAEDT API" + - name: Deploy the latest documentation index uses: ansys/actions/doc-deploy-index@v4 with: cname: ${{ env.DOCUMENTATION_CNAME }}/version/dev index-name: pyaedt-vdev - host-url: ${{ vars.MEILISEARCH_HOST_URL }} + host-url: ${{ env.MEILISEARCH_HOST_URL }} api-key: ${{ env.MEILISEARCH_API_KEY }} - - # docstring_testing: - # runs-on: Windows - - # steps: - # - uses: actions/checkout@v4 - - # - name: Setup Python - # uses: actions/setup-python@v2 - # with: - # python-version: 3.8 - - # - name: 'Create virtual env' - # run: | - # python -m venv testenv - # testenv\Scripts\Activate.ps1 - # python -m pip install pip -U - # python -m pip install wheel setuptools -U - # python -c "import sys; print(sys.executable)" - - # - name: 'Install pyaedt' - # run: | - # testenv\Scripts\Activate.ps1 - # pip install . --use-feature=in-tree-build - # cd _unittest - # python -c "import pyaedt; print('Imported pyaedt')" - - # - name: Install testing requirements - # run: | - # testenv\Scripts\Activate.ps1 - # pip install -r requirements/requirements_test.txt - # pip install pytest-azurepipelines - - # - name: Docstring testing - # run: | - # testenv\Scripts\Activate.ps1 - # pytest -v pyaedt/desktop.py pyaedt/icepak.py - # pytest -v pyaedt/desktop.py pyaedt/hfss.py + python-version: ${{ env.MAIN_PYTHON_VERSION }} diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml deleted file mode 100644 index 473ec840b8c..00000000000 --- a/.github/workflows/unit_tests.yml +++ /dev/null @@ -1,173 +0,0 @@ -name: CI - -env: - ANSYSLMD_LICENSE_FILE: ${{ format('1055@{0}', secrets.LICENSE_SERVER) }} - python.version: '3.10' - python.venv: 'testvenv' - # Following env vars when changed will "reset" the mentioned cache, - # by changing the cache file name. It is rendered as ...-v%RESET_XXX%-... - # You should go up in number, if you go down (or repeat a previous value) - # you might end up reusing a previous cache if it hasn't been deleted already. - # It applies 7 days retention policy by default. - RESET_PIP_CACHE: 0 - PACKAGE_NAME: PyAEDT -# Controls when the workflow will run -on: - # Triggers the workflow on push or pull request events but only for the main branch - push: - tags: - - 'v*' - branches: - - main - pull_request: - branches: [ main ] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - # This workflow contains a single job called "build" - build_solvers: - # The type of runner that the job will run on - runs-on: [Windows, self-hosted, pyaedt] - strategy: - matrix: - python-version: [ '3.10' ] - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: 'Create virtual env' - run: | - Remove-Item D:\Temp\* -Recurse -Force -ErrorAction SilentlyContinue - python -m venv testenv_s - testenv_s\Scripts\Activate.ps1 - python -m pip install pip -U - python -m pip install wheel setuptools -U - python -c "import sys; print(sys.executable)" - - - name: 'Install pyaedt' - run: | - testenv_s\Scripts\Activate.ps1 - pip install . - pip install .[tests] - pip install pytest-azurepipelines - Copy-Item -Path "C:\actions-runner\opengl32.dll" -Destination "testenv_s\Lib\site-packages\vtkmodules" -Force - mkdir tmp - cd tmp - python -c "import pyaedt; print('Imported pyaedt')" - - # - name: "Check licences of packages" - # uses: pyansys/pydpf-actions/check-licenses@v2.0 - - - name: 'Unit testing' - uses: nick-fields/retry@v3 - with: - max_attempts: 1 - retry_on: error - timeout_minutes: 40 - command: | - testenv_s\Scripts\Activate.ps1 - Set-Item -Path env:PYTHONMALLOC -Value "malloc" - pytest --durations=50 -v --cov=pyaedt --cov-report=xml --cov-report=html --junitxml=junit/test-results.xml _unittest_solvers - - - uses: codecov/codecov-action@v4 - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - with: - name: 'Upload coverage to Codecov' - - - name: Upload pytest test results - uses: actions/upload-artifact@v4 - with: - name: pytest-solver-results - path: junit/test-results.xml - # Use always() to always run this step to publish test results when there are test failures - if: ${{ always() }} - - - build: - # The type of runner that the job will run on - runs-on: [Windows, self-hosted, pyaedt] - strategy: - matrix: - python-version: ['3.10'] - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: 'Create virtual env' - run: | - Remove-Item D:\Temp\* -Recurse -Force -ErrorAction SilentlyContinue - python -m venv testenv - testenv\Scripts\Activate.ps1 - python -m pip install pip -U - python -m pip install wheel setuptools -U - python -c "import sys; print(sys.executable)" - - - name: 'Install pyaedt' - run: | - testenv\Scripts\Activate.ps1 - pip install . - pip install .[tests] - pip install pytest-azurepipelines - Copy-Item -Path "C:\actions-runner\opengl32.dll" -Destination "testenv\Lib\site-packages\vtkmodules" -Force - mkdir tmp - cd tmp - python -c "import pyaedt; print('Imported pyaedt')" - - # - name: "Check licences of packages" - # uses: pyansys/pydpf-actions/check-licenses@v2.0 - - - name: 'Unit testing' - uses: nick-fields/retry@v3 - with: - max_attempts: 2 - retry_on: error - timeout_minutes: 50 - command: | - testenv\Scripts\Activate.ps1 - Set-Item -Path env:PYTHONMALLOC -Value "malloc" - pytest -n 4 --dist loadfile --durations=50 -v --cov=pyaedt --cov-report=xml --cov-report=html --junitxml=junit/test-results.xml _unittest - - - uses: codecov/codecov-action@v4 - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - with: - name: 'Upload coverage to Codecov' - - - name: Upload pytest test results - uses: actions/upload-artifact@v4 - with: - name: pytest-results - path: junit/test-results.xml - # Use always() to always run this step to publish test results when there are test failures - if: ${{ always() }} - - - name: 'Build and validate source distribution' - run: | - testenv\Scripts\Activate.ps1 - python -m pip install build twine - python -m build - python -m twine check dist/* - - - name: "Builds and uploads to PyPI" - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - run: | - testenv\Scripts\Activate.ps1 - python setup.py sdist - python -m pip install twine - python -m twine upload --skip-existing dist/* - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} diff --git a/.github/workflows/unit_tests_solvers.bkp b/.github/workflows/unit_tests_solvers.bkp deleted file mode 100644 index 4d0691a5dab..00000000000 --- a/.github/workflows/unit_tests_solvers.bkp +++ /dev/null @@ -1,103 +0,0 @@ -name: CI_Solvers - -env: - python.version: '3.10' - python.venv: 'testvenv' - # Following env vars when changed will "reset" the mentioned cache, - # by changing the cache file name. It is rendered as ...-v%RESET_XXX%-... - # You should go up in number, if you go down (or repeat a previous value) - # you might end up reusing a previous cache if it hasn't been deleted already. - # It applies 7 days retention policy by default. - RESET_PIP_CACHE: 0 - PACKAGE_NAME: PyAEDT -# Controls when the workflow will run -on: - # Triggers the workflow on push or pull request events but only for the main branch - push: - tags: - - 'v*' - branches: - - main - pull_request: - branches: [ main ] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - # This workflow contains a single job called "build" - build: - # The type of runner that the job will run on - runs-on: [Windows, self-hosted, pyaedt] - strategy: - matrix: - python-version: ['3.10'] - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - - name: 'Create virtual env' - run: | - Remove-Item D:\Temp\* -Recurse -Force - python -m venv testenv_s - testenv_s\Scripts\Activate.ps1 - python -m pip install pip -U - python -m pip install wheel setuptools -U - python -c "import sys; print(sys.executable)" - - - name: 'Install pyaedt' - run: | - testenv_s\Scripts\Activate.ps1 - pip install . - pip install .[tests] - pip install pytest-azurepipelines - Copy-Item -Path "C:\actions-runner\opengl32.dll" -Destination "testenv_s\Lib\site-packages\vtkmodules" -Force - mkdir tmp - cd tmp - python -c "import pyaedt; print('Imported pyaedt')" - - # - name: "Check licences of packages" - # uses: pyansys/pydpf-actions/check-licenses@v2.0 - - - name: 'Unit testing' - timeout-minutes: 40 - run: | - testenv_s\Scripts\Activate.ps1 - Set-Item -Path env:PYTHONMALLOC -Value "malloc" - pytest --durations=50 -v --cov=pyaedt --cov-report=xml --cov-report=html --junitxml=junit/test-results.xml _unittest_solvers - - - uses: codecov/codecov-action@v3 - if: matrix.python-version == '3.10' - name: 'Upload coverage to Codecov' - - - name: Upload pytest test results - uses: actions/upload-artifact@v3 - with: - name: pytest-results - path: junit/test-results.xml - # Use always() to always run this step to publish test results when there are test failures - if: ${{ always() }} - - - name: 'Build and validate source distribution' - run: | - testenv_s\Scripts\Activate.ps1 - python -m pip install build twine - python -m build - python -m twine check dist/* - - - name: "Builds and uploads to PyPI" - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - run: | - testenv_s\Scripts\Activate.ps1 - python setup.py sdist - python -m pip install twine - python -m twine upload --skip-existing dist/* - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} diff --git a/.github/workflows/wheelhouse.yml b/.github/workflows/wheelhouse.yml deleted file mode 100644 index 600b1c73e9c..00000000000 --- a/.github/workflows/wheelhouse.yml +++ /dev/null @@ -1,90 +0,0 @@ -# This is a basic workflow to help you get started with Actions - -name: WheelHouse - -env: - python.venv: 'testvenv' - # Following env vars when changed will "reset" the mentioned cache, - # by changing the cache file name. It is rendered as ...-v%RESET_XXX%-... - # You should go up in number, if you go down (or repeat a previous value) - # you might end up reusing a previous cache if it haven't been deleted already. - # It applies 7 days retention policy by default. - RESET_PIP_CACHE: 0 - PACKAGE_NAME: PyAEDT -# Controls when the workflow will run -on: - # Triggers the workflow on push or pull request events but only for the main branch - push: - tags: - - 'v*' - - v* - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - # This workflow contains a single job called "build" - build: - # The type of runner that the job will run on - runs-on: [windows-latest] - strategy: - matrix: - python-version: [ 3.7, 3.8, 3.9, '3.10'] - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: 'Create virtual env' - run: | - python -m venv testenv - testenv\Scripts\Activate.ps1 - python -m pip install pip -U - python -m pip install wheel setuptools -U - python -c "import sys; print(sys.executable)" - pip install .[all,dotnet] - pip install jupyterlab - - - - name: Retrieve PyAEDT version - run: | - testenv\Scripts\Activate.ps1 - echo "PYAEDT_VERSION=$(python -c 'from pyaedt import __version__; print(__version__)')" >> $GITHUB_OUTPUT - echo "PyAEDT version is: $(python -c "from pyaedt import __version__; print(__version__)")" - id: version - - - name: Generate wheelhouse - run: | - testenv\Scripts\Activate.ps1 - $packages=$(pip freeze) - # Iterate over the packages and generate wheels - foreach ($package in $packages) { - echo "Generating wheel for $package" - pip wheel "$package" -w wheelhouse - } - - - name: Zip wheelhouse - uses: vimtor/action-zip@v1 - with: - files: wheelhouse - dest: ${{ env.PACKAGE_NAME }}-v${{ steps.version.outputs.PYAEDT_VERSION }}-${{ runner.os }}-${{ matrix.python-version }}.zip - - - name: Upload Wheelhouse - uses: actions/upload-artifact@v4 - with: - name: ${{ env.PACKAGE_NAME }}-v${{ steps.version.outputs.PYAEDT_VERSION }}-${{ runner.os }}-${{ matrix.python-version }} - path: '*.zip' - retention-days: 7 - - - name: Release - uses: softprops/action-gh-release@v2 - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - with: - generate_release_notes: true - files: | - ${{ env.PACKAGE_NAME }}-v${{ steps.version.outputs.PYAEDT_VERSION }}-${{ runner.os }}-${{ matrix.python-version }}.zip diff --git a/.github/workflows/wheelhouse_linux.yml b/.github/workflows/wheelhouse_linux.yml deleted file mode 100644 index 67458e53f95..00000000000 --- a/.github/workflows/wheelhouse_linux.yml +++ /dev/null @@ -1,89 +0,0 @@ -# This is a basic workflow to help you get started with Actions - -name: WheelHouse Linux - -env: - python.venv: 'testvenv' - # Following env vars when changed will "reset" the mentioned cache - # by changing the cache file name. It is rendered as ...-v%RESET_XXX%-... - # You should go up in number. If you go down (or repeat a previous value), - # you might end up reusing a previous cache if it hasn't been deleted already. - # It applies a 7-day retention policy by default. - RESET_PIP_CACHE: 0 - PACKAGE_NAME: PyAEDT -# Controls when the workflow will run -on: - # Triggers the workflow on push or pull request events but only for the main branch - push: - tags: - - 'v*' - - v* - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - # This workflow contains a single job called "build" - build: - # The type of runner that the job will run on - runs-on: ubuntu-20.04 - strategy: - matrix: - python-version: [ 3.7, 3.8, 3.9, '3.10'] - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: Install pyaedt - run: | - pip install .[all,dotnet] - pip install jupyterlab - - - name: Verify pyaedt can be imported - run: python -c "import pyaedt" - - - name: Retrieve PyAEDT version - run: | - echo "PYAEDT_VERSION=$(python -c 'from pyaedt import __version__; print(__version__)')" >> $GITHUB_OUTPUT - echo "PyAEDT version is: $(python -c "from pyaedt import __version__; print(__version__)")" - id: version - - - name: Generate wheelhouse - run: | - pip install wheel setuptools -U - pip install --upgrade pip - pip wheel . -w wheelhouse - export wheellist=$(pip freeze) - for file in $wheellist; do - if [[ $file != *"@"* ]] && [[ $file != *"pyaedt"* ]]; then - pip wheel $file -w wheelhouse - fi - done - continue-on-error: true - - - name: Zip wheelhouse - uses: vimtor/action-zip@v1 - with: - files: wheelhouse - dest: ${{ env.PACKAGE_NAME }}-v${{ steps.version.outputs.PYAEDT_VERSION }}-wheelhouse-${{ runner.os }}-${{ matrix.python-version }}.zip - - - name: Upload Wheelhouse - uses: actions/upload-artifact@v4 - with: - name: ${{ env.PACKAGE_NAME }}-v${{ steps.version.outputs.PYAEDT_VERSION }}-wheelhouse-${{ runner.os }}-${{ matrix.python-version }} - path: '*.zip' - retention-days: 7 - - - name: Release - uses: softprops/action-gh-release@v2 - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - with: - generate_release_notes: true - files: | - ${{ env.PACKAGE_NAME }}-v${{ steps.version.outputs.PYAEDT_VERSION }}-wheelhouse-${{ runner.os }}-${{ matrix.python-version }}.zip \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 74280703f34..9e0d207e297 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,7 +39,8 @@ Here's a simple overview of how you can start making contributions: **Push Your Changes:** After committing your changes, "push" them to your forked repository on GitHub with `git push origin `. -**Create a Pull Request:** A Pull Request (PR) lets us know you have changes you think should be included in the main project. Go to your forked repository on GitHub and click on the 'Pull request' button. +**Create a Pull Request:** A Pull Request (PR) lets us know you have changes you think should be included in the main project. Go to your forked repository on GitHub and click on the 'Pull request' button. The title of your +'Pull request' must follow the `conventional commits standard `_ where the type field is expected to be defined with upper cases. Following these steps ensures that your contributions will be easily reviewed and potentially included in the project much faster. diff --git a/_unittest/test_01_Design.py b/_unittest/test_01_Design.py index c1d1a1c7acd..ae2150c5391 100644 --- a/_unittest/test_01_Design.py +++ b/_unittest/test_01_Design.py @@ -15,6 +15,7 @@ from pyaedt.application.design_solutions import model_names from pyaedt.generic.general_methods import is_linux from pyaedt.generic.general_methods import settings +from pyaedt.workflows import customize_automation_tab test_subfolder = "T01" if config["desktopVersion"] > "2022.2": @@ -398,17 +399,18 @@ def test_36_test_load(self, add_app): assert True def test_37_add_custom_toolkit(self, desktop): - assert desktop.get_available_toolkits() + assert customize_automation_tab.available_toolkits def test_38_toolkit(self, desktop): file = os.path.join(self.local_scratch.path, "test.py") with open(file, "w") as f: f.write("import pyaedt\n") - assert desktop.add_script_to_menu( - "test_toolkit", - file, + assert customize_automation_tab.add_script_to_menu( + desktop_object=self.aedtapp.desktop_class, name="test_toolkit", script_file=file + ) + assert customize_automation_tab.remove_script_from_menu( + desktop_object=self.aedtapp.desktop_class, name="test_toolkit" ) - assert desktop.remove_script_from_menu("test_toolkit") def test_39_load_project(self, desktop): new_project = os.path.join(self.local_scratch.path, "new.aedt") diff --git a/_unittest/test_01_toolkit_icons.py b/_unittest/test_01_toolkit_icons.py index 1cad078d49e..55ec3a74cbd 100644 --- a/_unittest/test_01_toolkit_icons.py +++ b/_unittest/test_01_toolkit_icons.py @@ -1,9 +1,14 @@ import os -import xml.etree.ElementTree as ET + +import defusedxml.ElementTree as ET +import defusedxml.minidom + +defusedxml.defuse_stdlib() + import pytest -from pyaedt.misc.aedtlib_personalib_install import write_tab_config +from pyaedt.workflows.customize_automation_tab import add_automation_tab @pytest.fixture(scope="module", autouse=True) @@ -17,8 +22,7 @@ def init(self, local_scratch): self.local_scratch = local_scratch def test_00_write_new_xml(self): - file_path = os.path.join(self.local_scratch.path, "TabConfig.xml") - write_tab_config(os.path.dirname(file_path), self.local_scratch.path) + file_path = add_automation_tab(name="Test", lib_dir=self.local_scratch.path) root = self.validate_file_exists_and_pyaedt_tabs_added(file_path) panels = root.findall("./panel") panel_names = [panel.attrib["label"] for panel in panels] @@ -29,7 +33,7 @@ def test_01_add_pyaedt_config_to_existing_existing_xml(self): First write a dummy XML with a different Panel and then add PyAEDT's tabs :return: """ - file_path = os.path.join(self.local_scratch.path, "TabConfig.xml") + file_path = os.path.join(self.local_scratch.path, "Project", "TabConfig.xml") with open(file_path, "w") as fid: fid.write( """ @@ -47,7 +51,7 @@ def test_01_add_pyaedt_config_to_existing_existing_xml(self): """ ) - write_tab_config(os.path.dirname(file_path), self.local_scratch.path) + file_path = add_automation_tab(name="Test", lib_dir=self.local_scratch.path) root = self.validate_file_exists_and_pyaedt_tabs_added(file_path) panels = root.findall("./panel") panel_names = [panel.attrib["label"] for panel in panels] @@ -55,7 +59,7 @@ def test_01_add_pyaedt_config_to_existing_existing_xml(self): assert "Panel_1" in panel_names def test_03_overwrite_existing_pyaedt_config(self): - file_path = os.path.join(self.local_scratch.path, "TabConfig.xml") + file_path = os.path.join(self.local_scratch.path, "Project", "TabConfig.xml") with open(file_path, "w") as fid: fid.write( """ @@ -72,14 +76,14 @@ def test_03_overwrite_existing_pyaedt_config(self): """ ) - write_tab_config(os.path.dirname(file_path), self.local_scratch.path) + file_path = add_automation_tab(name="Test", lib_dir=self.local_scratch.path) root = self.validate_file_exists_and_pyaedt_tabs_added(file_path) panels = root.findall("./panel") panel_names = [panel.attrib["label"] for panel in panels] - assert len(panel_names) == 1 + assert len(panel_names) == 2 def test_04_write_to_existing_file_but_no_panels(self): - file_path = os.path.join(self.local_scratch.path, "TabConfig.xml") + file_path = os.path.join(self.local_scratch.path, "Project", "TabConfig.xml") with open(file_path, "w") as fid: fid.write( """ @@ -88,7 +92,7 @@ def test_04_write_to_existing_file_but_no_panels(self): """ ) - write_tab_config(os.path.dirname(file_path), self.local_scratch.path) + file_path = add_automation_tab(name="Test", lib_dir=self.local_scratch.path) root = self.validate_file_exists_and_pyaedt_tabs_added(file_path) junks = root.findall("./junk") junk_names = [junk.attrib["label"] for junk in junks] @@ -98,15 +102,13 @@ def test_04_write_to_existing_file_but_no_panels(self): panel_names = [panel.attrib["label"] for panel in panels] assert len(panel_names) == 1 - def validate_file_exists_and_pyaedt_tabs_added(self, file_path): + @staticmethod + def validate_file_exists_and_pyaedt_tabs_added(file_path): assert os.path.isfile(file_path) is True assert ET.parse(file_path) is not None tree = ET.parse(file_path) root = tree.getroot() panels = root.findall("./panel") panel_names = [panel.attrib["label"] for panel in panels] - assert "Panel_PyAEDT" in panel_names - files_to_verify = ["images/large/pyansys.png", "images/gallery/PyAEDT.png"] - for file_name in files_to_verify: - assert os.path.isfile(os.path.join(os.path.dirname(file_path), file_name)) + assert "Panel_PyAEDT_Toolkits" in panel_names return root diff --git a/doc/Makefile b/doc/Makefile index 1336617c68a..6b135dfaa78 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -3,10 +3,12 @@ # You can set these variables from the command line, and also # from the environment for the first two. -SPHINXOPTS ?= -j auto --color +SPHINXOPTS ?= -j auto --color -w build_errors.txt SPHINXBUILD ?= sphinx-build SOURCEDIR = source BUILDDIR = _build +LINKCHECKDIR = $(BUILDDIR)/linkcheck +LINKCHECKOPTS = -d $(BUILDDIR)/.doctrees -W --keep-going --color # Put it first so that "make" without argument is like "make help". help: @@ -14,41 +16,67 @@ help: .PHONY: help Makefile -clean: - rm -rf $(BUILDDIR)/* - rm -rf examples/ - find . -type d -name "_autosummary" -exec rm -rf {} + +.install-deps: + @pip freeze | grep -q "vtk-osmesa" && is_vtk_osmesa_installed="yes" || is_vtk_osmesa_installed="no" + @if [ "${ON_CI}" = "True" ] && [ "$$is_vtk_osmesa_installed" != "yes" ]; then \ + @echo "Removing package(s) to avoid conflicts with package(s) needed for CI/CD"; \ + pip uninstall --yes vtk; \ + @echo "Installing CI/CD required package(s)"; \ + pip install --extra-index-url https://wheels.vtk.org vtk-osmesa==9.2.20230527.dev0; \ + fi # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile +%: .install-deps Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -# build html docs in parallel using all available CPUs -# WARNING: this is a resource hog -phtml: - $(SPHINXBUILD) -M html "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -j auto +clean: .install-deps + @echo "Cleaning everything." + rm -rf $(BUILDDIR)/* + rm -rf examples/ + find . -type d -name "_autosummary" -exec rm -rf {} + + +# FIXME: currently linkcheck freezes and further investigation must be performed +# linkcheck: +# @echo "Checking links." +# @$(SPHINXBUILD) -M linkcheck "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(LINKCHECKOPTS) -vv +# @echo +# @echo "Check finished. Report is in $(LINKCHECKDIR)." -phtml-no-examples: - export PYAEDT_SKIP_EXAMPLE="1" - $(SPHINXBUILD) -M html "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -j auto +html-no-examples: .install-deps + @echo "Building HTML pages without examples." + export PYAEDT_DOC_RUN_EXAMPLES="0" + export PYAEDT_DOC_USE_GIF="1" + @# FIXME: currently linkcheck freezes and further investigation must be performed + @# @$(SPHINXBUILD) -M linkcheck "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(LINKCHECKOPTS) $(O) + @$(SPHINXBUILD) -M html "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)." -# Build pdf docs. -pdf: +html: .install-deps + @echo "Building HTML pages with examples." + export PYAEDT_DOC_RUN_EXAMPLES="1" + export PYAEDT_DOC_USE_GIF="1" + @# FIXME: currently linkcheck freezes and further investigation must be performed + @# @$(SPHINXBUILD) -M linkcheck "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(LINKCHECKOPTS) $(O) + @$(SPHINXBUILD) -M html "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)." + +pdf-no-examples: .install-deps + @echo "Building PDF pages without examples." + export PYAEDT_DOC_RUN_EXAMPLES="0" + export PYAEDT_DOC_USE_GIF="0" @$(SPHINXBUILD) -M latex "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) cd $(BUILDDIR)/latex && latexmk -r latexmkrc -pdf *.tex -interaction=nonstopmode || true (test -f $(BUILDDIR)/latex/PyAEDT-Documentation-*.pdf && echo pdf exists) || exit 1 + @echo "Build finished. The PDF pages are in $(BUILDDIR)." -pdf-no-examples: - export PYAEDT_SKIP_EXAMPLE="1" +pdf: .install-deps + @echo "Building PDF pages with examples." + export PYAEDT_DOC_RUN_EXAMPLES="1" + export PYAEDT_DOC_USE_GIF="0" @$(SPHINXBUILD) -M latex "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) cd $(BUILDDIR)/latex && latexmk -r latexmkrc -pdf *.tex -interaction=nonstopmode || true (test -f $(BUILDDIR)/latex/PyAEDT-Documentation-*.pdf && echo pdf exists) || exit 1 - -# build docs like the CI build -cibuild: - mkdir source/examples -p - echo 'Examples' > source/examples/index.rst - echo '========' >> source/examples/index.rst - $(SPHINXBUILD) -M html "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -j auto -w build_errors.txt -N - python print_errors.py + @echo "Build finished. The PDF pages are in $(BUILDDIR)." diff --git a/doc/make.bat b/doc/make.bat index 226691ae917..bc9100f4b20 100644 --- a/doc/make.bat +++ b/doc/make.bat @@ -4,14 +4,32 @@ pushd %~dp0 REM Command file for Sphinx documentation +if "%SPHINXOPTS%" == "" ( + set SPHINXOPTS=-j auto --color -w build_errors.txt +) if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=source set BUILDDIR=_build +set LINKCHECKDIR=\%BUILDDIR%\linkcheck +set LINKCHECKOPTS=-d %BUILDDIR%\.doctrees -W --keep-going --color + +REM This LOCs are used to uninstall and install specific package(s) during CI/CD +for /f %%i in ('pip freeze ^| findstr /c:"vtk-osmesa"') do set is_vtk_osmesa_installed=%%i +if NOT "%is_vtk_osmesa_installed%" == "vtk-osmesa" if "%ON_CI%" == "True" ( + echo "Removing package(s) to avoid conflicts with package(s) needed for CI/CD" + pip uninstall --yes vtk + echo "Installing CI/CD required package(s)" + pip install --extra-index-url https://wheels.vtk.org vtk-osmesa==9.2.20230527.dev0) +REM End of CICD dedicated setup if "%1" == "" goto help +if "%1" == "clean" goto clean +if "%1" == "html" goto html +if "%1" == "html-no-examples" goto html-no-examples if "%1" == "pdf" goto pdf +if "%1" == "pdf-no-examples" goto pdf-no-examples %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( @@ -31,15 +49,66 @@ goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end -:pdf -set PYAEDT_SKIP_EXAMPLE=1 +:clean +echo Cleaning everything +rmdir /s /q %SOURCEDIR%\examples > /NUL 2>&1 +rmdir /s /q %BUILDDIR% > /NUL 2>&1 +for /d /r %SOURCEDIR% %%d in (_autosummary) do @if exist "%%d" rmdir /s /q "%%d" +goto end + +:html +echo Building HTML pages with examples +set PYAEDT_DOC_RUN_EXAMPLES=1 +set PYAEDT_DOC_USE_GIF=1 +::FIXME: currently linkcheck freezes and further investigation must be performed +::%SPHINXBUILD% -M linkcheck %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %LINKCHECKOPTS% %O% +%SPHINXBUILD% -M html %SOURCEDIR% %BUILDDIR% +echo +echo "Build finished. The HTML pages are in %BUILDDIR%." +goto end + +:html-no-examples +echo Building HTML pages without examples +set PYAEDT_DOC_RUN_EXAMPLES=0 +set PYAEDT_DOC_USE_GIF=1 +if not exist "source\examples" mkdir "source\examples" +echo Examples> source\examples\index.rst +echo ========> source\examples\index.rst +::FIXME: currently linkcheck freezes and further investigation must be performed +::%SPHINXBUILD% -M linkcheck %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %LINKCHECKOPTS% %O% +%SPHINXBUILD% -M html %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +echo +echo "Build finished. The HTML pages are in %BUILDDIR%." +goto end +:pdf +echo Building PDF pages with examples +set PYAEDT_DOC_RUN_EXAMPLES=1 +set PYAEDT_DOC_USE_GIF=0 +%SPHINXBUILD% -M latex %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +cd "%BUILDDIR%\latex" +for %%f in (*.tex) do ( +xelatex "%%f" --interaction=nonstopmode) +echo "Build finished. The PDF pages are in %BUILDDIR%." +goto end +:pdf-no-examples +echo Building PDF pages without examples +set PYAEDT_DOC_RUN_EXAMPLES=0 +set PYAEDT_DOC_USE_GIF=0 +if not exist "source\examples" mkdir "source\examples" +echo Examples> source\examples\index.rst +echo ========> source\examples\index.rst +::FIXME: currently linkcheck freezes and further investigation must be performed +::%SPHINXBUILD% -M linkcheck %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %LINKCHECKOPTS% %O% %SPHINXBUILD% -M latex %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% cd "%BUILDDIR%\latex" for %%f in (*.tex) do ( xelatex "%%f" --interaction=nonstopmode) +echo "Build finished. The PDF pages are in %BUILDDIR%." +goto end :end popd diff --git a/doc/source/Getting_started/About.rst b/doc/source/Getting_started/About.rst index ceff4174ca2..5f42b245b34 100644 --- a/doc/source/Getting_started/About.rst +++ b/doc/source/Getting_started/About.rst @@ -26,7 +26,7 @@ and mechanical solvers for comprehensive multiphysics analysis. Tight integration among these solutions provides unprecedented ease of use for setup and faster resolution of complex simulations for design and optimization. -.. image:: https://images.ansys.com/is/image/ansys/ansys-electronics-technology-collage?wid=941&op_usm=0.9,1.0,20,0&fit=constrain,0 +.. image:: ../Resources/aedt_collage.jpg :width: 800 :alt: AEDT Applications :target: https://www.ansys.com/products/electronics diff --git a/doc/source/Resources/PyAEDTInstallerFromDesktop.py b/doc/source/Resources/PyAEDTInstallerFromDesktop.py index 8513222d0d9..7d857c83b6c 100644 --- a/doc/source/Resources/PyAEDTInstallerFromDesktop.py +++ b/doc/source/Resources/PyAEDTInstallerFromDesktop.py @@ -60,10 +60,10 @@ def run_pyinstaller_from_c_python(oDesktop): # enable in debu mode # f.write("import sys\n") # f.write('sys.path.insert(0, r"c:\\ansysdev\\git\\repos\\pyaedt")\n') - f.write("from pyaedt.misc.aedtlib_personalib_install import add_pyaedt_to_aedt\n") + f.write("from pyaedt.workflows.installer.pyaedt_installer import add_pyaedt_to_aedt\n") f.write( - 'add_pyaedt_to_aedt(aedt_version="{}", is_student_version={}, use_sys_lib=False, new_desktop_session=False, pers_dir=r"{}")\n'.format( - oDesktop.GetVersion()[:6], is_student_version(oDesktop), oDesktop.GetPersonalLibDirectory())) + 'add_pyaedt_to_aedt(aedt_version="{}", student_version={}, new_desktop_session=False)\n'.format( + oDesktop.GetVersion()[:6], is_student_version(oDesktop))) command = r'"{}" "{}"'.format(python_exe, python_script) oDesktop.AddMessage("", "", 0, command) @@ -119,6 +119,14 @@ def install_pyaedt(): if args.version < "232": ld_library_path_dirs_to_add.append("{}/Delcross".format(args.edt_root)) os.environ["LD_LIBRARY_PATH"] = ":".join(ld_library_path_dirs_to_add) + ":" + os.getenv("LD_LIBRARY_PATH", "") + os.environ["TK_LIBRARY"] = ("{}/commonfiles/CPython/{}/linx64/Release/python/lib/tk8.5". + format(args.edt_root, + args.python_version.replace( + ".", "_"))) + os.environ["TCL_LIBRARY"] = ("{}/commonfiles/CPython/{}/linx64/Release/python/lib/tcl8.5". + format(args.edt_root, + args.python_version.replace( + ".", "_"))) if not os.path.exists(venv_dir): @@ -139,7 +147,8 @@ def install_pyaedt(): zip_ref.extractall(unzipped_path) run_command( - '"{}" install --no-cache-dir --no-index --find-links={} pyaedt[all,dotnet]'.format(pip_exe, unzipped_path)) + '"{}" install --no-cache-dir --no-index --find-links={} pyaedt[all,dotnet]'.format(pip_exe, + unzipped_path)) run_command( '"{}" install --no-cache-dir --no-index --find-links={} jupyterlab'.format(pip_exe, unzipped_path)) @@ -147,14 +156,11 @@ def install_pyaedt(): run_command('"{}" -m pip install --upgrade pip'.format(python_exe)) run_command('"{}" --default-timeout=1000 install wheel'.format(pip_exe)) run_command('"{}" --default-timeout=1000 install pyaedt[all]'.format(pip_exe)) - # run_command('"{}" --default-timeout=1000 install git+https://github.com/ansys/pyaedt.git@main'.format(pip_exe)) + # run_command( + # '"{}" --default-timeout=1000 install git+https://github.com/ansys/pyaedt.git@main'.format(pip_exe)) run_command('"{}" --default-timeout=1000 install jupyterlab'.format(pip_exe)) run_command('"{}" --default-timeout=1000 install ipython -U'.format(pip_exe)) run_command('"{}" --default-timeout=1000 install ipyvtklink'.format(pip_exe)) - # User can uncomment these lines to install Pyside6 modules - # run_command('"{}" --default-timeout=1000 install pyside6==6.4.0'.format(pip_exe)) - # run_command('"{}" --default-timeout=1000 install pyqtgraph'.format(pip_exe)) - # run_command('"{}" --default-timeout=1000 install qdarkstyle'.format(pip_exe)) if args.version == "231": run_command('"{}" uninstall -y pywin32'.format(pip_exe)) @@ -176,20 +182,6 @@ def install_pyaedt(): run_command('"{}" install --no-cache-dir --no-index --find-links={} pyaedt'.format(pip_exe, unzipped_path)) else: run_command('"{}" --default-timeout=1000 install pyaedt[all]'.format(pip_exe)) - - # if is_windows: - # pyaedt_setup_script = "{}/Lib/site-packages/pyaedt/misc/aedtlib_personalib_install.py".format(venv_dir) - # else: - # pyaedt_setup_script = "{}/lib/python{}/site-packages/pyaedt/misc/aedtlib_personalib_install.py".format( - # venv_dir, args.python_version) - # - # if not os.path.isfile(pyaedt_setup_script): - # sys.exit("[ERROR] PyAEDT was not setup properly since {} file does not exist.".format(pyaedt_setup_script)) - # - # command = '"{}" "{}" --version={}'.format(python_exe, pyaedt_setup_script, args.version) - # if args.student: - # command += " --student" - # run_command(command) sys.exit(0) diff --git a/doc/source/Resources/aedt_collage.jpg b/doc/source/Resources/aedt_collage.jpg new file mode 100644 index 00000000000..9028deeb9fb Binary files /dev/null and b/doc/source/Resources/aedt_collage.jpg differ diff --git a/doc/source/User_guide/pyaedt_file_data/project.rst b/doc/source/User_guide/pyaedt_file_data/project.rst index d9b965f0de1..f9800aade81 100644 --- a/doc/source/User_guide/pyaedt_file_data/project.rst +++ b/doc/source/User_guide/pyaedt_file_data/project.rst @@ -33,7 +33,7 @@ File structure examples: :download:`HFSS 3D Layout Example <../../Resources/hfss3dlayout_project_example.json>` -.. code-block:: json +.. code-block:: { "general": { @@ -145,11 +145,10 @@ File structure examples: ], "monitors": [ # Monitor Name : {Monitor Properties} - ], + ], "native components": { # Component Name : {Component Properties} - - } + } } For a practical demonstration, see the diff --git a/doc/source/conf.py b/doc/source/conf.py index 4629c5687cc..712bb1f6c96 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -7,9 +7,7 @@ import sys import warnings -import pyvista import numpy as np -import json from sphinx_gallery.sorting import FileNameSortKey from ansys_sphinx_theme import (ansys_favicon, get_version_match, pyansys_logo_black, @@ -21,6 +19,7 @@ from docutils.parsers.rst import Directive from docutils import nodes from sphinx import addnodes +from sphinx.util import logging import shutil # <-----------------Override the sphinx pdf builder----------------> @@ -42,8 +41,14 @@ def visit_desc_content(self, node: Element) -> None: # <----------------- End of sphinx pdf builder override----------------> + +logger = logging.getLogger(__name__) + + +# Sphinx event hooks + class PrettyPrintDirective(Directive): - """Renders a constant using ``pprint.pformat`` and inserts into the document.""" + """Renders a constant using ``pprint.pformat`` and inserts it into the document.""" required_arguments = 1 def run(self): @@ -71,11 +76,29 @@ def autodoc_skip_member(app, what, name, obj, skip, options): # return True if exclude else None -def remove_doctree(app, exception): - """Remove the .doctree directory created during the documentation build. - """ - shutil.rmtree(app.doctreedir) +def directory_size(directory_path): + """Compute the size (in megabytes) of a directory.""" + res = 0 + for path, _, files in os.walk(directory_path): + for f in files: + fp = os.path.join(path, f) + res += os.stat(fp).st_size + # Convert in megabytes + res /= 1e6 + return res +def remove_doctree(app, exception): + """Remove the ``.doctree`` directory created during the documentation build.""" + + # Keep the ``doctree`` directory to avoid creating it twice. This is typically helpful in CI/CD + # where we want to build both HTML and PDF pages. + if bool(int(os.getenv("SPHINXBUILD_KEEP_DOCTREEDIR", "0"))): + logger.info(f"Keeping directory {app.doctreedir}.") + else: + size = directory_size(app.doctreedir) + logger.info(f"Removing doctree {app.doctreedir} ({size} MB).") + shutil.rmtree(app.doctreedir, ignore_errors=True) + logger.info(f"Doctree removed.") def setup(app): app.add_directive('pprint', PrettyPrintDirective) @@ -101,20 +124,15 @@ def setup(app): author = "Ansys Inc." cname = os.getenv("DOCUMENTATION_CNAME", "nocname.com") switcher_version = get_version_match(__version__) - -# Check for the local config file, otherwise use default desktop configuration -local_config_file = os.path.join(local_path, "local_config.json") -if os.path.exists(local_config_file): - with open(local_config_file) as f: - config = json.load(f) -else: - config = {"run_examples": True} - release = version = __version__ os.environ["PYAEDT_NON_GRAPHICAL"] = "1" os.environ["PYAEDT_DOC_GENERATION"] = "1" +# Do not run examples by default +run_examples = bool(int(os.getenv("PYAEDT_DOC_RUN_EXAMPLES", "0"))) +use_gif = bool(int(os.getenv("PYAEDT_DOC_USE_GIF", "1"))) + # -- General configuration --------------------------------------------------- # Add any Sphinx_PyAEDT extension module names here as strings. They can be @@ -214,6 +232,7 @@ def setup(app): # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ["_build", "sphinx_boogergreen_theme_1", "Thumbs.db", ".DS_Store", "*.txt"] + inheritance_graph_attrs = dict(rankdir="RL", size='"8.0, 10.0"', fontsize=14, ratio="compress") inheritance_node_attrs = dict(shape="ellipse", fontsize=14, height=0.75, color="dodgerblue1", style="filled") @@ -237,63 +256,59 @@ def setup(app): pygments_style = "sphinx" -# Manage errors -pyvista.set_error_output_file("errors.txt") - -# Ensure that offscreen rendering is used for docs generation -pyvista.OFF_SCREEN = True - -# Preferred plotting style for documentation -# pyvista.set_plot_theme('document') - -# must be less than or equal to the XVFB window size -pyvista.global_theme["window_size"] = np.array([1024, 768]) - -# Save figures in specified directory -pyvista.FIGURE_PATH = os.path.join(os.path.abspath("./images/"), "auto-generated/") -if not os.path.exists(pyvista.FIGURE_PATH): - os.makedirs(pyvista.FIGURE_PATH) - # gallery build requires AEDT install -if is_windows and "PYAEDT_CI_NO_EXAMPLES" not in os.environ: +# if is_windows and bool(os.getenv("PYAEDT_CI_RUN_EXAMPLES", "0")): +if run_examples: + import pyvista + + # PyVista settings + + # Ensure that offscreen rendering is used for docs generation + pyvista.OFF_SCREEN = True + # Save figures in specified directory + pyvista.FIGURE_PATH = os.path.join(os.path.abspath("./images/"), "auto-generated/") + if not os.path.exists(pyvista.FIGURE_PATH): + os.makedirs(pyvista.FIGURE_PATH) + # Necessary for pyvista when building the sphinx gallery + pyvista.BUILDING_GALLERY = True + + # Manage errors + pyvista.set_error_output_file("errors.txt") + # Must be less than or equal to the XVFB window size + pyvista.global_theme["window_size"] = np.array([1024, 768]) # suppress annoying matplotlib bug warnings.filterwarnings( "ignore", category=UserWarning, - message="Matplotlib is currently using agg, which is a non-GUI backend, so cannot show the figure.", + message="Matplotlib is currently using agg, which is a non-GUI backend, so it cannot show the figure.", ) - # necessary for pyvista when building the sphinx gallery - pyvista.BUILDING_GALLERY = True - - if config["run_examples"] and not os.environ.get("PYAEDT_SKIP_EXAMPLE", False): - extensions.append("sphinx_gallery.gen_gallery") - - sphinx_gallery_conf = { - # convert rst to md for ipynb - "pypandoc": True, - # path to your examples scripts - "examples_dirs": ["../../examples/"], - # path where to save gallery generated examples - "gallery_dirs": ["examples"], - # Pattern to search for examples files - "filename_pattern": r"\.py", - # Remove the "Download all examples" button from the top level gallery - "download_all_examples": False, - # Sort gallery examples by file name instead of number of lines (default) - "within_subsection_order": FileNameSortKey, - # directory where function granular galleries are stored - "backreferences_dir": None, - # Modules for which function level galleries are created. In - "doc_module": "ansys-pyaedt", - "image_scrapers": ("pyvista", "matplotlib"), - "ignore_pattern": "flycheck*", - "thumbnail_size": (350, 350), - # 'first_notebook_cell': ("%matplotlib inline\n" - # "from pyvista import set_plot_theme\n" - # "set_plot_theme('document')"), - } + extensions.append("sphinx_gallery.gen_gallery") + sphinx_gallery_conf = { + # convert rst to md for ipynb + "pypandoc": True, + # path to your examples scripts + "examples_dirs": ["../../examples/"], + # path where to save gallery generated examples + "gallery_dirs": ["examples"], + # Pattern to search for examples files + "filename_pattern": r"\.py", + # Remove the "Download all examples" button from the top level gallery + "download_all_examples": False, + # Sort gallery examples by file name instead of number of lines (default) + "within_subsection_order": FileNameSortKey, + # Directory where function granular galleries are stored + "backreferences_dir": None, + # Modules for which function level galleries are created. In + "doc_module": "ansys-pyaedt", + "image_scrapers": ("pyvista", "matplotlib"), + "ignore_pattern": r"flycheck.*", + "thumbnail_size": (350, 350), + } + if not use_gif: + gif_ignore_pattern = r"|.*Maxwell2D_Transient\.py|.*Maxwell2D_DCConduction\.py|.*Hfss_Icepak_Coupling\.py|.*SBR_Time_Plot\.py" + sphinx_gallery_conf["ignore_pattern"] = sphinx_gallery_conf["ignore_pattern"] + gif_ignore_pattern # -- Options for HTML output ------------------------------------------------- html_short_title = html_title = "PyAEDT" @@ -334,7 +349,6 @@ def setup(app): "api_key": os.getenv("MEILISEARCH_PUBLIC_API_KEY", ""), "index_uids": { f"pyaedt-v{get_version_match(__version__).replace('.', '-')}": "PyAEDT", - f"pyedb-v{get_version_match(__version__).replace('.', '-')}": "EDB API", }, }, } diff --git a/doc/source/index.rst b/doc/source/index.rst index e2c60197713..b805fb58c85 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -54,6 +54,7 @@ enabling straightforward and efficient automation in your workflow. .. toctree:: :hidden: + Getting_started/index User_guide/index API/index diff --git a/examples/05-Q3D/Q2D_Armoured_Cable.py b/examples/05-Q3D/Q2D_Armoured_Cable.py index 3e5781417fb..4a0426cbc4f 100644 --- a/examples/05-Q3D/Q2D_Armoured_Cable.py +++ b/examples/05-Q3D/Q2D_Armoured_Cable.py @@ -135,17 +135,17 @@ mod2D.create_coordinate_system(['c_strand_xy_coord', 'c_strand_xy_coord', '0mm'], name='CS_c_strand_1') mod2D.set_working_coordinate_system('CS_c_strand_1') -c1_id = mod2D.create_circle(['0mm', '0mm', '0mm'], 'c_strand_radius', name='c_strand_1', matname='copper') +c1_id = mod2D.create_circle(['0mm', '0mm', '0mm'], 'c_strand_radius', name='c_strand_1', material='copper') c2_id = c1_id.duplicate_along_line(vector=['0mm', '2.0*c_strand_radius', '0mm'], nclones=2) mod2D.duplicate_around_axis(c2_id, axis="Z", angle=360 / core_n_strands, clones=6) c_unite_name = mod2D.unite(q2d.get_all_conductors_names()) fill_id = mod2D.create_circle(['0mm', '0mm', '0mm'], '3*c_strand_radius', name='c_strand_fill', - matname='plastic_pp_carbon_fiber') + material='plastic_pp_carbon_fiber') fill_id.color = (255, 255, 0) xlpe_id = mod2D.create_circle(['0mm', '0mm', '0mm'], '3*c_strand_radius+' + str(core_xlpe_ins_thickness) + 'mm', name='c_strand_xlpe', - matname='plastic_pe_cable_grade') + material='plastic_pe_cable_grade') xlpe_id.color = (0, 128, 128) mod2D.set_working_coordinate_system('Global') @@ -158,7 +158,7 @@ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ filling_id = mod2D.create_circle(['0mm', '0mm', '0mm'], 'filling_radius', name='Filling', - matname='plastic_pp_carbon_fiber') + material='plastic_pp_carbon_fiber') filling_id.color = (255, 255, 180) ##################################################################################### @@ -166,7 +166,7 @@ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ inner_sheath_id = mod2D.create_circle(['0mm', '0mm', '0mm'], 'inner_sheath_radius', name='InnerSheath', - matname='PVC plastic') + material='PVC plastic') inner_sheath_id.color = (0, 0, 0) ##################################################################################### @@ -174,7 +174,7 @@ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ arm_fill_id = mod2D.create_circle(['0mm', '0mm', '0mm'], 'armour_radius', name='ArmourFilling', - matname='plastic_pp_carbon_fiber') + material='plastic_pp_carbon_fiber') arm_fill_id.color = (255, 255, 255) ##################################################################################### @@ -182,7 +182,7 @@ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ outer_sheath_id = mod2D.create_circle(['0mm', '0mm', '0mm'], 'outer_sheath_radius', name='OuterSheath', - matname='PVC plastic') + material='PVC plastic') outer_sheath_id.color = (0, 0, 0) ##################################################################################### @@ -190,9 +190,9 @@ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ arm_strand_1_id = mod2D.create_circle(['0mm', 'armour_centre_pos', '0mm'], '1.1mm', name='arm_strand_1', - matname='steel_stainless') + material='steel_stainless') arm_strand_1_id.color = (128, 128, 64) -arm_strand_1_id.duplicate_around_axis('Z', '360deg/n_arm_strands', nclones='n_arm_strands') +arm_strand_1_id.duplicate_around_axis('Z', '360deg/n_arm_strands', clones='n_arm_strands') arm_strand_names = mod2D.get_objects_w_string('arm_strand') ##################################################################################### @@ -218,9 +218,8 @@ # ~~~~~~~~~~~~~~~~~~~~~~ lumped_length = "100m" -q2d_des_settings = q2d.design_settings() +q2d_des_settings = q2d.design_settings q2d_des_settings['LumpedLength'] = lumped_length -q2d.change_design_settings(q2d_des_settings) ########################################################## # Insert setup and frequency sweep diff --git a/pyaedt/desktop.py b/pyaedt/desktop.py index 38feb28ff68..2e5ee69272f 100644 --- a/pyaedt/desktop.py +++ b/pyaedt/desktop.py @@ -1461,7 +1461,8 @@ def _exception(self, ex_value, tb_data): tblist = tb_trace[0].split("\n") self.logger.error(str(ex_value)) for el in tblist: - self.logger.error(el) + if el: + self.logger.error(el) return str(ex_value) @@ -1744,172 +1745,6 @@ def get_available_toolkits(self): return list(available_toolkits.keys()) - @pyaedt_function_handler() - def add_custom_toolkit(self, toolkit_name): # pragma: no cover - """Add toolkit to AEDT Automation Tab. - - Parameters - ---------- - toolkit_name : str - Name of toolkit to add. - - Returns - ------- - bool - """ - from pyaedt.misc.install_extra_toolkits import available_toolkits - - toolkit = available_toolkits[toolkit_name] - toolkit_name = toolkit_name.replace("_", "") - - def install(package_path, package_name=None): - executable = '"{}"'.format(sys.executable) if is_windows else sys.executable - - commands = [] - if package_path.startswith("git") and package_name: - commands.append([executable, "-m", "pip", "uninstall", "--yes", package_name]) - - commands.append([executable, "-m", "pip", "install", "--upgrade", package_path]) - - if self.aedt_version_id == "2023.1" and is_windows and "AnsysEM" in sys.base_prefix: - commands.append([executable, "-m", "pip", "uninstall", "--yes", "pywin32"]) - - for command in commands: - if is_linux: - p = subprocess.Popen(command) - else: - p = subprocess.Popen(" ".join(command)) - p.wait() - - install(toolkit["pip"], toolkit.get("package_name", None)) - import site - - packages = site.getsitepackages() - full_path = None - for pkg in packages: - if os.path.exists(os.path.join(pkg, toolkit["toolkit_script"])): - full_path = os.path.join(pkg, toolkit["toolkit_script"]) - break - if not full_path: - raise FileNotFoundError("Error finding the package.") - self.add_script_to_menu( - toolkit_name=toolkit_name, - script_path=full_path, - script_image=toolkit, - product=toolkit["installation_path"], - copy_to_personal_lib=False, - add_pyaedt_desktop_init=False, - ) - - @pyaedt_function_handler() - def add_script_to_menu( - self, - toolkit_name, - script_path, - script_image=None, - product="Project", - copy_to_personal_lib=True, - add_pyaedt_desktop_init=True, - ): - """Add a script to the ribbon menu. - - .. note:: - This method is available in AEDT 2023 R2 and later. PyAEDT must be installed - in AEDT to allow this method to run. For more information, see `Installation - `_. - - Parameters - ---------- - toolkit_name : str - Name of the toolkit to appear in AEDT. - script_path : str - Full path to the script file. The script will be moved to Personal Lib. - script_image : str, optional - Full path to the image logo (a 30x30 pixel PNG file) to add to the UI. - The default is ``None``. - product : str, optional - Product to which the toolkit applies. The default is ``"Project"``, in which case - it applies to all designs. You can also specify a product, such as ``"HFSS"``. - copy_to_personal_lib : bool, optional - Whether to copy the script to Personal Lib or link the original script. Default is ``True``. - - Returns - ------- - bool - - """ - if not os.path.exists(script_path): - self.logger.error("Script does not exists.") - return False - from pyaedt.misc.install_extra_toolkits import write_toolkit_config - - toolkit_dir = os.path.join(self.personallib, "Toolkits") - aedt_version = self.aedt_version_id - tool_dir = os.path.join(toolkit_dir, product, toolkit_name) - lib_dir = os.path.join(tool_dir, "Lib") - toolkit_rel_lib_dir = os.path.relpath(lib_dir, tool_dir) - if is_linux and aedt_version <= "2023.1": - toolkit_rel_lib_dir = os.path.join("Lib", toolkit_name) - lib_dir = os.path.join(toolkit_dir, toolkit_rel_lib_dir) - toolkit_rel_lib_dir = "../../" + toolkit_rel_lib_dir - os.makedirs(lib_dir, exist_ok=True) - os.makedirs(tool_dir, exist_ok=True) - dest_script_path = script_path - if copy_to_personal_lib: - dest_script_path = os.path.join(lib_dir, os.path.split(script_path)[-1]) - shutil.copy2(script_path, dest_script_path) - files_to_copy = ["Run_PyAEDT_Toolkit_Script"] - executable_version_agnostic = sys.executable - for file_name in files_to_copy: - src = os.path.join(pathname, "misc", file_name + ".py_build") - dst = os.path.join(tool_dir, file_name.replace("_", " ") + ".py") - if not os.path.isfile(src): - raise FileNotFoundError("File not found: {}".format(src)) - with open_file(src, "r") as build_file: - with open_file(dst, "w") as out_file: - self.logger.info("Building to " + dst) - build_file_data = build_file.read() - build_file_data = ( - build_file_data.replace("##TOOLKIT_REL_LIB_DIR##", toolkit_rel_lib_dir) - .replace("##PYTHON_EXE##", executable_version_agnostic) - .replace("##PYTHON_SCRIPT##", dest_script_path) - ) - build_file_data = build_file_data.replace(" % version", "") - out_file.write(build_file_data) - if aedt_version >= "2023.2": - if not script_image: - script_image = os.path.join(os.path.dirname(__file__), "misc", "images", "large", "pyansys.png") - write_toolkit_config(os.path.join(toolkit_dir, product), lib_dir, toolkit_name, toolkit=script_image) - self.logger.info("{} toolkit installed.".format(toolkit_name)) - return True - - @pyaedt_function_handler() - def remove_script_from_menu(self, toolkit_name, product="Project"): - """Remove a toolkit script from the menu. - - Parameters - ---------- - toolkit_name : str - Name of the toolkit to remove. - product : str, optional - Product to which the toolkit applies. The default is ``"Project"``, in which case - it applies to all designs. You can also specify a product, such as ``"HFSS"``. - - Returns - ------- - bool - """ - from pyaedt.misc.install_extra_toolkits import remove_toolkit_config - - toolkit_dir = os.path.join(self.personallib, "Toolkits") - aedt_version = self.aedt_version_id - tool_dir = os.path.join(toolkit_dir, product, toolkit_name) - shutil.rmtree(tool_dir, ignore_errors=True) - if aedt_version >= "2023.2": - remove_toolkit_config(os.path.join(toolkit_dir, product), toolkit_name) - self.logger.info("{} toolkit removed successfully.".format(toolkit_name)) - return True - @pyaedt_function_handler() def submit_job( self, diff --git a/pyaedt/generic/general_methods.py b/pyaedt/generic/general_methods.py index 432acb95a12..5dad1863a90 100644 --- a/pyaedt/generic/general_methods.py +++ b/pyaedt/generic/general_methods.py @@ -110,7 +110,6 @@ def _exception(ex_info, func, args, kwargs, message="Type Error"): ] if any(exc in trace for exc in exceptions): continue - # if func.__name__ in trace: for el in trace.split("\n"): _write_mes(el) for trace in tb_trace: @@ -118,14 +117,10 @@ def _exception(ex_info, func, args, kwargs, message="Type Error"): continue tblist = trace.split("\n") for el in tblist: - # if func.__name__ in el: - _write_mes(el) + if el: + _write_mes(el) _write_mes("{} on {}".format(message, func.__name__)) - # try: - # _write_mes(ex_info[1].args[0]) - # except (IndexError, AttributeError): - # pass message_to_print = "" messages = "" @@ -138,7 +133,6 @@ def _exception(ex_info, func, args, kwargs, message="Type Error"): pass if "error" in messages: message_to_print = messages[messages.index("[error]") :] - # _write_mes("{} - {} - {}.".format(ex_info[1], func.__name__, message.upper())) if message_to_print: _write_mes("Last Electronics Desktop Message - " + message_to_print) diff --git a/pyaedt/misc/aedtlib_personalib_install.py b/pyaedt/misc/aedtlib_personalib_install.py deleted file mode 100644 index 2eb3b9740c6..00000000000 --- a/pyaedt/misc/aedtlib_personalib_install.py +++ /dev/null @@ -1,250 +0,0 @@ -import argparse -import os -import shutil -import sys -import warnings -from xml.dom.minidom import parseString -import xml.etree.ElementTree as ET -from xml.etree.ElementTree import ParseError - -current_dir = os.path.dirname(os.path.realpath(__file__)) -pyaedt_path = os.path.normpath( - os.path.join( - current_dir, - "..", - ) -) -sys.path.append(os.path.normpath(os.path.join(pyaedt_path, ".."))) - -is_linux = os.name == "posix" -is_windows = not is_linux -pid = 0 - - -def main(): - args = parse_arguments() - add_pyaedt_to_aedt( - args.version, is_student_version=args.student, use_sys_lib=args.sys_lib, new_desktop_session=args.new_session - ) - - -def parse_arguments(): - parser = argparse.ArgumentParser(description="Install PyAEDT and setup PyAEDT toolkits in AEDT.") - parser.add_argument( - "--version", "-v", default="231", metavar="XY.Z", help="AEDT three-digit version (e.g. 231). Default=231" - ) - parser.add_argument( - "--student", "--student_version", action="store_true", help="Install toolkits for AEDT Student Version." - ) - parser.add_argument("--sys_lib", "--syslib", action="store_true", help="Install toolkits in SysLib.") - parser.add_argument( - "--new_session", action="store_true", help="Start a new session of AEDT after installing PyAEDT." - ) - - args = parser.parse_args() - args = process_arguments(args, parser) - return args - - -def process_arguments(args, parser): - if len(args.version) != 3: - parser.print_help() - parser.error("Version should be a three digit number (e.g. 231)") - - args.version = "20" + args.version[-3:-1] + "." + args.version[-1:] - return args - - -def add_pyaedt_to_aedt( - aedt_version, is_student_version=False, use_sys_lib=False, new_desktop_session=False, sys_dir="", pers_dir="" -): - if not (sys_dir or pers_dir): - from pyaedt import Desktop - from pyaedt.generic.general_methods import grpc_active_sessions - from pyaedt.generic.settings import settings - - sessions = grpc_active_sessions(aedt_version, is_student_version) - close_on_exit = True - if not sessions: - if not new_desktop_session: - print("Launching a new AEDT desktop session.") - new_desktop_session = True - else: - close_on_exit = False - settings.use_grpc_api = True - with Desktop( - specified_version=aedt_version, - non_graphical=new_desktop_session, - new_desktop_session=new_desktop_session, - student_version=is_student_version, - close_on_exit=close_on_exit, - ) as d: - desktop = sys.modules["__main__"].oDesktop - pers1 = os.path.join(desktop.GetPersonalLibDirectory(), "pyaedt") - pid = desktop.GetProcessID() - # Linking pyaedt in PersonalLib for IronPython compatibility. - if os.path.exists(pers1): - d.logger.info("PersonalLib already mapped.") - else: - if is_windows: - os.system('mklink /D "{}" "{}"'.format(pers1, pyaedt_path)) - else: - os.system('ln -s "{}" "{}"'.format(pyaedt_path, pers1)) - sys_dir = d.syslib - pers_dir = d.personallib - if pid and new_desktop_session: - try: - os.kill(pid, 9) - except Exception: - pass - - toolkits = ["Project"] - # Bug on Linux 23.1 and before where Project level toolkits don't show up. Thus copying to individual design - # toolkits. - if is_linux and aedt_version <= "2023.1": - toolkits = [ - "2DExtractor", - "CircuitDesign", - "HFSS", - "HFSS-IE", - "HFSS3DLayoutDesign", - "Icepak", - "Maxwell2D", - "Maxwell3D", - "Q3DExtractor", - "Mechanical", - ] - - for product in toolkits: - if use_sys_lib: - try: - sys_dir = os.path.join(sys_dir, "Toolkits") - install_toolkit(sys_dir, product, aedt_version) - print("Installed toolkit for {} in sys lib.".format(product)) - # d.logger.info("Installed toolkit for {} in sys lib.".format(product)) - - except IOError: - pers_dir = os.path.join(pers_dir, "Toolkits") - install_toolkit(pers_dir, product, aedt_version) - print("Installed toolkit for {} in sys lib.".format(product)) - # d.logger.info("Installed toolkit for {} in personal lib.".format(product)) - else: - pers_dir = os.path.join(pers_dir, "Toolkits") - install_toolkit(pers_dir, product, aedt_version) - print("Installed toolkit for {} in sys lib.".format(product)) - # d.logger.info("Installed toolkit for {} in personal lib.".format(product)) - - -def install_toolkit(toolkit_dir, product, aedt_version): - tool_dir = os.path.join(toolkit_dir, product, "PyAEDT") - lib_dir = os.path.join(tool_dir, "Lib") - toolkit_rel_lib_dir = os.path.relpath(lib_dir, tool_dir) - # Bug on Linux 23.1 and before where Project level toolkits don't show up. Thus copying to individual design - # toolkits. - if is_linux and aedt_version <= "2023.1": - toolkit_rel_lib_dir = os.path.join("Lib", "PyAEDT") - lib_dir = os.path.join(toolkit_dir, toolkit_rel_lib_dir) - toolkit_rel_lib_dir = "../../" + toolkit_rel_lib_dir - tool_dir = os.path.join(toolkit_dir, product, "PyAEDT") - os.makedirs(lib_dir, exist_ok=True) - os.makedirs(tool_dir, exist_ok=True) - files_to_copy = ["Console", "Run_PyAEDT_Script", "Jupyter"] - # Remove hard-coded version number from Python virtual environment path, and replace it with the corresponding AEDT - # version's Python virtual environment. - version_agnostic = False - if aedt_version[2:6].replace(".", "") in sys.executable: - executable_version_agnostic = sys.executable.replace(aedt_version[2:6].replace(".", ""), "%s") - version_agnostic = True - else: - executable_version_agnostic = sys.executable - jupyter_executable = executable_version_agnostic.replace("python" + exe(), "jupyter" + exe()) - ipython_executable = executable_version_agnostic.replace("python" + exe(), "ipython" + exe()) - for file_name in files_to_copy: - with open(os.path.join(current_dir, file_name + ".py_build"), "r") as build_file: - file_name_dest = file_name.replace("_", " ") + ".py" - with open(os.path.join(tool_dir, file_name_dest), "w") as out_file: - print("Building to " + os.path.join(tool_dir, file_name_dest)) - build_file_data = build_file.read() - build_file_data = ( - build_file_data.replace("##TOOLKIT_REL_LIB_DIR##", toolkit_rel_lib_dir) - .replace("##PYTHON_EXE##", executable_version_agnostic) - .replace("##IPYTHON_EXE##", ipython_executable) - .replace("##JUPYTER_EXE##", jupyter_executable) - ) - if not version_agnostic: - build_file_data = build_file_data.replace(" % version", "") - out_file.write(build_file_data) - shutil.copyfile(os.path.join(current_dir, "console_setup.py"), os.path.join(lib_dir, "console_setup.py")) - shutil.copyfile( - os.path.join(current_dir, "jupyter_template.ipynb"), - os.path.join(lib_dir, "jupyter_template.ipynb"), - ) - if aedt_version >= "2023.2": - write_tab_config(os.path.join(toolkit_dir, product), lib_dir) - - -def write_tab_config(product_toolkit_dir, pyaedt_lib_dir, force_write=False): - tab_config_file_path = os.path.join(product_toolkit_dir, "TabConfig.xml") - if not os.path.isfile(tab_config_file_path) or force_write: - root = ET.Element("TabConfig") - else: - try: - tree = ET.parse(tab_config_file_path) - except ParseError as e: - warnings.warn("Unable to parse %s\nError received = %s" % (tab_config_file_path, str(e))) - return - root = tree.getroot() - - panels = root.findall("./panel") - if panels: - panel_names = [panel.attrib["label"] for panel in panels] - if "Panel_PyAEDT" in panel_names: - # Remove previously existing PyAEDT panel and update with newer one. - panel = [panel for panel in panels if panel.attrib["label"] == "Panel_PyAEDT"][0] - root.remove(panel) - - # Write a new "Panel_PyAEDT" sub-element. - panel = ET.SubElement(root, "panel", label="Panel_PyAEDT") - gallery = ET.SubElement(panel, "gallery", imagewidth="120", imageheight="72") - image_rel_path = os.path.relpath(pyaedt_lib_dir, product_toolkit_dir).replace("\\", "/") + "/" - if image_rel_path == "./": - image_rel_path = "" - ET.SubElement(gallery, "button", label="PyAEDT", isLarge="1", image=image_rel_path + "images/large/pyansys.png") - group = ET.SubElement(gallery, "group", label="PyAEDT Menu", image=image_rel_path + "images/gallery/PyAEDT.png") - ET.SubElement(group, "button", label="Console", script="PyAEDT/Console") - ET.SubElement(group, "button", label="Jupyter Notebook", script="PyAEDT/Jupyter") - ET.SubElement(group, "button", label="Run PyAEDT Script", script="PyAEDT/Run PyAEDT Script") - - # Backup any existing file if present - if os.path.isfile(tab_config_file_path): - shutil.copy(tab_config_file_path, tab_config_file_path + ".orig") - - write_pretty_xml(root, tab_config_file_path) - - files_to_copy = ["images/large/pyansys.png", "images/gallery/PyAEDT.png"] - for file_name in files_to_copy: - dest_file = os.path.normpath(os.path.join(pyaedt_lib_dir, file_name)) - os.makedirs(os.path.dirname(dest_file), exist_ok=True) - shutil.copy(os.path.normpath(os.path.join(current_dir, file_name)), dest_file) - - -def write_pretty_xml(root, file_path): - """Write the XML in a pretty format.""" - # If we use the commented code below, then the previously existing lines will have double lines added. We need to - # split and ignore the double lines. - # xml_str = parseString(ET.tostring(root)).toprettyxml(indent=" " * 4) - lines = [line for line in parseString(ET.tostring(root)).toprettyxml(indent=" " * 4).split("\n") if line.strip()] - xml_str = "\n".join(lines) - - with open(file_path, "w") as f: - f.write(xml_str) - - -def exe(): - if is_windows: - return ".exe" - return "" - - -if __name__ == "__main__": - main() diff --git a/pyaedt/misc/images/gallery/PyAEDT.png b/pyaedt/misc/images/gallery/PyAEDT.png deleted file mode 100644 index a51a6cd31aa..00000000000 Binary files a/pyaedt/misc/images/gallery/PyAEDT.png and /dev/null differ diff --git a/pyaedt/misc/install_extra_toolkits.py b/pyaedt/misc/install_extra_toolkits.py deleted file mode 100644 index fc89a420d04..00000000000 --- a/pyaedt/misc/install_extra_toolkits.py +++ /dev/null @@ -1,132 +0,0 @@ -import os -import shutil -import warnings -import xml.etree.ElementTree as ET -from xml.etree.ElementTree import ParseError - -from pyaedt.misc.aedtlib_personalib_install import current_dir -from pyaedt.misc.aedtlib_personalib_install import write_pretty_xml - -available_toolkits = { - "AntennaWizard": { - "pip": "git+https://github.com/ansys/pyaedt-antenna-toolkit.git", - "image": "pyansys.png", - "toolkit_script": "ansys/aedt/toolkits/antenna/run_toolkit.py", - "installation_path": "HFSS", - "package_name": "ansys.aedt.toolkits.antenna", - }, - "ChokeWizard": { - "pip": "git+https://github.com/ansys/pyaedt-choke-toolkit.git", - "image": "pyansys.png", - "toolkit_script": "ansys/aedt/toolkits/choke/choke_toolkit.py", - "installation_path": "Project", - "package_name": "ansys.aedt.toolkits.choke", - }, - "MagnetSegmentationWizard": { - "pip": "git+https://github.com/ansys/magnet-segmentation-toolkit.git", - "image": "pyansys.png", - "toolkit_script": "ansys/aedt/toolkits/magnet_segmentation/run_toolkit.py", - "installation_path": "Maxwell3d", - "package_name": "magnet-segmentation-toolkit", - }, -} - - -def write_toolkit_config(product_toolkit_dir, pyaedt_lib_dir, toolkitname, toolkit, force_write=False): - """Write a toolkit configuration file and, if needed a button in Automation menu.""" - tab_config_file_path = os.path.join(product_toolkit_dir, "TabConfig.xml") - if not os.path.isfile(tab_config_file_path) or force_write: - root = ET.Element("TabConfig") - else: - try: - tree = ET.parse(tab_config_file_path) - except ParseError as e: - warnings.warn("Unable to parse %s\nError received = %s" % (tab_config_file_path, str(e))) - return - root = tree.getroot() - - panels = root.findall("./panel") - if panels: - panel_names = [panel.attrib["label"] for panel in panels] - if "Panel_PyAEDT_Toolkits" in panel_names: - # Remove previously existing PyAEDT panel and update with newer one. - panel = [panel for panel in panels if panel.attrib["label"] == "Panel_PyAEDT_Toolkits"][0] - else: - panel = ET.SubElement(root, "panel", label="Panel_PyAEDT_Toolkits") - else: - panel = ET.SubElement(root, "panel", label="Panel_PyAEDT_Toolkits") - - # Write a new "Panel_PyAEDT_Toolkits" sub-element. - image_rel_path = os.path.relpath(pyaedt_lib_dir, product_toolkit_dir).replace("\\", "/") + "/" - if image_rel_path == "./": - image_rel_path = "" - - buttons = panel.findall("./button") - if buttons: - button_names = [button.attrib["label"] for button in buttons] - if toolkitname in button_names: - # Remove previously existing PyAEDT panel and update with newer one. - b = [button for button in buttons if button.attrib["label"] == toolkitname][0] - panel.remove(b) - if isinstance(toolkit, str) and os.path.exists(toolkit): - image_name = os.path.split(toolkit)[-1] - else: - image_name = toolkit["image"] - image_abs_path = image_rel_path + "images/large/{}".format(image_name) - ET.SubElement( - panel, - "button", - label=toolkitname, - isLarge="1", - image=image_abs_path, - script="{}/Run PyAEDT Toolkit Script".format(toolkitname), - ) - - # Backup any existing file if present - if os.path.isfile(tab_config_file_path): - shutil.copy(tab_config_file_path, tab_config_file_path + ".orig") - - write_pretty_xml(root, tab_config_file_path) - - files_to_copy = ["images/large/{}".format(image_name)] - for file_name in files_to_copy: - dest_file = os.path.normpath(os.path.join(pyaedt_lib_dir, file_name)) - os.makedirs(os.path.dirname(dest_file), exist_ok=True) - if isinstance(toolkit, str): - shutil.copy(toolkit, dest_file) - else: - shutil.copy(os.path.normpath(os.path.join(current_dir, file_name)), dest_file) - - -def remove_toolkit_config(product_toolkit_dir, toolkitname): - """Remove a toolkit configuration file and, if needed a button in Automation menu.""" - tab_config_file_path = os.path.join(product_toolkit_dir, "TabConfig.xml") - if not os.path.isfile(tab_config_file_path): - return True - try: - tree = ET.parse(tab_config_file_path) - except ParseError as e: - warnings.warn("Unable to parse %s\nError received = %s" % (tab_config_file_path, str(e))) - return - root = tree.getroot() - - panels = root.findall("./panel") - if panels: - panel_names = [panel.attrib["label"] for panel in panels] - if "Panel_PyAEDT_Toolkits" in panel_names: - # Remove previously existing PyAEDT panel and update with newer one. - panel = [panel for panel in panels if panel.attrib["label"] == "Panel_PyAEDT_Toolkits"][0] - else: - panel = ET.SubElement(root, "panel", label="Panel_PyAEDT_Toolkits") - else: - panel = ET.SubElement(root, "panel", label="Panel_PyAEDT_Toolkits") - - buttons = panel.findall("./button") - if buttons: - button_names = [button.attrib["label"] for button in buttons] - if toolkitname in button_names: - # Remove previously existing PyAEDT panel and update with newer one. - b = [button for button in buttons if button.attrib["label"] == toolkitname][0] - panel.remove(b) - - write_pretty_xml(root, tab_config_file_path) diff --git a/pyaedt/modeler/modeler3d.py b/pyaedt/modeler/modeler3d.py index 590a94e0420..b2f19ef0ec0 100644 --- a/pyaedt/modeler/modeler3d.py +++ b/pyaedt/modeler/modeler3d.py @@ -877,7 +877,7 @@ def objects_in_bounding_box(self, bounding_box, check_solids=True, check_lines=T return objects @pyaedt_function_handler() - def import_nastran(self, file_path, import_lines=True, lines_thickness=0, import_solids=True): + def import_nastran(self, file_path, import_lines=True, lines_thickness=0, **kwargs): """Import Nastran file into 3D Modeler by converting the faces to stl and reading it. The solids are translated directly to AEDT format. @@ -890,17 +890,46 @@ def import_nastran(self, file_path, import_lines=True, lines_thickness=0, import lines_thickness : float, optional Whether to thicken lines after creation and it's default value. Every line will be parametrized with a design variable called ``xsection_linename``. - import_solids : bool, optional - Whether to import the solids or only triangles. Default is ``True``. Returns ------- List of :class:`pyaedt.modeler.Object3d.Object3d` """ - nas_to_dict = {"Points": {}, "PointsId": {}, "Triangles": {}, "Lines": {}, "Solids": {}} + + def _write_solid_stl(triangle, nas_to_dict): + try: + points = [nas_to_dict["Points"][id] for id in triangle] + except KeyError: + return + fc = GeometryOperators.get_polygon_centroid(points) + v1 = points[0] + v2 = points[1] + cv1 = GeometryOperators.v_points(fc, v1) + cv2 = GeometryOperators.v_points(fc, v2) + if cv2[0] == cv1[0] == 0.0 and cv2[1] == cv1[1] == 0.0: + n = [0, 0, 1] + elif cv2[0] == cv1[0] == 0.0 and cv2[2] == cv1[2] == 0.0: + n = [0, 1, 0] + elif cv2[1] == cv1[1] == 0.0 and cv2[2] == cv1[2] == 0.0: + n = [1, 0, 0] + else: + n = GeometryOperators.v_cross(cv1, cv2) + + normal = GeometryOperators.normalize_vector(n) + if normal: + f.write(" facet normal {} {} {}\n".format(normal[0], normal[1], normal[2])) + f.write(" outer loop\n") + f.write(" vertex {} {} {}\n".format(points[0][0], points[0][1], points[0][2])) + f.write(" vertex {} {} {}\n".format(points[1][0], points[1][1], points[1][2])) + f.write(" vertex {} {} {}\n".format(points[2][0], points[2][1], points[2][2])) + f.write(" endloop\n") + f.write(" endfacet\n") + + nas_to_dict = {"Points": {}, "PointsId": {}, "Triangles": [], "Lines": {}, "Solids": {}} self.logger.reset_timer() self.logger.info("Loading file") + el_ids = [] with open_file(file_path, "r") as f: lines = f.read().splitlines() id = 0 @@ -927,22 +956,11 @@ def import_nastran(self, file_path, import_lines=True, lines_thickness=0, import nas_to_dict["PointsId"][grid_id] = grid_id id += 1 else: - if tria_id in nas_to_dict["Triangles"]: - nas_to_dict["Triangles"][tria_id].append( - [ - int(n1), - int(n2), - int(n3), - ] - ) - else: - nas_to_dict["Triangles"][tria_id] = [ - [ - int(n1), - int(n2), - int(n3), - ] - ] + tri = [int(n1), int(n2), int(n3)] + tri.sort() + if tri not in nas_to_dict["Triangles"]: + nas_to_dict["Triangles"].append(tri) + elif line_type in ["GRID*", "CTRIA3*"]: grid_id = int(line[8:24]) if line_type == "CTRIA3*": @@ -955,7 +973,7 @@ def import_nastran(self, file_path, import_lines=True, lines_thickness=0, import n2 = n2[0] + n2[1:].replace("-", "e-") n3 = line[72:88].strip() - if not n3 or n3 == "*": + if not n3 or n3.startswith("*"): lk += 1 n3 = lines[lk][8:24].strip() if "-" in n3[1:]: @@ -965,46 +983,60 @@ def import_nastran(self, file_path, import_lines=True, lines_thickness=0, import nas_to_dict["PointsId"][grid_id] = id id += 1 else: - if tria_id in nas_to_dict["Triangles"]: - nas_to_dict["Triangles"][tria_id].append( - [ - int(n1), - int(n2), - int(n3), - ] - ) - else: - nas_to_dict["Triangles"][tria_id] = [ - [ - int(n1), - int(n2), - int(n3), - ] - ] + tri = [int(n1), int(n2), int(n3)] + tri.sort() + if tri not in nas_to_dict["Triangles"]: + nas_to_dict["Triangles"].append(tri) + elif line_type in ["CPENTA", "CHEXA", "CTETRA"]: - obj_id = int(line[16:24]) - n1 = int(line[24:32]) - n2 = int(line[32:40]) - n3 = int(line[40:48]) - n4 = int(line[48:56]) - obj_list = [line_type, n1, n2, n3, n4] + obj_id = line[16:24].strip() + n = [] + el_id = line[24:32].strip() + # n = [int(line[24:32])] + n.append(int(line[32:40])) + n.append(int(line[40:48])) + n.append(int(line[48:56])) if line_type == "CPENTA": - n5 = int(line[56:64]) - n6 = int(line[64:72]) - obj_list.extend([n5, n6]) + n.append(int(line[56:64])) + n.append(int(line[64:72])) if line_type == "CHEXA": - n5 = int(line[56:64]) - n6 = int(line[64:72]) + n.append(int(line[56:64])) + n.append(int(line[64:72])) lk += 1 - n7 = int(lines[lk][8:16].strip()) - n8 = int(lines[lk][16:24].strip()) + n.append(int(lines[lk][8:16].strip())) + n.append(int(lines[lk][16:24].strip())) + from itertools import combinations + + tris = [] + for k in list(combinations(n, 3)): + tri = [int(k[0]), int(k[1]), int(k[2])] + tris.append(tri) + nas_to_dict["Solids"]["{}_{}".format(el_id, obj_id)] = tris + if el_id not in el_ids: + el_ids.append(el_id) + elif line_type in ["CTETRA*"]: + obj_id = line[8:24].strip() + n = [] + el_id = line[24:40].strip() + # n.append(line[24:40].strip()) + n.append(line[40:56].strip()) + + n.append(line[56:72].strip()) + lk += 1 + n.extend([lines[lk][i : i + 16] for i in range(16, len(lines[lk]), 16)]) + + from itertools import combinations + + tris = [] + for k in list(combinations(n, 3)): + tri = [int(k[0]), int(k[1]), int(k[2])] + tris.append(tri) + + nas_to_dict["Solids"]["{}_{}".format(el_id, obj_id)] = tris + if el_id not in el_ids: + el_ids.append(el_id) - obj_list.extend([n5, n6, n7, n8]) - if obj_id in nas_to_dict["Solids"]: - nas_to_dict["Solids"][obj_id].append(obj_list) - else: - nas_to_dict["Solids"][obj_id] = [[i for i in obj_list]] elif line_type in ["CROD", "CBEAM"]: obj_id = int(line[16:24]) n1 = int(line[24:32]) @@ -1021,40 +1053,21 @@ def import_nastran(self, file_path, import_lines=True, lines_thickness=0, import self.logger.info("Creating STL file with detected faces") f = open(os.path.join(self._app.working_directory, self._app.design_name + "_test.stl"), "w") f.write("solid PyaedtStl\n") - for triangles in nas_to_dict["Triangles"].values(): - for triangle in triangles: - try: - points = [nas_to_dict["Points"][id] for id in triangle] - except KeyError: - continue - fc = GeometryOperators.get_polygon_centroid(points) - v1 = points[0] - v2 = points[1] - cv1 = GeometryOperators.v_points(fc, v1) - cv2 = GeometryOperators.v_points(fc, v2) - if cv2[0] == cv1[0] == 0.0 and cv2[1] == cv1[1] == 0.0: - n = [0, 0, 1] - elif cv2[0] == cv1[0] == 0.0 and cv2[2] == cv1[2] == 0.0: - n = [0, 1, 0] - elif cv2[1] == cv1[1] == 0.0 and cv2[2] == cv1[2] == 0.0: - n = [1, 0, 0] - else: - n = GeometryOperators.v_cross(cv1, cv2) - - normal = GeometryOperators.normalize_vector(n) - if normal: - f.write(" facet normal {} {} {}\n".format(normal[0], normal[1], normal[2])) - f.write(" outer loop\n") - f.write(" vertex {} {} {}\n".format(points[0][0], points[0][1], points[0][2])) - f.write(" vertex {} {} {}\n".format(points[1][0], points[1][1], points[1][2])) - f.write(" vertex {} {} {}\n".format(points[2][0], points[2][1], points[2][2])) - f.write(" endloop\n") - f.write(" endfacet\n") + for triangle in nas_to_dict["Triangles"]: + _write_solid_stl(triangle, nas_to_dict) f.write("endsolid\n") + for solidid, solid_triangles in nas_to_dict["Solids"].items(): + f.write("solid Solid_{}\n".format(solidid)) + for triangle in solid_triangles: + _write_solid_stl(triangle, nas_to_dict) + f.write("endsolid\n") f.close() self.logger.info("STL file created") self.import_3d_cad(os.path.join(self._app.working_directory, self._app.design_name + "_test.stl")) + for el in el_ids: + obj_names = [i for i in self.solid_names if i.startswith("Solid_{}_".format(el))] + self.create_group(obj_names, group_name=el) self.logger.info_timer("Faces imported") if import_lines: @@ -1087,48 +1100,6 @@ def import_nastran(self, file_path, import_lines=True, lines_thickness=0, import if not lines_thickness and out_poly: self.generate_object_history(out_poly) - if import_solids and nas_to_dict["Solids"]: - self.logger.reset_timer() - self.logger.info("Loading solids") - for solid_pid in nas_to_dict["Solids"]: - for solid in nas_to_dict["Solids"][solid_pid]: - points = [nas_to_dict["Points"][id] for id in solid[1:]] - if solid[0] == "CPENTA": - element1 = self._app.modeler.create_polyline( - points=[points[0], points[1], points[2]], cover_surface=True, close_surface=True - ) - element2 = self._app.modeler.create_polyline( - points=[points[3], points[4], points[5]], cover_surface=True, close_surface=True - ) - self._app.modeler.connect([element1.name, element2.name]) - element1.group_name = "PID_" + str(solid_pid) - elif solid[0] == "CHEXA": - element1 = self._app.modeler.create_polyline( - points=[points[0], points[1], points[2], points[3]], cover_surface=True, close_surface=True - ) - element2 = self._app.modeler.create_polyline( - points=[points[4], points[5], points[6], points[7]], cover_surface=True, close_surface=True - ) - self._app.modeler.connect([element1.name, element2.name]) - element1.group_name = "PID_" + str(solid_pid) - elif solid[0] == "CTETRA": - element1 = self._app.modeler.create_polyline( - points=[points[0], points[1], points[2]], cover_surface=True, close_surface=True - ) - element2 = self._app.modeler.create_polyline( - points=[points[0], points[1], points[3]], cover_surface=True, close_surface=True - ) - element3 = self._app.modeler.create_polyline( - points=[points[0], points[2], points[3]], cover_surface=True, close_surface=True - ) - element4 = self._app.modeler.create_polyline( - points=[points[1], points[2], points[3]], cover_surface=True, close_surface=True - ) - self._app.modeler.unite([element1.name, element2.name, element3.name, element4.name]) - element1.group_name = "PID_" + str(solid_pid) - - self.logger.info_timer("Solids loaded") - objs_after = [i for i in self.object_names] new_objects = [self[i] for i in objs_after if i not in objs_before] return new_objects diff --git a/pyaedt/workflows/__init__.py b/pyaedt/workflows/__init__.py new file mode 100644 index 00000000000..3bc3c8e5b19 --- /dev/null +++ b/pyaedt/workflows/__init__.py @@ -0,0 +1,21 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/pyaedt/workflows/circuit/__init__.py b/pyaedt/workflows/circuit/__init__.py new file mode 100644 index 00000000000..3bc3c8e5b19 --- /dev/null +++ b/pyaedt/workflows/circuit/__init__.py @@ -0,0 +1,21 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/pyaedt/workflows/customize_automation_tab.py b/pyaedt/workflows/customize_automation_tab.py new file mode 100644 index 00000000000..5f9eb6dd85e --- /dev/null +++ b/pyaedt/workflows/customize_automation_tab.py @@ -0,0 +1,601 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os +import shutil +import subprocess # nosec +import sys +import xml.etree.ElementTree as ET # nosec + +import defusedxml.minidom + +defusedxml.defuse_stdlib() + +import warnings + +from defusedxml.ElementTree import ParseError +from defusedxml.minidom import parseString + +from pyaedt import is_linux +from pyaedt.generic.general_methods import read_toml +import pyaedt.workflows +import pyaedt.workflows.templates + + +def add_automation_tab( + name, + lib_dir, + icon_file=None, + product="Project", + template="Run PyAEDT Toolkit Script", + overwrite=False, + panel="Panel_PyAEDT_Toolkits", +): + """Add an automation tab in AEDT. + + Parameters + ---------- + name : str + Toolkit name. + lib_dir : str + Path to the library directory. + icon_file : str + Full path to the icon file. The default is the PyAnsys icon. + product : str, optional + Product directory to install the toolkit. + template : str, optional + Script template name to use + overwrite : bool, optional + Whether to overwrite the existing automation tab. The default is ``False``, in + which case is adding new tabs to the existing ones. + panel : str, optional + Panel name. The default is ``"Panel_PyAEDT_Toolkits"``. + + Returns + ------- + str + Automation tab path. + + """ + + product = __tab_map(product) + + tab_config_file_path = os.path.join(lib_dir, product, "TabConfig.xml") + if not os.path.isfile(tab_config_file_path) or overwrite: + root = ET.Element("TabConfig") + else: + try: + tree = ET.parse(tab_config_file_path) # nosec + except ParseError as e: + warnings.warn("Unable to parse %s\nError received = %s" % (tab_config_file_path, str(e))) + return + root = tree.getroot() + + panels = root.findall("./panel") + if panels: + panel_names = [panel_element.attrib["label"] for panel_element in panels] + if panel in panel_names: + # Remove previously existing PyAEDT panel and update with newer one. + panel_element = [panel_element for panel_element in panels if panel_element.attrib["label"] == panel][0] + else: + panel_element = ET.SubElement(root, "panel", label=panel) + else: + panel_element = ET.SubElement(root, "panel", label=panel) + + buttons = panel_element.findall("./button") + if buttons: + button_names = [button.attrib["label"] for button in buttons] + if name in button_names: + # Remove previously existing PyAEDT panel and update with newer one. + b = [button for button in buttons if button.attrib["label"] == name][0] + panel_element.remove(b) + + if not icon_file: + icon_file = os.path.join(os.path.dirname(pyaedt.workflows.__file__), "images", "large", "pyansys.png") + + file_name = os.path.basename(icon_file) + dest_dir = os.path.normpath(os.path.join(lib_dir, product, name, "images", "large")) + dest_file = os.path.normpath(os.path.join(dest_dir, file_name)) + os.makedirs(os.path.dirname(dest_dir), exist_ok=True) + if not os.path.exists(dest_dir): + os.makedirs(dest_dir) + shutil.copy(icon_file, dest_file) + + relative_image_path = os.path.relpath(dest_file, os.path.join(lib_dir, product)) + + ET.SubElement( + panel_element, + "button", + label=name, + isLarge="1", + image=relative_image_path, + script="{}/{}".format(name, template), + ) + + # Backup any existing file if present + if os.path.isfile(tab_config_file_path): + shutil.copy(tab_config_file_path, tab_config_file_path + ".orig") + + create_xml_tab(root, tab_config_file_path) + return tab_config_file_path + + +def remove_automation_tab(name, lib_dir, panel="Panel_PyAEDT_Toolkits"): + """Remove automation tab in AEDT. + + Parameters + ---------- + name : str + Toolkit name. + lib_dir : str + Path to the library directory. + panel : str, optional + Panel name. The default is ``"Panel_PyAEDT_Toolkits"``. + + Returns + ------- + float + Result of the dot product. + + """ + + tab_config_file_path = os.path.join(lib_dir, "TabConfig.xml") + if not os.path.isfile(tab_config_file_path): + return True + try: + tree = ET.parse(tab_config_file_path) # nosec + except ParseError as e: + warnings.warn("Unable to parse %s\nError received = %s" % (tab_config_file_path, str(e))) + return + root = tree.getroot() + + panels = root.findall("./panel") + if panels: + panel_names = [panel_element.attrib["label"] for panel_element in panels] + if panel in panel_names: + # Remove previously existing PyAEDT panel and update with newer one. + panel_element = [panel_element for panel_element in panels if panel.attrib["label"] == panel][0] + else: + panel_element = ET.SubElement(root, "panel", label=panel) + else: + panel_element = ET.SubElement(root, "panel", label=panel) + + buttons = panel_element.findall("./button") + if buttons: + button_names = [button.attrib["label"] for button in buttons] + if name in button_names: + # Remove previously existing PyAEDT panel and update with newer one. + b = [button for button in buttons if button.attrib["label"] == name][0] + panel_element.remove(b) + + create_xml_tab(root, tab_config_file_path) + + +def create_xml_tab(root, output_file): + """Write the XML file to create the automation tab. + + Parameters + ---------- + root : :class:xml.etree.ElementTree + Root element of the main panel. + output_file : str + Full name of the file to save the XML tab. + """ + lines = [line for line in parseString(ET.tostring(root)).toprettyxml(indent=" " * 4).split("\n") if line.strip()] + xml_str = "\n".join(lines) + + with open(output_file, "w") as f: + f.write(xml_str) + + +def remove_xml_tab(toolkit_dir, name, panel="Panel_PyAEDT_Toolkits"): + """Remove a toolkit configuration file.""" + tab_config_file_path = os.path.join(toolkit_dir, "TabConfig.xml") + if not os.path.isfile(tab_config_file_path): + return True + try: + tree = ET.parse(tab_config_file_path) # nosec + except ParseError as e: + warnings.warn("Unable to parse %s\nError received = %s" % (tab_config_file_path, str(e))) + return + root = tree.getroot() + + panels = root.findall("./panel") + if panels: + panel_names = [panel_element.attrib["label"] for panel_element in panels] + if panel in panel_names: + # Remove previously existing PyAEDT panel and update with newer one. + panel_element = [panel_element for panel_element in panels if panel_element.attrib["label"] == panel][0] + else: + panel_element = ET.SubElement(root, "panel", label=panel) + else: + panel_element = ET.SubElement(root, "panel", label=panel) + + buttons = panel_element.findall("./button") + if buttons: + button_names = [button.attrib["label"] for button in buttons] + if name in button_names: + # Remove previously existing PyAEDT panel and update with newer one. + b = [button for button in buttons if button.attrib["label"] == name][0] + panel_element.remove(b) + + create_xml_tab(root, tab_config_file_path) + + +def available_toolkits(): + product_list = [ + "Circuit", + "EMIT", + "HFSS", + "HFSS3DLayout", + "Icepak", + "Maxwell2D", + "Maxwell3D", + "Mechanical", + "Project", + "Q2D", + "Q3D", + "Simplorer", + ] + + product_toolkits = {} + for product in product_list: + toml_file = os.path.join(os.path.dirname(__file__), product.lower(), "toolkits_catalog.toml") + if os.path.isfile(toml_file): + toolkits_catalog = read_toml(toml_file) + product_toolkits[product] = toolkits_catalog + return product_toolkits + + +def add_script_to_menu( + desktop_object, + name, + script_file, + template_file="Run_PyAEDT_Toolkit_Script", + icon_file=None, + product="Project", + copy_to_personal_lib=True, + executable_interpreter=None, + panel="Panel_PyAEDT_Toolkits", +): + """Add a script to the ribbon menu. + + .. note:: + This method is available in AEDT 2023 R2 and later. PyAEDT must be installed + in AEDT to allow this method to run. For more information, see `Installation + `_. + + Parameters + ---------- + desktop_object : :class:pyaedt.desktop.Desktop + Desktop object. + name : str + Name of the toolkit to appear in AEDT. + script_file : str + Full path to the script file. The script will be moved to Personal Lib. + template_file : str + Script template name to use. The default is ``"Run_PyAEDT_Toolkit_Script"``. + icon_file : str, optional + Full path to the icon (a 30x30 pixel PNG file) to add to the UI. + The default is ``None``. + product : str, optional + Product to which the toolkit applies. The default is ``"Project"``, in which case + it applies to all designs. You can also specify a product, such as ``"HFSS"``. + copy_to_personal_lib : bool, optional + Whether to copy the script to Personal Lib or link the original script. Default is ``True``. + executable_interpreter : str, optional + Executable python path. The default is the one current interpreter. + panel : str, optional + Panel name. The default is ``"Panel_PyAEDT_Toolkits"``. + + Returns + ------- + bool + + """ + + if script_file and not os.path.exists(script_file): + desktop_object.logger.error("Script does not exists.") + return False + + toolkit_dir = os.path.join(desktop_object.personallib, "Toolkits") + aedt_version = desktop_object.aedt_version_id + tool_map = __tab_map(product) + tool_dir = os.path.join(toolkit_dir, tool_map, name) + lib_dir = os.path.join(tool_dir, "Lib") + toolkit_rel_lib_dir = os.path.relpath(lib_dir, tool_dir) + if is_linux and aedt_version <= "2023.1": + toolkit_rel_lib_dir = os.path.join("Lib", name) + lib_dir = os.path.join(toolkit_dir, toolkit_rel_lib_dir) + toolkit_rel_lib_dir = "../../" + toolkit_rel_lib_dir + os.makedirs(lib_dir, exist_ok=True) + os.makedirs(tool_dir, exist_ok=True) + dest_script_path = None + if script_file and copy_to_personal_lib: + dest_script_path = os.path.join(lib_dir, os.path.split(script_file)[-1]) + shutil.copy2(script_file, dest_script_path) + + version_agnostic = False + if aedt_version[2:6].replace(".", "") in sys.executable: + executable_version_agnostic = sys.executable.replace(aedt_version[2:6].replace(".", ""), "%s") + version_agnostic = True + else: + executable_version_agnostic = sys.executable + + if executable_interpreter: + executable_version_agnostic = executable_interpreter + + templates_dir = os.path.dirname(pyaedt.workflows.templates.__file__) + + ipython_executable = executable_version_agnostic.replace("python" + __exe(), "ipython" + __exe()) + jupyter_executable = executable_version_agnostic.replace("python" + __exe(), "jupyter" + __exe()) + + with open(os.path.join(templates_dir, template_file + ".py_build"), "r") as build_file: + file_name_dest = template_file.replace("_", " ") + with open(os.path.join(tool_dir, file_name_dest + ".py"), "w") as out_file: + build_file_data = build_file.read() + build_file_data = build_file_data.replace("##TOOLKIT_REL_LIB_DIR##", toolkit_rel_lib_dir) + build_file_data = build_file_data.replace("##IPYTHON_EXE##", ipython_executable) + build_file_data = build_file_data.replace("##PYTHON_EXE##", executable_version_agnostic) + build_file_data = build_file_data.replace("##JUPYTER_EXE##", jupyter_executable) + if dest_script_path: + build_file_data = build_file_data.replace("##PYTHON_SCRIPT##", dest_script_path) + + if not version_agnostic: + build_file_data = build_file_data.replace(" % version", "") + out_file.write(build_file_data) + + if aedt_version >= "2023.2": + add_automation_tab( + name, toolkit_dir, icon_file=icon_file, product=product, template=file_name_dest, panel=panel + ) + desktop_object.logger.info("{} installed".format(name)) + return True + + +def __tab_map(product): # pragma: no cover + """Map exceptions in AEDT applications.""" + if product.lower() == "hfss3dlayout": + return "HFSS3DLayoutDesign" + elif product.lower() == "circuit": + return "CircuitDesign" + elif product.lower() == "q2d": + return "2DExtractor" + elif product.lower() == "q3d": + return "Q3DExtractor" + elif product.lower() == "simplorer": + return "TwinBuilder" + else: + return product + + +def add_custom_toolkit(desktop_object, toolkit_name, wheel_toolkit=None, install=True): # pragma: no cover + """Add toolkit to AEDT Automation Tab. + + Parameters + ---------- + desktop_object : :class:pyaedt.desktop.Desktop + Desktop object. + toolkit_name : str + Name of toolkit to add. + wheel_toolkit : str + Wheelhouse path. + install : bool, optional + Whether to install the toolkit. + + Returns + ------- + bool + """ + toolkits = available_toolkits() + toolkit_info = None + product_name = None + for product in toolkits: + if toolkit_name in toolkits[product]: + toolkit_info = toolkits[product][toolkit_name] + product_name = product + break + if not toolkit_info: + desktop_object.logger.error("Toolkit does not exist.") + return False + + # Set Python version based on AEDT version + python_version = "3.10" if desktop_object.aedt_version_id > "2023.1" else "3.7" + + if not is_linux: + base_venv = os.path.normpath( + os.path.join( + desktop_object.install_path, + "commonfiles", + "CPython", + python_version.replace(".", "_"), + "winx64", + "Release", + "python", + "python.exe", + ) + ) + else: + base_venv = os.path.normpath( + os.path.join( + desktop_object.install_path, + "commonfiles", + "CPython", + python_version.replace(".", "_"), + "linx64", + "Release", + "python", + "runpython", + ) + ) + + def run_command(command): + try: + if is_linux: # pragma: no cover + process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # nosec + else: + process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # nosec + _, stderr = process.communicate() + ret_code = process.returncode + if ret_code != 0: + print("Error occurred:", stderr.decode("utf-8")) + return ret_code + except Exception as e: + print("Exception occurred:", str(e)) + return 1 # Return non-zero exit code for indicating an error + + version = desktop_object.odesktop.GetVersion()[2:6].replace(".", "") + + if not is_linux: + venv_dir = os.path.join(os.environ["APPDATA"], "pyaedt_env_ide", "toolkits_v{}".format(version)) + python_exe = os.path.join(venv_dir, "Scripts", "python.exe") + pip_exe = os.path.join(venv_dir, "Scripts", "pip.exe") + package_dir = os.path.join(venv_dir, "Lib") + else: + venv_dir = os.path.join(os.environ["HOME"], "pyaedt_env_ide", "toolkits_v{}".format(version)) + python_exe = os.path.join(venv_dir, "bin", "python") + pip_exe = os.path.join(venv_dir, "bin", "pip") + package_dir = os.path.join(venv_dir, "lib") + edt_root = os.path.normpath(desktop_object.odesktop.GetExeDir()) + os.environ["ANSYSEM_ROOT{}".format(version)] = edt_root + ld_library_path_dirs_to_add = [ + "{}/commonfiles/CPython/{}/linx64/Release/python/lib".format(edt_root, python_version.replace(".", "_")), + "{}/common/mono/Linux64/lib64".format(edt_root), + "{}".format(edt_root), + ] + if version < "232": + ld_library_path_dirs_to_add.append("{}/Delcross".format(edt_root)) + os.environ["LD_LIBRARY_PATH"] = ":".join(ld_library_path_dirs_to_add) + ":" + os.getenv("LD_LIBRARY_PATH", "") + + # Create virtual environment + + if not os.path.exists(venv_dir): + desktop_object.logger.info("Creating virtual environment") + run_command('"{}" -m venv "{}" --system-site-packages'.format(base_venv, venv_dir)) + desktop_object.logger.info("Virtual environment created.") + + is_installed = False + script_file = None + if os.path.isdir(os.path.normpath(os.path.join(package_dir, toolkit_info["script"]))): + script_file = os.path.normpath(os.path.join(package_dir, toolkit_info["script"])) + else: + for dirpath, dirnames, _ in os.walk(package_dir): + if "site-packages" in dirnames: + script_file = os.path.normpath(os.path.join(dirpath, "site-packages", toolkit_info["script"])) + break + if os.path.isfile(script_file): + is_installed = True + if wheel_toolkit: + wheel_toolkit = os.path.normpath(wheel_toolkit) + desktop_object.logger.info("Installing dependencies") + if install and wheel_toolkit and os.path.exists(wheel_toolkit): + desktop_object.logger.info("Starting offline installation") + if is_installed: + run_command('"{}" uninstall --yes {}'.format(pip_exe, toolkit_info["pip"])) + import zipfile + + unzipped_path = os.path.join( + os.path.dirname(wheel_toolkit), os.path.splitext(os.path.basename(wheel_toolkit))[0] + ) + if os.path.exists(unzipped_path): + shutil.rmtree(unzipped_path, ignore_errors=True) + with zipfile.ZipFile(wheel_toolkit, "r") as zip_ref: + zip_ref.extractall(unzipped_path) + + package_name = toolkit_info["package"] + run_command( + '"{}" install --no-cache-dir --no-index --find-links={} {}'.format(pip_exe, unzipped_path, package_name) + ) + elif install and not is_installed: + # Install the specified package + run_command('"{}" --default-timeout=1000 install {}'.format(pip_exe, toolkit_info["pip"])) + elif not install and is_installed: + # Uninstall toolkit + run_command('"{}" --default-timeout=1000 uninstall -y {}'.format(pip_exe, toolkit_info["package"])) + elif install and is_installed: + # Update toolkit + run_command('"{}" --default-timeout=1000 install {} -U'.format(pip_exe, toolkit_info["pip"])) + else: + desktop_object.logger.info("Incorrect input") + return + toolkit_dir = os.path.join(desktop_object.personallib, "Toolkits") + tool_dir = os.path.join(toolkit_dir, product_name, toolkit_info["name"]) + + script_image = os.path.abspath( + os.path.join(os.path.dirname(pyaedt.workflows.__file__), product_name.lower(), toolkit_info["icon"]) + ) + + if install: + if not os.path.exists(tool_dir): + # Install toolkit inside AEDT + add_script_to_menu( + desktop_object=desktop_object, + name=toolkit_info["name"], + script_file=script_file, + icon_file=script_image, + product=product_name, + template_file="Run_PyAEDT_Toolkit_Script", + copy_to_personal_lib=True, + executable_interpreter=python_exe, + ) + else: + if os.path.exists(tool_dir): + # Install toolkit inside AEDT + remove_script_from_menu( + desktop_object=desktop_object, + name=toolkit_info["name"], + product=product_name, + ) + + +def remove_script_from_menu(desktop_object, name, product="Project"): + """Remove a toolkit script from the menu. + + Parameters + ---------- + desktop_object : :class:pyaedt.desktop.Desktop + Desktop object. + name : str + Name of the toolkit to remove. + product : str, optional + Product to which the toolkit applies. The default is ``"Project"``, in which case + it applies to all designs. You can also specify a product, such as ``"HFSS"``. + + Returns + ------- + bool + """ + product = __tab_map(product) + toolkit_dir = os.path.join(desktop_object.personallib, "Toolkits") + aedt_version = desktop_object.aedt_version_id + tool_dir = os.path.join(toolkit_dir, product, name) + shutil.rmtree(tool_dir, ignore_errors=True) + if aedt_version >= "2023.2": + remove_xml_tab(os.path.join(toolkit_dir, product), name) + desktop_object.logger.info("{} toolkit removed successfully.".format(name)) + return True + + +def __exe(): + if not is_linux: + return ".exe" + return "" diff --git a/pyaedt/workflows/emit/__init__.py b/pyaedt/workflows/emit/__init__.py new file mode 100644 index 00000000000..3bc3c8e5b19 --- /dev/null +++ b/pyaedt/workflows/emit/__init__.py @@ -0,0 +1,21 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/pyaedt/workflows/hfss/__init__.py b/pyaedt/workflows/hfss/__init__.py new file mode 100644 index 00000000000..3bc3c8e5b19 --- /dev/null +++ b/pyaedt/workflows/hfss/__init__.py @@ -0,0 +1,21 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/pyaedt/workflows/hfss/images/large/antenna.png b/pyaedt/workflows/hfss/images/large/antenna.png new file mode 100644 index 00000000000..205f7c54aca Binary files /dev/null and b/pyaedt/workflows/hfss/images/large/antenna.png differ diff --git a/pyaedt/workflows/hfss/toolkits_catalog.toml b/pyaedt/workflows/hfss/toolkits_catalog.toml new file mode 100644 index 00000000000..8626340b3fa --- /dev/null +++ b/pyaedt/workflows/hfss/toolkits_catalog.toml @@ -0,0 +1,7 @@ +[AntennaWizard] +name = "Antenna Wizard" +script = "ansys/aedt/toolkits/antenna/run_toolkit.py" +icon = "images/large/antenna.png" +template = "Run_PyAEDT_Toolkit_Script" +pip = "git+https://github.com/ansys/pyaedt-antenna-toolkit.git" +package = "ansys.aedt.toolkits.antenna" diff --git a/pyaedt/workflows/hfss3dlayout/__init__.py b/pyaedt/workflows/hfss3dlayout/__init__.py new file mode 100644 index 00000000000..3bc3c8e5b19 --- /dev/null +++ b/pyaedt/workflows/hfss3dlayout/__init__.py @@ -0,0 +1,21 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/pyaedt/workflows/hfss3dlayout/export_to_3D.py b/pyaedt/workflows/hfss3dlayout/export_to_3D.py new file mode 100644 index 00000000000..0ab251c87d7 --- /dev/null +++ b/pyaedt/workflows/hfss3dlayout/export_to_3D.py @@ -0,0 +1,111 @@ +import os +from tkinter import Button +from tkinter import Label +from tkinter import RAISED +from tkinter import StringVar +from tkinter import Tk +from tkinter import mainloop +from tkinter import ttk +from tkinter.ttk import Combobox + +import PIL.Image +import PIL.ImageTk + +from pyaedt import Desktop +from pyaedt import Hfss +from pyaedt import Hfss3dLayout +from pyaedt import Icepak +from pyaedt import Maxwell3d +from pyaedt import Q3d +import pyaedt.workflows.hfss3dlayout + +master = Tk() + +master.geometry("400x150") + +master.title("Export to 3D") + +# Load the logo for the main window +icon_path = os.path.join(os.path.dirname(pyaedt.workflows.__file__), "images", "large", "logo.png") +im = PIL.Image.open(icon_path) +photo = PIL.ImageTk.PhotoImage(im) + +# Set the icon for the main window +master.iconphoto(True, photo) + +# Configure style for ttk buttons +style = ttk.Style() +style.configure("Toolbutton.TButton", padding=6, font=("Helvetica", 10)) + +var = StringVar() +label = Label(master, textvariable=var, relief=RAISED) +var.set("Choose an option:") +label.pack(pady=10) +combo = Combobox(master, width=40) # Set the width of the combobox +combo["values"] = ("Export to HFSS", "Export to Q3D", "Export to Maxwell 3D", "Export to Icepak") +combo.current(0) +combo.pack(pady=10) + +combo.focus_set() +choice = "Export to HFSS" + + +def callback(): + global choice + choice = combo.get() + master.destroy() + return True + + +b = Button(master, text="Export", width=40, command=callback) +b.pack(pady=10) + +mainloop() + +suffixes = {"Export to HFSS": "HFSS", "Export to Q3D": "Q3D", "Export to Maxwell 3D": "M3D", "Export to Icepak": "IPK"} + +if "PYAEDT_SCRIPT_PORT" in os.environ and "PYAEDT_SCRIPT_VERSION" in os.environ: + port = os.environ["PYAEDT_SCRIPT_PORT"] + version = os.environ["PYAEDT_SCRIPT_VERSION"] +else: + port = 0 + version = "2024.1" + +with Desktop(new_desktop_session=False, close_on_exit=False, specified_version=version, port=port) as d: + proj = d.active_project() + des = d.active_design() + projname = proj.GetName() + if des.GetDesignType() in ["HFSS 3D Layout Design"]: + desname = des.GetName().split(";")[1] + else: + d.odesktop.AddMessage("", "", 3, "Hfss 3D Layout project is needed.") + d.release_desktop(False, False) + raise Exception("Hfss 3D Layout project is needed.") + h3d = Hfss3dLayout(projectname=projname, designname=desname) + setup = h3d.create_setup() + suffix = suffixes[choice] + + if choice == "Export to Q3D": + setup.export_to_q3d(h3d.project_file[:-5] + f"_{suffix}.aedt", keep_net_name=True) + else: + setup.export_to_hfss(h3d.project_file[:-5] + f"_{suffix}.aedt", keep_net_name=True) + h3d.delete_setup(setup.name) + if choice == "Export to Q3D": + app = Q3d(projectname=h3d.project_file[:-5] + f"_{suffix}.aedt") + else: + app = Hfss(projectname=h3d.project_file[:-5] + f"_{suffix}.aedt") + app2 = None + if choice == "Export to Maxwell 3D": + app2 = Maxwell3d(projectname=app.project_name) + elif choice == "Export to Icepak": + app2 = Icepak(projectname=app.project_name) + if app2: + app2.copy_solid_bodies_from( + app, + no_vacuum=False, + no_pec=False, + include_sheets=True, + ) + app2.delete_design(app.design_name) + app2.save_project() + d.logger.info("Project generated correctly.") diff --git a/pyaedt/workflows/hfss3dlayout/images/large/cad3d.png b/pyaedt/workflows/hfss3dlayout/images/large/cad3d.png new file mode 100644 index 00000000000..13e42309060 Binary files /dev/null and b/pyaedt/workflows/hfss3dlayout/images/large/cad3d.png differ diff --git a/pyaedt/workflows/hfss3dlayout/toolkits_catalog.toml b/pyaedt/workflows/hfss3dlayout/toolkits_catalog.toml new file mode 100644 index 00000000000..1149935ea8c --- /dev/null +++ b/pyaedt/workflows/hfss3dlayout/toolkits_catalog.toml @@ -0,0 +1,6 @@ +[Export3D] +name = "Export to 3D" +script = "export_to_3D.py" +icon = "images/large/cad3d.png" +template = "Run_PyAEDT_Script" +pip = "" diff --git a/pyaedt/workflows/icepak/__init__.py b/pyaedt/workflows/icepak/__init__.py new file mode 100644 index 00000000000..3bc3c8e5b19 --- /dev/null +++ b/pyaedt/workflows/icepak/__init__.py @@ -0,0 +1,21 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/pyaedt/workflows/images/large/logo.png b/pyaedt/workflows/images/large/logo.png new file mode 100644 index 00000000000..554dc38242d Binary files /dev/null and b/pyaedt/workflows/images/large/logo.png differ diff --git a/pyaedt/misc/images/large/pyansys.png b/pyaedt/workflows/images/large/pyansys.png similarity index 100% rename from pyaedt/misc/images/large/pyansys.png rename to pyaedt/workflows/images/large/pyansys.png diff --git a/pyaedt/workflows/installer/__init__.py b/pyaedt/workflows/installer/__init__.py new file mode 100644 index 00000000000..3bc3c8e5b19 --- /dev/null +++ b/pyaedt/workflows/installer/__init__.py @@ -0,0 +1,21 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/pyaedt/misc/console_setup.py b/pyaedt/workflows/installer/console_setup.py similarity index 97% rename from pyaedt/misc/console_setup.py rename to pyaedt/workflows/installer/console_setup.py index d044e63a1a5..b32f3273fa1 100644 --- a/pyaedt/misc/console_setup.py +++ b/pyaedt/workflows/installer/console_setup.py @@ -20,7 +20,7 @@ # to PyAEDT is created in the personal library. console_setup_dir = os.path.dirname(__file__) if "PersonalLib" in console_setup_dir: - sys.path.append(os.path.join(console_setup_dir, "..", "..", "..")) + sys.path.append(os.path.join(console_setup_dir, "../..", "..", "..")) import pyaedt diff --git a/pyaedt/workflows/installer/create_report.py b/pyaedt/workflows/installer/create_report.py new file mode 100644 index 00000000000..55a0ad348cd --- /dev/null +++ b/pyaedt/workflows/installer/create_report.py @@ -0,0 +1,39 @@ +# Generate pdf report +# ~~~~~~~~~~~~~~~~~~~ +# Generate a pdf report with output of simultion. +import os + +from pyaedt import Desktop +from pyaedt import get_pyaedt_app +from pyaedt.generic.pdf import AnsysReport + +if "PYAEDT_SCRIPT_PORT" in os.environ and "PYAEDT_SCRIPT_VERSION" in os.environ: + port = os.environ["PYAEDT_SCRIPT_PORT"] + version = os.environ["PYAEDT_SCRIPT_VERSION"] +else: + port = 0 + version = "2024.1" + +with Desktop(new_desktop_session=False, close_on_exit=False, specified_version=version, port=port) as d: + + proj = d.active_project() + des = d.active_design() + projname = proj.GetName() + desname = des.GetName() + if des.GetDesignType() in ["HFSS 3D Layout Design", "Circuit Design"]: + desname = None + app = get_pyaedt_app(projname, desname) + + report = AnsysReport(version=d.aedt_version_id, design_name=app.design_name, project_name=app.project_name) + report.create() + report.add_section() + report.add_chapter(f"{app.solution_type} Results") + report.add_sub_chapter("Plots") + report.add_text("This section contains all reports results.") + for plot in app.post.plots: + app.post.export_report_to_jpg(app.working_directory, plot.plot_name) + report.add_image(os.path.join(app.working_directory, plot.plot_name + ".jpg"), plot.plot_name) + report.add_page_break() + report.add_toc() + out = report.save_pdf(app.working_directory, "AEDT_Results.pdf") + d.odesktop.AddMessage("", "", 0, f"Report Generated. {out}") diff --git a/pyaedt/workflows/installer/images/large/console.png b/pyaedt/workflows/installer/images/large/console.png new file mode 100644 index 00000000000..5d22ff8a4c9 Binary files /dev/null and b/pyaedt/workflows/installer/images/large/console.png differ diff --git a/pyaedt/workflows/installer/images/large/jupyter.png b/pyaedt/workflows/installer/images/large/jupyter.png new file mode 100644 index 00000000000..2e3be441a11 Binary files /dev/null and b/pyaedt/workflows/installer/images/large/jupyter.png differ diff --git a/pyaedt/workflows/installer/images/large/run_script.png b/pyaedt/workflows/installer/images/large/run_script.png new file mode 100644 index 00000000000..993fe8b3f69 Binary files /dev/null and b/pyaedt/workflows/installer/images/large/run_script.png differ diff --git a/pyaedt/workflows/installer/images/large/toolkit_manager.png b/pyaedt/workflows/installer/images/large/toolkit_manager.png new file mode 100644 index 00000000000..8ee1525df77 Binary files /dev/null and b/pyaedt/workflows/installer/images/large/toolkit_manager.png differ diff --git a/pyaedt/misc/jupyter_template.ipynb b/pyaedt/workflows/installer/jupyter_template.ipynb similarity index 100% rename from pyaedt/misc/jupyter_template.ipynb rename to pyaedt/workflows/installer/jupyter_template.ipynb diff --git a/pyaedt/workflows/installer/pyaedt_installer.py b/pyaedt/workflows/installer/pyaedt_installer.py new file mode 100644 index 00000000000..eae13eeb397 --- /dev/null +++ b/pyaedt/workflows/installer/pyaedt_installer.py @@ -0,0 +1,124 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Methods to add PyAEDT in AEDT.""" + +import os + +from pyaedt import is_windows +from pyaedt import pyaedt_path +from pyaedt.generic.general_methods import read_toml +from pyaedt.workflows import customize_automation_tab + + +def add_pyaedt_to_aedt( + aedt_version="2024.1", + student_version=False, + new_desktop_session=False, + non_graphical=False, +): + """Add PyAEDT tabs in AEDT. + + Parameters + ---------- + aedt_version : str, optional + AEDT release. + student_version : bool, optional + Whether to use the student version of AEDT. The default + is ``False``. + new_desktop_session : bool, optional + Whether to create a new AEDT session. The default + is ``False`` + non_graphical : bool, optional + Whether to run AEDT in non-graphical mode. The default + is ``False``. + """ + + from pyaedt import Desktop + from pyaedt.generic.general_methods import grpc_active_sessions + from pyaedt.generic.settings import settings + + sessions = grpc_active_sessions(aedt_version, student_version) + close_on_exit = True + if not sessions: + if not new_desktop_session: + print("Launching a new AEDT desktop session.") + new_desktop_session = True + else: + close_on_exit = False + settings.use_grpc_api = True + with Desktop( + specified_version=aedt_version, + non_graphical=non_graphical, + new_desktop_session=new_desktop_session, + student_version=student_version, + close_on_exit=close_on_exit, + ) as d: + personal_lib_dir = d.odesktop.GetPersonalLibDirectory() + pers1 = os.path.join(personal_lib_dir, "pyaedt") + pid = d.odesktop.GetProcessID() + # Linking pyaedt in PersonalLib for IronPython compatibility. + if os.path.exists(pers1): + d.logger.info("PersonalLib already mapped.") + else: + if is_windows: + os.system('mklink /D "{}" "{}"'.format(pers1, pyaedt_path)) + else: + os.system('ln -s "{}" "{}"'.format(pyaedt_path, pers1)) + + __add_pyaedt_tabs(d) + + if pid and new_desktop_session: + try: + os.kill(pid, 9) + except Exception: # pragma: no cover + return False + + +def __add_pyaedt_tabs(desktop_object): + """Add PyAEDT tabs in AEDT.""" + + pyaedt_tabs = ["Console", "Jupyter", "Run_Script", "ToolkitManager"] + + toolkits_catalog = read_toml(os.path.join(os.path.dirname(__file__), "toolkits_catalog.toml")) + + project_workflows_dir = os.path.dirname(__file__) + + for toolkit in pyaedt_tabs: + if toolkit in toolkits_catalog.keys(): + toolkit_info = toolkits_catalog[toolkit] + script_path = None + if toolkit_info["script"]: + script_path = os.path.join(project_workflows_dir, toolkit_info["script"]) + icon_file = os.path.join(project_workflows_dir, "images", "large", toolkit_info["icon"]) + template_name = toolkit_info["template"] + customize_automation_tab.add_script_to_menu( + desktop_object, + toolkit_info["name"], + script_path, + template_name, + icon_file=icon_file, + product="Project", + copy_to_personal_lib=True, + executable_interpreter=None, + panel="Panel_PyAEDT_Installer", + ) diff --git a/pyaedt/workflows/installer/toolkit_manager.py b/pyaedt/workflows/installer/toolkit_manager.py new file mode 100644 index 00000000000..6ec606b8228 --- /dev/null +++ b/pyaedt/workflows/installer/toolkit_manager.py @@ -0,0 +1,356 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os +import tkinter as tk +from tkinter import ttk + +import PIL.Image +import PIL.ImageTk + +from pyaedt import Desktop +from pyaedt import is_windows +import pyaedt.workflows +from pyaedt.workflows.customize_automation_tab import add_custom_toolkit +from pyaedt.workflows.customize_automation_tab import add_script_to_menu +from pyaedt.workflows.customize_automation_tab import available_toolkits +from pyaedt.workflows.customize_automation_tab import remove_script_from_menu + +env_vars = ["PYAEDT_SCRIPT_VERSION", "PYAEDT_SCRIPT_PORT", "PYAEDT_STUDENT_VERSION"] +if all(var in os.environ for var in env_vars): + version = os.environ["PYAEDT_SCRIPT_VERSION"] + port = int(os.environ["PYAEDT_SCRIPT_PORT"]) + student_version = False if os.environ["PYAEDT_STUDENT_VERSION"] == "False" else True +else: + version = "241" + port = 0 + student_version = False + +if is_windows: + venv_dir = os.path.join(os.environ["APPDATA"], "pyaedt_env_ide", "toolkits_v{}".format(version)) + python_exe = os.path.join(venv_dir, "Scripts", "python.exe") + package_dir = os.path.join(venv_dir, "Lib", "site-packages") + +else: + venv_dir = os.path.join(os.environ["HOME"], "pyaedt_env_ide", "toolkits_v{}".format(version)) + python_exe = os.path.join(venv_dir, "bin", "python") + package_dir = os.path.join(venv_dir, "lib", "site-packages") + + +def create_toolkit_page(frame, window_name, internal_toolkits): + """Create page to display toolkit on.""" + # Available toolkits + toolkits = ["Custom"] + internal_toolkits + + max_length = max(len(item) for item in toolkits) + 1 + + # Pip or Offline radio options + installation_option_action = tk.StringVar(value="Offline") + pip_installation_radio = tk.Radiobutton(frame, text="Pip", variable=installation_option_action, value="Pip") + offline_installation_radio = tk.Radiobutton( + frame, text="Offline", variable=installation_option_action, value="Offline" + ) + pip_installation_radio.grid(row=1, column=0, padx=5, pady=5) + offline_installation_radio.grid(row=1, column=1, padx=5, pady=5) + + # Combobox with available toolkit options + toolkits_combo_label = tk.Label(frame, text="Toolkit:", width=max_length) + toolkits_combo_label.grid(row=2, column=0, padx=5, pady=5) + + toolkits_combo = ttk.Combobox( + frame, values=list(filter(lambda x: x != "", toolkits)), state="readonly", width=max_length + ) + toolkits_combo.set("Custom") + toolkits_combo.grid(row=2, column=1, padx=5, pady=5) + + # Create entry box for directory path + input_file_label = tk.Label(frame, text="Enter script path:") + input_file_label.grid(row=3, column=0, padx=5, pady=5) + input_file = tk.Entry(frame) + input_file.grid(row=3, column=1, padx=5, pady=5) + + toolkit_name_label = tk.Label(frame, text="Enter toolkit name:") + toolkit_name_label.grid(row=4, column=0, padx=5, pady=5) + toolkit_name = tk.Entry(frame) + toolkit_name.grid(row=4, column=1, padx=5, pady=5) + + # Install button + install_button = tk.Button(frame, text="Install", bg="green", fg="white", padx=20, pady=5) + install_button.grid(row=5, column=0, padx=5, pady=5, sticky="nsew") + uninstall_button = tk.Button(frame, text="Uninstall", bg="red", fg="white", padx=20, pady=5) + uninstall_button.grid(row=5, column=1, padx=5, pady=5, sticky="nsew") + + def update_page(event=None): + selected_toolkit = toolkits_combo.get() + + toolkits = available_toolkits() + selected_toolkit_info = {} + if window_name in toolkits and selected_toolkit in toolkits[window_name]: + selected_toolkit_info = toolkits[window_name][selected_toolkit] + + if selected_toolkit == "Custom" or not selected_toolkit_info.get("pip"): + install_button.config(text="Install") + uninstall_button.config(state="normal") + else: + if is_toolkit_installed(selected_toolkit, window_name): + install_button.config(text="Update") + uninstall_button.config(state="normal") + else: + install_button.config(text="Install") + uninstall_button.config(state="disabled") + + if ( + installation_option_action.get() == "Pip" + and selected_toolkit != "Custom" + and selected_toolkit_info.get("pip") + ): + toolkit_name.config(state="disabled") + input_file_label.config(text="Enter wheelhouse path:") + input_file.config(state="disabled") + elif (installation_option_action.get() == "Pip" and selected_toolkit == "Custom") or ( + installation_option_action.get() == "Offline" and selected_toolkit == "Custom" + ): + toolkit_name.config(state="normal") + input_file_label.config(text="Enter script path:") + input_file.config(state="normal") + installation_option_action.set("Offline") + elif not selected_toolkit_info.get("pip") and selected_toolkit != "Custom": + input_file.config(state="disabled") + toolkit_name.config(state="disabled") + else: + toolkit_name.config(state="disabled") + input_file_label.config(text="Enter wheelhouse path:") + input_file.config(state="normal") + + toolkits_combo.bind("<>", update_page) + + update_page() + + return install_button, uninstall_button, input_file, toolkits_combo, toolkit_name + + +def is_toolkit_installed(toolkit_name, window_name): + """Check if toolkit is installed.""" + if toolkit_name == "Custom": + return False + toolkits = available_toolkits() + script_file = os.path.normpath(os.path.join(package_dir, toolkits[window_name][toolkit_name]["script"])) + if os.path.isfile(script_file): + return True + else: + lib_dir = os.path.dirname(package_dir) + for dirpath, dirnames, _ in os.walk(lib_dir): + if "site-packages" in dirnames: + script_file = os.path.normpath( + os.path.join(dirpath, "site-packages", toolkits[window_name][toolkit_name]["script"]) + ) + if os.path.isfile(script_file): + return True + break + return False + + +def open_window(window, window_name, internal_toolkits): + """Open a window.""" + if not hasattr(window, "opened"): + window.opened = True + window.title(window_name) + install_button, uninstall_button, input_file, toolkits_combo, toolkit_name = create_toolkit_page( + window, window_name, internal_toolkits + ) + root.minsize(500, 250) + return install_button, uninstall_button, input_file, toolkits_combo, toolkit_name + else: + window.deiconify() + + +def __get_command_function( + is_install, toolkit_level, input_file, toolkits_combo, toolkit_name, install_button, uninstall_button +): + return lambda: button_is_clicked( + is_install, toolkit_level, input_file, toolkits_combo, toolkit_name, install_button, uninstall_button + ) + + +def toolkit_window(toolkit_level="Project"): + """Create interactive toolkit window.""" + toolkit_window_var = tk.Toplevel(root) + + toolkits = available_toolkits() + + if toolkit_level not in toolkits: + install_button, uninstall_button, input_file, toolkits_combo, toolkit_name = open_window( + toolkit_window_var, toolkit_level, [] + ) + else: + install_button, uninstall_button, input_file, toolkits_combo, toolkit_name = open_window( + toolkit_window_var, toolkit_level, list(toolkits[toolkit_level].keys()) + ) + toolkit_window_var.minsize(250, 150) + + install_command = __get_command_function( + True, toolkit_level, input_file, toolkits_combo, toolkit_name, install_button, uninstall_button + ) + uninstall_command = __get_command_function( + False, toolkit_level, input_file, toolkits_combo, toolkit_name, install_button, uninstall_button + ) + + install_button.configure(command=install_command) + uninstall_button.configure(command=uninstall_command) + + +def button_is_clicked( + install_action, toolkit_level, input_file, combo_toolkits, toolkit_name, install_button, uninstall_button +): + """Set up a button for installing and uninstalling the toolkit.""" + file = input_file.get() + selected_toolkit_name = combo_toolkits.get() + name = toolkit_name.get() + + desktop = Desktop( + specified_version=version, + port=port, + new_desktop_session=False, + non_graphical=False, + close_on_exit=False, + student_version=student_version, + ) + + desktop.odesktop.CloseAllWindows() + + toolkits = available_toolkits() + selected_toolkit_info = {} + icon = None + if toolkit_level in toolkits and selected_toolkit_name in toolkits[toolkit_level]: + selected_toolkit_info = toolkits[toolkit_level][selected_toolkit_name] + if not selected_toolkit_info.get("pip"): + product_path = os.path.join(os.path.dirname(pyaedt.workflows.__file__), toolkit_level.lower()) + file = os.path.abspath(os.path.join(product_path, selected_toolkit_info.get("script"))) + name = selected_toolkit_info.get("name") + icon = os.path.abspath(os.path.join(product_path, selected_toolkit_info.get("icon"))) + + if selected_toolkit_name != "Custom" and selected_toolkit_info.get("pip"): + if is_toolkit_installed(selected_toolkit_name, toolkit_level) and install_action: + desktop.logger.info("Updating {}".format(selected_toolkit_name)) + add_custom_toolkit(desktop, selected_toolkit_name, file) + install_button.config(text="Update") + uninstall_button.config(state="normal") + desktop.logger.info("{} updated".format(selected_toolkit_name)) + elif install_action: + desktop.logger.info("Installing {}".format(selected_toolkit_name)) + add_custom_toolkit(desktop, selected_toolkit_name, file) + install_button.config(text="Update") + uninstall_button.config(state="normal") + elif is_toolkit_installed(selected_toolkit_name, toolkit_level) and not install_action: + desktop.logger.info("Uninstalling {}".format(selected_toolkit_name)) + add_custom_toolkit(desktop, selected_toolkit_name, install=False) + install_button.config(text="Install") + uninstall_button.config(state="disabled") + desktop.logger.info("{} uninstalled".format(selected_toolkit_name)) + else: + desktop.logger.info("{} not installed".format(selected_toolkit_name)) + + else: + if install_action: + desktop.logger.info("Install {}".format(name)) + if is_windows: + pyaedt_venv_dir = os.path.join(os.environ["APPDATA"], "pyaedt_env_ide", "v{}".format(version)) + executable_interpreter = os.path.join(pyaedt_venv_dir, "Scripts", "python.exe") + else: + pyaedt_venv_dir = os.path.join(os.environ["HOME"], "pyaedt_env_ide", "v{}".format(version)) + executable_interpreter = os.path.join(pyaedt_venv_dir, "bin", "python") + + if os.path.isfile(executable_interpreter): + add_script_to_menu( + desktop_object=desktop, + name=name, + script_file=file, + product=toolkit_level, + icon_file=icon, + executable_interpreter=executable_interpreter, + ) + else: + desktop.logger.info("PyAEDT environment is not installed.") + else: + desktop.logger.info("Uninstall {}.".format(name)) + remove_script_from_menu(desktop_object=desktop, name=name, product=toolkit_level) + + desktop.odesktop.CloseAllWindows() + desktop.odesktop.RefreshToolkitUI() + desktop.release_desktop(False, False) + + +root = tk.Tk() +root.title("AEDT Toolkit Manager") + +# Load the logo for the main window +icon_path = os.path.join(os.path.dirname(pyaedt.workflows.__file__), "images", "large", "logo.png") +im = PIL.Image.open(icon_path) +photo = PIL.ImageTk.PhotoImage(im) + +# Set the icon for the main window +root.iconphoto(True, photo) + +# Configure style for ttk buttons +style = ttk.Style() +style.configure("Toolbutton.TButton", padding=6, font=("Helvetica", 10)) + +toolkit_levels = [ + "Project", + "", + "", + "", + "HFSS", + "Maxwell3D", + "Icepak", + "Q3D", + "Maxwell2D", + "Q2D", + "HFSS3DLayout", + "Mechanical", + "Circuit", + "EMIT", + "Simplorer", + "", +] + +window_width, window_height = 500, 250 +screen_width = root.winfo_screenwidth() +screen_height = root.winfo_screenheight() +x_position = (screen_width - window_width) // 2 +y_position = (screen_height - window_height) // 2 + +root.geometry(f"{window_width}x{window_height}+{x_position}+{y_position}") + +# Create buttons in a 4x4 grid, centered +for i, level in enumerate(toolkit_levels): + row_num = i // 4 + col_num = i % 4 + if level: + toolkit_button = ttk.Button( + root, text=level, command=lambda l=level: toolkit_window(l), style="Toolbutton.TButton" + ) + toolkit_button.grid(row=row_num, column=col_num, padx=10, pady=10) + +root.minsize(window_width, window_height) + +root.mainloop() diff --git a/pyaedt/workflows/installer/toolkits_catalog.toml b/pyaedt/workflows/installer/toolkits_catalog.toml new file mode 100644 index 00000000000..a5baf22c440 --- /dev/null +++ b/pyaedt/workflows/installer/toolkits_catalog.toml @@ -0,0 +1,23 @@ +[Console] +name = "PyAEDT Console" +script = "console_setup.py" +icon = "console.png" +template = "PyAEDT_Console" + +[Jupyter] +name = "Jupyter Notebook" +script = "jupyter_template.ipynb" +icon = "jupyter.png" +template = "Jupyter" + +[Run_Script] +name = "Run PyAEDT Script" +script = "" +icon = "run_script.png" +template = "Run_PyAEDT_Script" + +[ToolkitManager] +name = "Toolkit Manager" +script = "toolkit_manager.py" +icon = "toolkit_manager.png" +template = "Run_Toolkit_Manager" diff --git a/pyaedt/workflows/maxwell2d/__init__.py b/pyaedt/workflows/maxwell2d/__init__.py new file mode 100644 index 00000000000..3bc3c8e5b19 --- /dev/null +++ b/pyaedt/workflows/maxwell2d/__init__.py @@ -0,0 +1,21 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/pyaedt/workflows/maxwell3d/__init__.py b/pyaedt/workflows/maxwell3d/__init__.py new file mode 100644 index 00000000000..3bc3c8e5b19 --- /dev/null +++ b/pyaedt/workflows/maxwell3d/__init__.py @@ -0,0 +1,21 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/pyaedt/workflows/maxwell3d/images/large/magnet_segmentation.png b/pyaedt/workflows/maxwell3d/images/large/magnet_segmentation.png new file mode 100644 index 00000000000..2732690092d Binary files /dev/null and b/pyaedt/workflows/maxwell3d/images/large/magnet_segmentation.png differ diff --git a/pyaedt/workflows/maxwell3d/toolkits_catalog.toml b/pyaedt/workflows/maxwell3d/toolkits_catalog.toml new file mode 100644 index 00000000000..1b403d6e8df --- /dev/null +++ b/pyaedt/workflows/maxwell3d/toolkits_catalog.toml @@ -0,0 +1,7 @@ +[MagnetSegmentationWizard] +name = "Magnet Segmentation Wizard" +script = "ansys/aedt/toolkits/magnet_segmentation/run_toolkit.py" +icon = "images/large/magnet_segmentation.png" +template = "Run_PyAEDT_Toolkit_Script" +pip = "ansys-magnet-segmentation-toolkit" +package = "ansys-magnet-segmentation-toolkit" diff --git a/pyaedt/workflows/mechanical/__init__.py b/pyaedt/workflows/mechanical/__init__.py new file mode 100644 index 00000000000..3bc3c8e5b19 --- /dev/null +++ b/pyaedt/workflows/mechanical/__init__.py @@ -0,0 +1,21 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/pyaedt/workflows/project/__init__.py b/pyaedt/workflows/project/__init__.py new file mode 100644 index 00000000000..3bc3c8e5b19 --- /dev/null +++ b/pyaedt/workflows/project/__init__.py @@ -0,0 +1,21 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/pyaedt/workflows/project/create_report.py b/pyaedt/workflows/project/create_report.py new file mode 100644 index 00000000000..91c99945671 --- /dev/null +++ b/pyaedt/workflows/project/create_report.py @@ -0,0 +1,39 @@ +# Generate pdf report +# ~~~~~~~~~~~~~~~~~~~ +# Generate a pdf report with output of simulation. +import os + +from pyaedt import Desktop +from pyaedt import get_pyaedt_app +from pyaedt.generic.pdf import AnsysReport + +if "PYAEDT_SCRIPT_PORT" in os.environ and "PYAEDT_SCRIPT_VERSION" in os.environ: + port = os.environ["PYAEDT_SCRIPT_PORT"] + version = os.environ["PYAEDT_SCRIPT_VERSION"] +else: + port = 0 + version = "2024.1" + +with Desktop(new_desktop_session=False, close_on_exit=False, specified_version=version, port=port) as d: + + proj = d.active_project() + des = d.active_design() + projname = proj.GetName() + desname = des.GetName() + if des.GetDesignType() in ["HFSS 3D Layout Design", "Circuit Design"]: + desname = None + app = get_pyaedt_app(projname, desname) + + report = AnsysReport(version=d.aedt_version_id, design_name=app.design_name, project_name=app.project_name) + report.create() + report.add_section() + report.add_chapter(f"{app.solution_type} Results") + report.add_sub_chapter("Plots") + report.add_text("This section contains all reports results.") + for plot in app.post.plots: + app.post.export_report_to_jpg(app.working_directory, plot.plot_name) + report.add_image(os.path.join(app.working_directory, plot.plot_name + ".jpg"), plot.plot_name) + report.add_page_break() + report.add_toc() + out = report.save_pdf(app.working_directory, "AEDT_Results.pdf") + d.odesktop.AddMessage("", "", 0, f"Report Generated. {out}") diff --git a/pyaedt/workflows/project/images/large/cad3d.png b/pyaedt/workflows/project/images/large/cad3d.png new file mode 100644 index 00000000000..13e42309060 Binary files /dev/null and b/pyaedt/workflows/project/images/large/cad3d.png differ diff --git a/pyaedt/workflows/project/images/large/pdf.png b/pyaedt/workflows/project/images/large/pdf.png new file mode 100644 index 00000000000..d2647a81bbe Binary files /dev/null and b/pyaedt/workflows/project/images/large/pdf.png differ diff --git a/pyaedt/workflows/project/import_nastran.py b/pyaedt/workflows/project/import_nastran.py new file mode 100644 index 00000000000..a646a5d65dd --- /dev/null +++ b/pyaedt/workflows/project/import_nastran.py @@ -0,0 +1,39 @@ +import os.path + +# import filedialog module +from tkinter import filedialog + +from pyaedt import Desktop +from pyaedt import get_pyaedt_app + + +# Function for opening the +# file explorer window +def browseFiles(): + filename = filedialog.askopenfilename( + initialdir="/", title="Select a File", filetypes=(("Nastran files", "*.nas*"), ("all files", "*.*")) + ) + + # Change label contents + return filename + + +nas_input = browseFiles() +if "PYAEDT_SCRIPT_PORT" in os.environ and "PYAEDT_SCRIPT_VERSION" in os.environ: + port = os.environ["PYAEDT_SCRIPT_PORT"] + version = os.environ["PYAEDT_SCRIPT_VERSION"] +else: + port = 0 + version = "2024.2" +if os.path.exists(nas_input): + with Desktop(new_desktop_session=False, close_on_exit=False, specified_version=version, port=port) as d: + proj = d.active_project() + des = d.active_design() + projname = proj.GetName() + desname = des.GetName() + app = get_pyaedt_app(projname, desname) + app.modeler.import_nastran(nas_input) + d.logger.info("Nastran imported correctly.") +else: + with Desktop(new_desktop_session=False, close_on_exit=False, specified_version=version, port=port) as d: + d.odesktop.AddMessage("", "", 3, "Wrong file selected. Select a .nas file") diff --git a/pyaedt/workflows/project/toolkits_catalog.toml b/pyaedt/workflows/project/toolkits_catalog.toml new file mode 100644 index 00000000000..76270600ced --- /dev/null +++ b/pyaedt/workflows/project/toolkits_catalog.toml @@ -0,0 +1,13 @@ +[GenerateReport] +name = "Generate report" +script = "create_report.py" +icon = "images/large/pdf.png" +template = "Run_PyAEDT_Script" +pip = "" + +[GenerateReport] +name = "Import Nastran" +script = "import_nastran.py" +icon = "images/large/cad3d.png" +template = "Run_PyAEDT_Script" +pip = "" diff --git a/pyaedt/workflows/q2d/__init__.py b/pyaedt/workflows/q2d/__init__.py new file mode 100644 index 00000000000..3bc3c8e5b19 --- /dev/null +++ b/pyaedt/workflows/q2d/__init__.py @@ -0,0 +1,21 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/pyaedt/workflows/q3d/__init__.py b/pyaedt/workflows/q3d/__init__.py new file mode 100644 index 00000000000..3bc3c8e5b19 --- /dev/null +++ b/pyaedt/workflows/q3d/__init__.py @@ -0,0 +1,21 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/pyaedt/workflows/simplorer/__init__.py b/pyaedt/workflows/simplorer/__init__.py new file mode 100644 index 00000000000..3bc3c8e5b19 --- /dev/null +++ b/pyaedt/workflows/simplorer/__init__.py @@ -0,0 +1,21 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/pyaedt/misc/Jupyter.py_build b/pyaedt/workflows/templates/Jupyter.py_build similarity index 100% rename from pyaedt/misc/Jupyter.py_build rename to pyaedt/workflows/templates/Jupyter.py_build diff --git a/pyaedt/misc/Console.py_build b/pyaedt/workflows/templates/PyAEDT_Console.py_build similarity index 97% rename from pyaedt/misc/Console.py_build rename to pyaedt/workflows/templates/PyAEDT_Console.py_build index 60e5b261d74..fb80f53afff 100644 --- a/pyaedt/misc/Console.py_build +++ b/pyaedt/workflows/templates/PyAEDT_Console.py_build @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -* * * This script is meant to run in IronPython within the Ansys Electronics Desktop. * * * +* * * This script is meant to run in IronPython within AEDT. * * * It looks for a reference to a Python interpreter in the ``python_interpreter.bat`` file. diff --git a/pyaedt/misc/Run_PyAEDT_Script.py_build b/pyaedt/workflows/templates/Run_PyAEDT_Script.py_build similarity index 97% rename from pyaedt/misc/Run_PyAEDT_Script.py_build rename to pyaedt/workflows/templates/Run_PyAEDT_Script.py_build index 85bfca277d2..9913717274e 100644 --- a/pyaedt/misc/Run_PyAEDT_Script.py_build +++ b/pyaedt/workflows/templates/Run_PyAEDT_Script.py_build @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -* * * This script is meant to run in IronPython within the Ansys Electronics Desktop. * * * +* * * This script is meant to run in IronPython within AEDT. * * * The script provides for choosing the Python script to execute. It looks for a reference to a Python interpreter in the ``python_interpreter.bat`` file. diff --git a/pyaedt/misc/Run_PyAEDT_Toolkit_Script.py_build b/pyaedt/workflows/templates/Run_PyAEDT_Toolkit_Script.py_build similarity index 92% rename from pyaedt/misc/Run_PyAEDT_Toolkit_Script.py_build rename to pyaedt/workflows/templates/Run_PyAEDT_Toolkit_Script.py_build index 119e1ecced6..f25b98dd282 100644 --- a/pyaedt/misc/Run_PyAEDT_Toolkit_Script.py_build +++ b/pyaedt/workflows/templates/Run_PyAEDT_Toolkit_Script.py_build @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -* * * This script is meant to run in IronPython within the Ansys Electronics Desktop. * * * +* * * This script is meant to run in IronPython within AEDT. * * * The script provides for choosing the Python script to execute. It looks for a reference to a Python interpreter in the ``python_interpreter.bat`` file. @@ -32,7 +32,6 @@ def main(): try: oDesktop.AddMessage("", "", 0, "Toolkit launched. Please wait.") # launch file - version = oDesktop.GetVersion()[2:6].replace(".", "") python_exe = r"##PYTHON_EXE##" % version pyaedt_script = r"##PYTHON_SCRIPT##" check_file(python_exe) @@ -78,7 +77,7 @@ def main(): def check_file(file_path): if not os.path.isfile(file_path): - show_error('"{}" does not exist. Please click on the "Install PyAEDT" button in the Automation ribbon.'.format( + show_error('"{}" does not exist.'.format( file_path)) diff --git a/pyaedt/workflows/templates/Run_Toolkit_Manager.py_build b/pyaedt/workflows/templates/Run_Toolkit_Manager.py_build new file mode 100644 index 00000000000..4bebbdba872 --- /dev/null +++ b/pyaedt/workflows/templates/Run_Toolkit_Manager.py_build @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +""" +* * * This script is meant to run in IronPython within AEDT. * * * +The script provides for choosing the Python script to execute. + +It looks for a reference to a Python interpreter in the ``python_interpreter.bat`` file. + +It then uses this Python interpreter to execute the script. +See the declaration of the command variable to see the order in which arguments are passed to the script. + +The commands allow the launched script to still reference the project and design that was active when the script +was launched as well as the AEDT instance that has them open. + +""" +import os +import sys + +from System.Windows.Forms import MessageBox +from System.Windows.Forms import MessageBoxButtons +from System.Windows.Forms import MessageBoxIcon +from System.Windows.Forms import OpenFileDialog + +is_linux = os.name == "posix" +script_name = os.path.splitext(os.path.basename(__file__))[0] + +if is_linux: + import subprocessdotnet as subprocess +else: + import subprocess + + +def main(): + try: + version = oDesktop.GetVersion()[2:6].replace(".", "") + # launch toolkit manager + python_exe = r"##PYTHON_EXE##" % version + current_dir = os.path.dirname(os.path.abspath(os.path.realpath(__file__))) + pyaedt_toolkit_dir = os.path.normpath(os.path.join(current_dir, r"##TOOLKIT_REL_LIB_DIR##")) + pyaedt_script = os.path.join(pyaedt_toolkit_dir, "toolkit_manager.py") + check_file(python_exe) + check_file(pyaedt_script) + os.environ["PYAEDT_SCRIPT_PROCESS_ID"] = str(oDesktop.GetProcessID()) + os.environ["PYAEDT_SCRIPT_VERSION"] = version + if "Ansys Student" in str(oDesktop.GetExeDir()): + os.environ["PYAEDT_STUDENT_VERSION"] = "True" + else: + os.environ["PYAEDT_STUDENT_VERSION"] = "False" + if version > "2022.2": + os.environ["PYAEDT_SCRIPT_PORT"] = str(oDesktop.GetGrpcServerPort()) + if is_linux: + + edt_root = os.path.normpath(oDesktop.GetExeDir()) + os.environ["ANSYSEM_ROOT{}".format(version)] = edt_root + ld_library_path_dirs_to_add = [ + "{}/commonfiles/CPython/3_7/linx64/Release/python/lib".format(edt_root), + "{}/commonfiles/CPython/3_10/linx64/Release/python/lib".format(edt_root), + "{}/common/mono/Linux64/lib64".format(edt_root), + "{}/Delcross".format(edt_root), + "{}".format(edt_root), + ] + os.environ["LD_LIBRARY_PATH"] = ":".join(ld_library_path_dirs_to_add) + ":" + os.getenv( + "LD_LIBRARY_PATH", "") + command = [ + python_exe, + pyaedt_script, + ] + my_env = os.environ.copy() + subprocess.Popen(command, env=my_env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) + else: + command = [ + '"{}"'.format(python_exe), + '"{}"'.format(pyaedt_script), + ] + my_env = os.environ.copy() + subprocess.Popen(" ".join(command), env=my_env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, shell=True) + except Exception as e: + show_error(str(e)) + + +def check_file(file_path): + if not os.path.isfile(file_path): + show_error('"{}" does not exist. Click the "Install PyAEDT" button in the Automation ribbon.'.format( + file_path)) + + +def show_error(msg): + oDesktop.AddMessage("", "", 2, str(msg)) + MessageBox.Show(str(msg), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error) + sys.exit() + + +def debug(msg): + print("[debug] {}: {}".format(script_name, str(msg))) + LogDebug("{}: {}\n".format(script_name, str(msg))) + + +if __name__ == "__main__": + main() diff --git a/pyaedt/workflows/templates/__init__.py b/pyaedt/workflows/templates/__init__.py new file mode 100644 index 00000000000..3bc3c8e5b19 --- /dev/null +++ b/pyaedt/workflows/templates/__init__.py @@ -0,0 +1,21 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/pyproject.toml b/pyproject.toml index 23772ac788b..e04ca76f97a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ tests = [ "pytest-cov>=4.0.0,<5.1", "pytest-xdist>=3.5.0,<3.7", "pyedb>=0.4.0,<0.10; python_version == '3.7'", - "pyedb>=0.5.0,<0.10; python_version > '3.7'", + "pyedb>=0.5.0,<0.11; python_version > '3.7'", "pyvista>=0.38.0,<0.44", "scikit-learn>=1.0.0,<1.5", "scikit-rf>=0.30.0,<1.1", @@ -100,12 +100,12 @@ doc = [ "utm", "vtk==9.2.6", ] -doc-noexamples = [ +doc-no-examples = [ "ansys-sphinx-theme>=0.10.0,<0.16", "imageio>=2.30.0,<2.35", #"imageio-ffmpeg", "numpydoc>=1.5.0,<1.8", - # "recommonmark", + "recommonmark", "Sphinx==5.3.0; python_version == '3.7'", "Sphinx>=7.1.0,<7.4; python_version > '3.7'", "sphinx-autobuild==2021.3.14; python_version == '3.7'",