├── .flake8 ├── .gitattributes ├── .github ├── CODEOWNERS ├── dependabot.yml ├── labeler.yml ├── labels.yml └── workflows │ ├── ci_cd.yml │ └── label.yml ├── .gitignore ├── .pre-commit-config.yaml ├── AUTHORS ├── CHANGELOG.rst ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── LICENSE ├── README.rst ├── doc ├── .vale.ini ├── Makefile ├── make.bat ├── source │ ├── _static │ │ ├── I_Workflow_70pct.png │ │ ├── README.md │ │ ├── ansys_simai.png │ │ └── pyansys-logo-light_mode.png │ ├── _templates │ │ └── README.md │ ├── api_reference.rst │ ├── api_reference │ │ ├── client.rst │ │ ├── data_types.rst │ │ ├── errors.rst │ │ ├── geometries.rst │ │ ├── global_coefficients_requests.rst │ │ ├── model_configuration.rst │ │ ├── models.rst │ │ ├── optimizations.rst │ │ ├── post_processings.rst │ │ ├── predictions.rst │ │ ├── projects.rst │ │ ├── selections.rst │ │ ├── training_data.rst │ │ ├── training_data_parts.rst │ │ └── workspaces.rst │ ├── changelog.rst │ ├── conf.py │ ├── examples │ │ ├── index.rst │ │ └── pysimai_ex │ │ │ ├── 00-model_configuration_reuse.py │ │ │ ├── 01-model_recomputation.py │ │ │ ├── 02-subset_assignment.py │ │ │ ├── 03-list_based_subset_assignment.py │ │ │ └── README.txt │ ├── index.rst │ ├── user_guide.rst │ └── user_guide │ │ ├── config_file.rst │ │ ├── configuration.rst │ │ ├── installation.rst │ │ ├── proxy.rst │ │ └── pysimai_ug │ │ ├── best_practices.rst │ │ ├── building_a_model.rst │ │ ├── data_exploration.rst │ │ └── index.rst └── styles │ ├── .gitignore │ └── config │ └── vocabularies │ └── ANSYS │ ├── accept.txt │ └── reject.txt ├── pyproject.toml ├── src └── ansys │ └── simai │ └── core │ ├── __init__.py │ ├── api │ ├── __init__.py │ ├── client.py │ ├── geometry.py │ ├── mixin.py │ ├── optimization.py │ ├── point_cloud.py │ ├── post_processing.py │ ├── prediction.py │ ├── project.py │ ├── sse.py │ ├── training_data.py │ ├── training_data_part.py │ └── workspace.py │ ├── client.py │ ├── data │ ├── __init__.py │ ├── base.py │ ├── downloads.py │ ├── geometries.py │ ├── geometry_utils.py │ ├── global_coefficients_requests.py │ ├── lists.py │ ├── model_configuration.py │ ├── models.py │ ├── optimizations.py │ ├── post_processings.py │ ├── predictions.py │ ├── projects.py │ ├── selection_post_processings.py │ ├── selections.py │ ├── training_data.py │ ├── training_data_parts.py │ ├── types.py │ └── workspaces.py │ ├── errors.py │ ├── py.typed │ └── utils │ ├── __init__.py │ ├── auth.py │ ├── config_file.py │ ├── configuration.py │ ├── files.py │ ├── grouping.py │ ├── misc.py │ ├── numerical.py │ ├── pagination.py │ ├── requests.py │ ├── sse_client.py │ ├── typing.py │ └── validation.py ├── tests ├── __init__.py ├── conftest.py ├── test_carpet_bombing.py ├── test_client.py ├── test_client_config_tls_ca_bundle.py ├── test_config.py ├── test_core_mixin.py ├── test_core_sse.py ├── test_data_types.py ├── test_geometries.py ├── test_global_coefficient_requests.py ├── test_model_configuration.py ├── test_models.py ├── test_numerical_utils.py ├── test_optimizations.py ├── test_post_processing_actions.py ├── test_post_processing_export.py ├── test_post_processings.py ├── test_post_processings_results.py ├── test_post_processings_run.py ├── test_predictions.py ├── test_projects.py ├── test_selection.py ├── test_selection_post_processings.py ├── test_sweep_2d.py ├── test_sweep_basic.py ├── test_sweep_tolerance.py ├── test_tolerance_grouper.py ├── test_training_data.py ├── test_workspace.py └── utils │ ├── test_auth.py │ ├── test_config.py │ ├── test_files.py │ └── test_requests.py ├── tox.ini └── uv.lock /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = venv, __init__.py, doc/_build, .venv 3 | select = W191, W291, W293, W391, E115, E117, E122, E124, E125, E225, E231, E301, E303, E501, F401, F403 4 | count = True 5 | max-complexity = 10 6 | max-line-length = 100 7 | statistics = True 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto !eol 2 | *.sh eol=lf 3 | *.bat eol=crlf 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @ansys/pysimai-maintainers 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | labels: 8 | - "maintenance" 9 | - "dependencies" 10 | 11 | - package-ecosystem: "github-actions" 12 | directory: "/" 13 | schedule: 14 | interval: "daily" 15 | labels: 16 | - "maintenance" 17 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | documentation: 2 | - doc/source/**/* 3 | maintenance: 4 | - .github/**/* 5 | - .flake8 6 | - pyproject.toml 7 | dependencies: 8 | - requirements/* 9 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | - name: bug 2 | description: Something isn't working 3 | color: d42a34 4 | 5 | - name: dependencies 6 | description: Related with project dependencies 7 | color: ffc0cb 8 | 9 | - name: documentation 10 | description: Improvements or additions to documentation 11 | color: 0677ba 12 | 13 | - name: enhancement 14 | description: New features or code improvements 15 | color: FFD827 16 | 17 | - name: good first issue 18 | description: Easy to solve for newcomers 19 | color: 62ca50 20 | 21 | - name: maintenance 22 | description: Package and maintenance related 23 | color: f78c37 24 | 25 | - name: release 26 | description: Anything related to an incoming release 27 | color: ffffff 28 | -------------------------------------------------------------------------------- /.github/workflows/ci_cd.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | tags: 6 | - "*" 7 | branches: 8 | - main 9 | 10 | env: 11 | MAIN_PYTHON_VERSION: "3.13" 12 | DOCUMENTATION_CNAME: "simai.docs.pyansys.com" 13 | LIBRARY_NAME: "ansys-simai-core" 14 | 15 | concurrency: 16 | group: ${{ github.workflow }}-${{ github.ref }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | code-style: 21 | name: "Code style" 22 | runs-on: ubuntu-latest 23 | timeout-minutes: 3 24 | steps: 25 | - uses: ansys/actions/code-style@v10 26 | with: 27 | python-version: ${{ env.MAIN_PYTHON_VERSION }} 28 | 29 | doc-style: 30 | name: "Documentation style" 31 | runs-on: ubuntu-latest 32 | timeout-minutes: 3 33 | steps: 34 | - uses: ansys/actions/doc-style@v10 35 | with: 36 | token: ${{ secrets.GITHUB_TOKEN }} 37 | 38 | smoke-tests: 39 | name: "Build and Smoke tests" 40 | runs-on: ${{ matrix.os }} 41 | needs: [code-style] 42 | timeout-minutes: 5 43 | strategy: 44 | fail-fast: false 45 | matrix: 46 | os: [ubuntu-latest, windows-latest] 47 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 48 | steps: 49 | - uses: ansys/actions/build-wheelhouse@v10 50 | with: 51 | library-name: ${{ env.LIBRARY_NAME }} 52 | operating-system: ${{ matrix.os }} 53 | python-version: ${{ matrix.python-version }} 54 | check-licenses: ${{ matrix.python-version != '3.9' }} 55 | # jeepney is MIT but action thinks it's UNKNOWN 56 | # https://gitlab.com/takluyver/jeepney/-/blob/master/LICENSE?ref_type=heads 57 | whitelist-license-check: "jeepney,urllib3" 58 | 59 | tests: 60 | name: "Test using Python ${{ matrix.python-version }} on ${{ matrix.os }}" 61 | runs-on: ${{ matrix.os }} 62 | needs: [smoke-tests] 63 | timeout-minutes: 5 64 | strategy: 65 | matrix: 66 | os: [ubuntu-latest, windows-latest] 67 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 68 | fail-fast: false 69 | steps: 70 | - name: Setup uv 71 | uses: astral-sh/setup-uv@v5 72 | with: 73 | python-version: ${{ matrix.python-version }} 74 | - uses: actions/checkout@v4 75 | - name: Install dependencies 76 | run: uv sync 77 | - name: run tests 78 | run: uv run pytest --cov=ansys --cov-report=term --cov-report=html:.cov/html 79 | - name: Upload coverage to Codecov 80 | uses: codecov/codecov-action@v5 81 | if: matrix.python-version == ${{ env.MAIN_PYTHON_VERSION }} && matrix.os == 'ubuntu-latest' 82 | env: 83 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 84 | 85 | doc-build: 86 | name: "Build documentation" 87 | runs-on: ubuntu-latest 88 | needs: [doc-style] 89 | timeout-minutes: 10 90 | steps: 91 | - name: Setup uv 92 | uses: astral-sh/setup-uv@v5 93 | with: 94 | python-version: ${{ matrix.python-version }} 95 | - uses: actions/checkout@v4 96 | - name: Generate doc requirements 97 | run: uv export --only-group docs -o doc_requirements.txt 98 | - uses: ansys/actions/doc-build@v10 99 | with: 100 | python-version: ${{ env.MAIN_PYTHON_VERSION }} 101 | checkout: false 102 | check-links: false 103 | requirements-file: doc_requirements.txt 104 | 105 | build-library: 106 | name: "Build library basic example" 107 | runs-on: ubuntu-latest 108 | needs: [doc-build, tests] 109 | timeout-minutes: 10 110 | steps: 111 | - uses: ansys/actions/build-library@v10 112 | with: 113 | library-name: ${{ env.LIBRARY_NAME }} 114 | python-version: ${{ env.MAIN_PYTHON_VERSION }} 115 | 116 | doc-deploy-dev: 117 | name: "Deploy development documentation" 118 | runs-on: ubuntu-latest 119 | needs: [build-library] 120 | timeout-minutes: 10 121 | if: github.event_name == 'push' && !contains(github.ref, 'refs/tags') 122 | steps: 123 | - uses: ansys/actions/doc-deploy-dev@v10 124 | with: 125 | cname: ${{ env.DOCUMENTATION_CNAME }} 126 | bot-user: ${{ secrets.PYANSYS_CI_BOT_USERNAME }} 127 | bot-email: ${{ secrets.PYANSYS_CI_BOT_EMAIL }} 128 | token: ${{ secrets.GITHUB_TOKEN }} 129 | 130 | doc-deploy-stable: 131 | name: "Deploy stable documentation" 132 | runs-on: ubuntu-latest 133 | needs: [build-library] 134 | timeout-minutes: 10 135 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags') 136 | steps: 137 | - uses: ansys/actions/doc-deploy-stable@v10 138 | with: 139 | cname: ${{ env.DOCUMENTATION_CNAME }} 140 | bot-user: ${{ secrets.PYANSYS_CI_BOT_USERNAME }} 141 | bot-email: ${{ secrets.PYANSYS_CI_BOT_EMAIL }} 142 | token: ${{ secrets.GITHUB_TOKEN }} 143 | 144 | release: 145 | name: "Release to private and public PyPI and to GitHub" 146 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags') 147 | permissions: 148 | contents: write 149 | runs-on: ubuntu-latest 150 | needs: [build-library] 151 | timeout-minutes: 10 152 | steps: 153 | - name: "Release to the public PyPI repository" 154 | uses: ansys/actions/release-pypi-public@v10 155 | with: 156 | library-name: ${{ env.LIBRARY_NAME }} 157 | twine-username: "__token__" 158 | twine-token: ${{ secrets.PYPI_TOKEN }} 159 | 160 | - name: "Release to GitHub" 161 | uses: ansys/actions/release-github@v10 162 | with: 163 | library-name: ${{ env.LIBRARY_NAME }} 164 | token: ${{ secrets.GITHUB_TOKEN }} 165 | -------------------------------------------------------------------------------- /.github/workflows/label.yml: -------------------------------------------------------------------------------- 1 | name: Labeler 2 | on: 3 | pull_request: 4 | push: 5 | branches: [ main ] 6 | paths: 7 | - '../labels.yml' 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | 15 | label-syncer: 16 | name: Syncer 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: micnncim/action-label-syncer@v1 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | 24 | labeler: 25 | name: Set labels 26 | needs: [label-syncer] 27 | permissions: 28 | contents: read 29 | pull-requests: write 30 | runs-on: ubuntu-latest 31 | steps: 32 | 33 | # Label based on modified files 34 | - name: Label based on changed files 35 | uses: actions/labeler@v4 36 | with: 37 | repo-token: ${{ secrets.GITHUB_TOKEN }} 38 | sync-labels: '' 39 | 40 | # Label based on branch name 41 | - uses: actions-ecosystem/action-add-labels@v1 42 | if: | 43 | startsWith(github.event.pull_request.head.ref, 'doc') || 44 | startsWith(github.event.pull_request.head.ref, 'docs') 45 | with: 46 | labels: documentation 47 | 48 | - uses: actions-ecosystem/action-add-labels@v1 49 | if: | 50 | startsWith(github.event.pull_request.head.ref, 'maint') || 51 | startsWith(github.event.pull_request.head.ref, 'no-ci') || 52 | startsWith(github.event.pull_request.head.ref, 'ci') 53 | with: 54 | labels: maintenance 55 | 56 | - uses: actions-ecosystem/action-add-labels@v1 57 | if: startsWith(github.event.pull_request.head.ref, 'feat') 58 | with: 59 | labels: | 60 | enhancement 61 | 62 | - uses: actions-ecosystem/action-add-labels@v1 63 | if: | 64 | startsWith(github.event.pull_request.head.ref, 'fix') || 65 | startsWith(github.event.pull_request.head.ref, 'patch') 66 | with: 67 | labels: bug 68 | 69 | commenter: 70 | runs-on: ubuntu-latest 71 | steps: 72 | - name: Suggest to add labels 73 | uses: peter-evans/create-or-update-comment@v4 74 | # Execute only when no labels have been applied to the pull request 75 | if: toJSON(github.event.pull_request.labels.*.name) == '{}' 76 | with: 77 | issue-number: ${{ github.event.pull_request.number }} 78 | body: | 79 | Please add one of the following labels to add this contribution to the Release Notes :point_down: 80 | - [bug](https://github.com/ansys/pysimai/pulls?q=label%3Abug+) 81 | - [documentation](https://github.com/ansys/pysimai/pulls?q=label%3Adocumentation+) 82 | - [enhancement](https://github.com/ansys/pysimai/pulls?q=label%3Aenhancement+) 83 | - [good first issue](https://github.com/ansys/pysimai/pulls?q=label%3Agood+first+issue) 84 | - [maintenance](https://github.com/ansys/pysimai/pulls?q=label%3Amaintenance+) 85 | - [release](https://github.com/ansys/pysimai/pulls?q=label%3Arelease+) 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/python 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .cov 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | cover/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | doc/_build/ 78 | 79 | # PyBuilder 80 | .pybuilder/ 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | # For a library or package, you might want to ignore these files since the code is 92 | # intended to run in multiple environments; otherwise, check them in: 93 | # .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # poetry 103 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 104 | # This is especially recommended for binary packages to ensure reproducibility, and is more 105 | # commonly ignored for libraries. 106 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 107 | #poetry.lock 108 | 109 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 110 | __pypackages__/ 111 | 112 | # Celery stuff 113 | celerybeat-schedule 114 | celerybeat.pid 115 | 116 | # SageMath parsed files 117 | *.sage.py 118 | 119 | # Environments 120 | .env 121 | .venv 122 | env/ 123 | venv/ 124 | ENV/ 125 | env.bak/ 126 | venv.bak/ 127 | 128 | # Spyder project settings 129 | .spyderproject 130 | .spyproject 131 | 132 | # Rope project settings 133 | .ropeproject 134 | 135 | # mkdocs documentation 136 | /site 137 | 138 | # mypy 139 | .mypy_cache/ 140 | .dmypy.json 141 | dmypy.json 142 | 143 | # Pyre type checker 144 | .pyre/ 145 | 146 | # pytype static type analyzer 147 | .pytype/ 148 | 149 | # Cython debug symbols 150 | cython_debug/ 151 | 152 | # PyCharm 153 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 154 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 155 | # and can be added to the global gitignore or merged into this file. For a more nuclear 156 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 157 | #.idea/ 158 | 159 | # End of https://www.toptal.com/developers/gitignore/api/python 160 | 161 | # pdm 162 | .pdm-python 163 | 164 | # vscode 165 | .vscode/ 166 | 167 | # pycharm 168 | .idea/ 169 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | # Ruff version. 4 | rev: v0.11.12 5 | hooks: 6 | # Run the linter. 7 | - id: ruff 8 | args: [--fix] 9 | # Run the formatter. 10 | - id: ruff-format 11 | 12 | - repo: https://github.com/asottile/blacken-docs 13 | rev: 1.19.1 14 | hooks: 15 | - id: blacken-docs 16 | 17 | - repo: https://github.com/codespell-project/codespell 18 | rev: v2.4.1 19 | hooks: 20 | - id: codespell 21 | args: ["--ignore-words", "doc/styles/config/vocabularies/ANSYS/accept.txt"] 22 | 23 | - repo: https://github.com/pre-commit/pre-commit-hooks 24 | rev: v5.0.0 25 | hooks: 26 | - id: check-merge-conflict 27 | - id: trailing-whitespace 28 | 29 | - repo: https://github.com/python-jsonschema/check-jsonschema 30 | rev: 0.33.0 31 | hooks: 32 | - id: check-github-workflows 33 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the list of simai's significant contributors. 2 | # 3 | # This file does not necessarily list everyone who has contributed code. 4 | # 5 | # For contributions made under a Corporate CLA, the organization is 6 | # added to this file. 7 | # 8 | # If you have contributed to the repository and wish to be added to this file 9 | # please submit a request. 10 | # 11 | # 12 | ANSYS, Inc. 13 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our 7 | project and our community a harassment-free experience for everyone, 8 | regardless of age, body size, disability, ethnicity, sex 9 | characteristics, gender identity and expression, level of experience, 10 | education, socio-economic status, nationality, personal appearance, 11 | race, religion, or sexual identity and orientation. 12 | 13 | ## Our Standards 14 | 15 | Examples of behavior that contributes to creating a positive environment 16 | include: 17 | 18 | * Using welcoming and inclusive language 19 | * Being respectful of differing viewpoints and experiences 20 | * Gracefully accepting constructive criticism 21 | * Focusing on what is best for the community 22 | * Showing empathy towards other community members 23 | 24 | Examples of unacceptable behavior by participants include: 25 | 26 | * The use of sexualized language or imagery and unwelcome sexual 27 | attention or advances 28 | * Trolling, insulting/derogatory comments, and personal or political attacks 29 | * Public or private harassment 30 | * Publishing others' private information, such as a physical or electronic 31 | address, without explicit permission 32 | * Other conduct which could reasonably be considered inappropriate in a 33 | professional setting 34 | 35 | ## Our Responsibilities 36 | 37 | Project maintainers are responsible for clarifying the standards of acceptable 38 | behavior and are expected to take appropriate and fair corrective action in 39 | response to any instances of unacceptable behavior. 40 | 41 | Project maintainers have the right and responsibility to remove, edit, or reject 42 | comments, commits, code, wiki edits, issues, and other contributions that are 43 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 44 | contributor for other behaviors that they deem inappropriate, threatening, 45 | offensive, or harmful. 46 | 47 | ## Scope 48 | 49 | This Code of Conduct applies both within project spaces and in public spaces 50 | when an individual is representing the project or its community. Examples of 51 | representing a project or community include using an official project e-mail 52 | address, posting via an official social media account, or acting as an appointed 53 | representative at an online or offline event. Representation of a project may be 54 | further defined and clarified by project maintainers. 55 | 56 | ## Attribution 57 | 58 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 59 | version 1.4, available at 60 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 61 | 62 | [homepage]: https://www.contributor-covenant.org 63 | 64 | For answers to common questions about this code of conduct, see 65 | https://www.contributor-covenant.org/faq 66 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | ## Project Lead 4 | 5 | * [Marc Planelles](https://github.com/tmpbeing) 6 | 7 | ## Individual Contributors 8 | 9 | * [Arthur Woimbée](https://github.com/awoimbee) 10 | * [camzeech](https://github.com/camzeech) 11 | * [Iason Batzianoulis](https://github.com/yias) 12 | * [Kathy Pippert](https://github.com/PipKat) 13 | * [kliment-slice](https://github.com/kliment-slice) 14 | * [Maid Sultanovic](https://github.com/msd-11) 15 | * [Marie Lelandais](https://github.com/marielelandais) 16 | * [Maxime Rey](https://github.com/MaxJPRey) 17 | * [mmeisson](https://github.com/mmeisso) 18 | * [MorganeExtrality](https://github.com/MorganeExtrality) 19 | * [Revathy Venugopal](https://github.com/Revathyvenugopal162) 20 | * [Roberto Pastor Muela](https://github.com/RobPasMue) 21 | * [romane-m](https://github.com/romane-m) 22 | * [Sylvain Collas](https://github.com/u8slvn) 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 - 2025 ANSYS, Inc. and/or its affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | PySimAI 2 | ======= 3 | |pyansys| |python| |pypi| |GH-CI| |codecov| |MIT| |ruff| 4 | 5 | .. |pyansys| image:: https://img.shields.io/badge/Py-Ansys-ffc107.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABDklEQVQ4jWNgoDfg5mD8vE7q/3bpVyskbW0sMRUwofHD7Dh5OBkZGBgW7/3W2tZpa2tLQEOyOzeEsfumlK2tbVpaGj4N6jIs1lpsDAwMJ278sveMY2BgCA0NFRISwqkhyQ1q/Nyd3zg4OBgYGNjZ2ePi4rB5loGBhZnhxTLJ/9ulv26Q4uVk1NXV/f///////69du4Zdg78lx//t0v+3S88rFISInD59GqIH2esIJ8G9O2/XVwhjzpw5EAam1xkkBJn/bJX+v1365hxxuCAfH9+3b9/+////48cPuNehNsS7cDEzMTAwMMzb+Q2u4dOnT2vWrMHu9ZtzxP9vl/69RVpCkBlZ3N7enoDXBwEAAA+YYitOilMVAAAAAElFTkSuQmCC 6 | :target: https://docs.pyansys.com/ 7 | :alt: PyAnsys 8 | 9 | .. |python| image:: https://img.shields.io/pypi/pyversions/ansys-simai-core?logo=pypi 10 | :target: https://pypi.org/project/ansys-simai-core/ 11 | :alt: Python 12 | 13 | .. |pypi| image:: https://img.shields.io/pypi/v/ansys-simai-core.svg?logo=python&logoColor=white 14 | :target: https://pypi.org/project/ansys-simai-core 15 | :alt: PyPI 16 | 17 | .. |codecov| image:: https://codecov.io/gh/ansys/pysimai/branch/main/graph/badge.svg 18 | :target: https://codecov.io/gh/ansys/pysimai 19 | :alt: Codecov 20 | 21 | .. |GH-CI| image:: https://github.com/ansys/pysimai/actions/workflows/ci_cd.yml/badge.svg 22 | :target: https://github.com/ansys/pysimai/actions/workflows/ci_cd.yml 23 | :alt: GH-CI 24 | 25 | .. |MIT| image:: https://img.shields.io/badge/License-MIT-yellow.svg 26 | :target: https://opensource.org/licenses/MIT 27 | :alt: MIT 28 | 29 | .. |ruff| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json 30 | :target: https://github.com/astral-sh/ruff 31 | :alt: Ruff 32 | 33 | A Python wrapper for Ansys SimAI 34 | 35 | 36 | How to install 37 | -------------- 38 | 39 | At least two installation modes are provided: user and developer. 40 | 41 | For users 42 | ^^^^^^^^^ 43 | 44 | In order to install PySimAI, make sure you 45 | have the latest version of `pip`_. To do so, run: 46 | 47 | .. code:: bash 48 | 49 | python -m pip install -U pip 50 | 51 | Then, you can simply execute: 52 | 53 | .. code:: bash 54 | 55 | python -m pip install ansys-simai-core 56 | 57 | For developers 58 | ^^^^^^^^^^^^^^ 59 | 60 | Installing PySimAI in developer mode allows 61 | you to modify the source and enhance it. 62 | 63 | Before contributing to the project, please refer to the `PyAnsys Developer's guide`_. You will 64 | need to follow these steps: 65 | 66 | #. Start by cloning this repository: 67 | 68 | .. code:: bash 69 | 70 | git clone https://github.com/ansys/pysimai 71 | 72 | #. `Install uv `_. NB: If you are a Windows user, make sure that Python is installed on your system and it is added to the Path. 73 | 74 | #. Use uv to run commands 75 | 76 | .. code:: shell 77 | 78 | uv run pytest -xlv 79 | 80 | #. Finally, verify your development installation by running: 81 | 82 | .. code:: bash 83 | 84 | uv tool install tox --with tox-uv 85 | tox 86 | 87 | #. Alternatively, you can also run tasks defined in `pyproject.toml` using `poethepoet`: 88 | 89 | .. code:: shell 90 | 91 | uv tool install poethepoet 92 | uv run poe lint 93 | uv run poe test 94 | uv run poe doc 95 | 96 | 97 | How to test 98 | ----------- 99 | 100 | This project takes advantage of `tox`_. This tool allows to automate common 101 | development tasks (similar to Makefile) but it is oriented towards Python 102 | development. 103 | 104 | Using tox 105 | ^^^^^^^^^ 106 | 107 | As Makefile has rules, `tox`_ has environments. In fact, the tool creates its 108 | own virtual environment so anything being tested is isolated from the project in 109 | order to guarantee project's integrity. The following environments commands are provided: 110 | 111 | - **tox -e style**: will check for coding style quality. 112 | - **tox -e py**: checks for unit tests. 113 | - **tox -e py-coverage**: checks for unit testing and code coverage. 114 | - **tox -e doc**: checks for documentation building process. 115 | 116 | 117 | Raw testing 118 | ^^^^^^^^^^^ 119 | 120 | If required, you can always call the style commands (`ruff`_) or unit testing ones (`pytest`_) from the command line. However, 121 | this does not guarantee that your project is being tested in an isolated 122 | environment, which is the reason why tools like `tox`_ exist. 123 | 124 | 125 | A note on pre-commit 126 | ^^^^^^^^^^^^^^^^^^^^ 127 | 128 | The style checks take advantage of `pre-commit`_. Developers are not forced but 129 | encouraged to install this tool via: 130 | 131 | .. code:: bash 132 | 133 | uv tool install pre-commit && pre-commit install 134 | 135 | 136 | Documentation 137 | ------------- 138 | 139 | For building documentation, you can either run the usual rules provided in the 140 | `Sphinx`_ Makefile, such as: 141 | 142 | .. code:: bash 143 | 144 | uv run make -C doc/ html && open doc/html/index.html 145 | 146 | However, the recommended way of checking documentation integrity is using: 147 | 148 | .. code:: bash 149 | 150 | tox -e doc && open .tox/doc_out/index.html 151 | 152 | 153 | Distributing 154 | ------------ 155 | 156 | uv commands can help you build or publish the package 157 | 158 | .. code:: bash 159 | 160 | uv build 161 | uv publish 162 | 163 | 164 | .. LINKS AND REFERENCES 165 | .. _ruff: https://github.com/astral-sh/ruff 166 | .. _pip: https://pypi.org/project/pip/ 167 | .. _pre-commit: https://pre-commit.com/ 168 | .. _PyAnsys Developer's guide: https://dev.docs.pyansys.com/ 169 | .. _pytest: https://docs.pytest.org/en/stable/ 170 | .. _Sphinx: https://www.sphinx-doc.org/en/master/ 171 | .. _tox: https://tox.wiki/ 172 | -------------------------------------------------------------------------------- /doc/.vale.ini: -------------------------------------------------------------------------------- 1 | # Core settings 2 | # ============= 3 | 4 | # Location of our `styles` 5 | StylesPath = "styles" 6 | 7 | # The options are `suggestion`, `warning`, or `error` (defaults to “warning”). 8 | MinAlertLevel = warning 9 | 10 | # By default, `code` and `tt` are ignored. 11 | IgnoredScopes = code, tt 12 | 13 | # By default, `script`, `style`, `pre`, and `figure` are ignored. 14 | SkippedScopes = script, style, pre, figure 15 | 16 | # WordTemplate specifies what Vale will consider to be an individual word. 17 | WordTemplate = \b(?:%s)\b 18 | 19 | # List of Packages to be used for our guidelines 20 | Packages = Google 21 | 22 | # Define the Ansys vocabulary 23 | Vocab = ANSYS 24 | 25 | [*.{md,rst}] 26 | 27 | # Apply the following styles 28 | BasedOnStyles = Vale, Google 29 | 30 | # Inline roles are ignored 31 | TokenIgnores = (:.*:`.*`) 32 | 33 | # Turn off vale terms 34 | Vale.Terms = NO 35 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS = -j auto 7 | SPHINXBUILD = sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | 22 | # Customized clean due to examples gallery 23 | clean: 24 | rm -rf $(BUILDDIR)/* 25 | rm -rf $(SOURCEDIR)/examples 26 | find . -type d -name "_autosummary" -exec rm -rf {} + 27 | 28 | # Customized pdf for svg format images 29 | pdf: 30 | @$(SPHINXBUILD) -M latex "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 31 | cd $(BUILDDIR)/latex && latexmk -r latexmkrc -pdf *.tex -interaction=nonstopmode || true 32 | (test -f $(BUILDDIR)/latex/*.pdf && echo pdf exists) || exit 1 33 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | if "%1" == "clean" goto clean 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 30 | goto end 31 | 32 | :clean 33 | rmdir /s /q %BUILDDIR% > /NUL 2>&1 34 | for /d /r %SOURCEDIR% %%d in (_autosummary) do @if exist "%%d" rmdir /s /q "%%d" 35 | goto end 36 | 37 | :help 38 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 39 | 40 | :end 41 | popd 42 | -------------------------------------------------------------------------------- /doc/source/_static/I_Workflow_70pct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansys/pysimai/a7c55f0399a895014a9b4edbb5025297c04d2e03/doc/source/_static/I_Workflow_70pct.png -------------------------------------------------------------------------------- /doc/source/_static/README.md: -------------------------------------------------------------------------------- 1 | Static files can be found here (like images and other assets). 2 | -------------------------------------------------------------------------------- /doc/source/_static/ansys_simai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansys/pysimai/a7c55f0399a895014a9b4edbb5025297c04d2e03/doc/source/_static/ansys_simai.png -------------------------------------------------------------------------------- /doc/source/_static/pyansys-logo-light_mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansys/pysimai/a7c55f0399a895014a9b4edbb5025297c04d2e03/doc/source/_static/pyansys-logo-light_mode.png -------------------------------------------------------------------------------- /doc/source/_templates/README.md: -------------------------------------------------------------------------------- 1 | ## Contains templates for the documentation build 2 | -------------------------------------------------------------------------------- /doc/source/api_reference.rst: -------------------------------------------------------------------------------- 1 | .. _api_reference: 2 | 3 | API reference 4 | ============= 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | :glob: 9 | 10 | 11 | api_reference/* 12 | -------------------------------------------------------------------------------- /doc/source/api_reference/client.rst: -------------------------------------------------------------------------------- 1 | .. _simai_client: 2 | 3 | Client 4 | ====== 5 | 6 | .. py:module:: ansys.simai.core.client 7 | 8 | .. autoclass:: SimAIClient 9 | :members: 10 | -------------------------------------------------------------------------------- /doc/source/api_reference/data_types.rst: -------------------------------------------------------------------------------- 1 | Data types 2 | ========== 3 | 4 | .. automodule:: ansys.simai.core.data.types 5 | :members: 6 | -------------------------------------------------------------------------------- /doc/source/api_reference/errors.rst: -------------------------------------------------------------------------------- 1 | Errors 2 | ====== 3 | 4 | .. automodule:: ansys.simai.core.errors 5 | :members: 6 | -------------------------------------------------------------------------------- /doc/source/api_reference/geometries.rst: -------------------------------------------------------------------------------- 1 | .. _geometries: 2 | 3 | Geometries 4 | ========== 5 | 6 | .. py:module:: ansys.simai.core.data.geometries 7 | 8 | Geometries are the core of *SimAI deep learning-powered predictions*. 9 | A geometry is a 3D model and the associated metadata managed by the SimAI platform. 10 | 11 | .. _geometry_format: 12 | 13 | File format 14 | ----------- 15 | 16 | The input format for your workspace is described by the model manifest. 17 | You use the :attr:`workspace.model.geometry` 18 | attribute to access the information for a specific workspace. 19 | 20 | Directory 21 | --------- 22 | 23 | .. autoclass:: GeometryDirectory() 24 | :members: 25 | 26 | Model 27 | ----- 28 | 29 | .. autoclass:: Geometry() 30 | :members: 31 | :inherited-members: 32 | 33 | 34 | Filtering 35 | --------- 36 | 37 | .. autoclass:: Range() 38 | -------------------------------------------------------------------------------- /doc/source/api_reference/global_coefficients_requests.rst: -------------------------------------------------------------------------------- 1 | .. _global_coefficient_requests: 2 | 3 | Global Coefficients 4 | =================== 5 | 6 | .. py:module:: ansys.simai.core.data.global_coefficients_requests 7 | 8 | This module contains a collection of classes for validating global coefficient formulas and 9 | executing them on project samples. 10 | 11 | CheckGlobalCoefficient 12 | ---------------------- 13 | 14 | .. autoclass:: CheckGlobalCoefficient() 15 | :members: 16 | 17 | CheckGlobalCoefficientDirectory 18 | ------------------------------- 19 | 20 | .. autoclass:: CheckGlobalCoefficientDirectory() 21 | :members: 22 | 23 | ComputeGlobalCoefficient 24 | ------------------------ 25 | 26 | .. autoclass:: ComputeGlobalCoefficient() 27 | :members: 28 | 29 | ComputeGlobalCoefficientDirectory 30 | --------------------------------- 31 | 32 | .. autoclass:: ComputeGlobalCoefficientDirectory() 33 | :members: -------------------------------------------------------------------------------- /doc/source/api_reference/model_configuration.rst: -------------------------------------------------------------------------------- 1 | .. _model_configuration: 2 | 3 | Model configuration 4 | =================== 5 | 6 | .. py:module:: ansys.simai.core.data.model_configuration 7 | 8 | This module contains a collection of classes for creating a model configuration. 9 | The model configuration defines the model's inputs, outputs, Global Coefficients, 10 | build duration and project. The resulting 11 | (:py:class:`ModelConfiguration`) 12 | object is subsequently used to train a model. 13 | 14 | 15 | GlobalCoefficientDefinition 16 | --------------------------- 17 | 18 | .. autoclass:: GlobalCoefficientDefinition() 19 | :members: 20 | 21 | 22 | DomainAxisDefinition 23 | -------------------- 24 | 25 | .. autoclass:: DomainAxisDefinition() 26 | 27 | 28 | DomainOfAnalysis 29 | -------------------- 30 | 31 | .. autoclass:: DomainOfAnalysis() 32 | 33 | 34 | ModelInput 35 | ---------- 36 | 37 | .. autoclass:: ModelInput() 38 | :members: 39 | 40 | ModelOutput 41 | ----------- 42 | 43 | .. autoclass:: ModelOutput() 44 | :members: 45 | 46 | ModelConfiguration 47 | ------------------ 48 | 49 | .. autoclass:: ModelConfiguration() 50 | :members: 51 | 52 | PostProcessInput 53 | ----------------- 54 | 55 | .. autoclass:: PostProcessInput() 56 | :members: 57 | -------------------------------------------------------------------------------- /doc/source/api_reference/models.rst: -------------------------------------------------------------------------------- 1 | .. _models: 2 | 3 | Models 4 | ====== 5 | 6 | .. py:module:: ansys.simai.core.data.models 7 | 8 | A collection of classes for building a SimAI model. 9 | Launching a build requires a configuration 10 | (:py:class:`ModelConfiguration`) 11 | which defines the model properties, such as its inputs and outputs, 12 | the Global Coefficients and the Domain of Analysis, and its project. The 13 | :py:class:`ModelConfiguration` 14 | object is, then, parsed to :py:meth:`models.build()` for 15 | launching a build. 16 | 17 | 18 | Directory 19 | --------- 20 | 21 | .. autoclass:: ModelDirectory() 22 | :members: 23 | :exclude-members: get 24 | 25 | 26 | Model 27 | --------- 28 | 29 | .. autoclass:: Model() 30 | :members: 31 | -------------------------------------------------------------------------------- /doc/source/api_reference/optimizations.rst: -------------------------------------------------------------------------------- 1 | .. _optimizations: 2 | 3 | Optimization 4 | ============ 5 | 6 | .. py:module:: ansys.simai.core.data.optimizations 7 | 8 | Directory 9 | --------- 10 | 11 | .. autoclass:: OptimizationDirectory() 12 | :members: 13 | 14 | Model 15 | ----- 16 | 17 | .. autoclass:: Optimization() 18 | :members: 19 | :inherited-members: 20 | 21 | .. 22 | TrialRuns 23 | ========= 24 | 25 | Trial runs are a single step of the optimization process. 26 | 27 | Directory 28 | --------- 29 | 30 | .. autoclass:: __OptimizationTrialRunDirectory() 31 | :members: 32 | 33 | 34 | Model 35 | ----- 36 | 37 | .. autoclass:: __OptimizationTrialRun() 38 | :members: 39 | :inherited-members: 40 | -------------------------------------------------------------------------------- /doc/source/api_reference/post_processings.rst: -------------------------------------------------------------------------------- 1 | .. _post_processings: 2 | 3 | Postprocessings 4 | =============== 5 | 6 | .. py:module:: ansys.simai.core.data.post_processings 7 | 8 | Directory 9 | --------- 10 | 11 | .. autoclass:: PostProcessingDirectory 12 | :members: 13 | 14 | Model 15 | ----- 16 | 17 | .. autoclass:: PostProcessing() 18 | :members: 19 | :inherited-members: 20 | 21 | 22 | .. _pp_methods: 23 | 24 | Nested prediction namespace 25 | --------------------------- 26 | 27 | .. autoclass:: PredictionPostProcessings() 28 | :members: 29 | 30 | .. _available_pp: 31 | 32 | Available postprocessings 33 | -------------------------- 34 | 35 | .. note:: 36 | Depending on the capabilities of your model, some of these objects may not 37 | be available in your workspace. You can use the 38 | :meth:`~ansys.simai.core.data.post_processings.PostProcessingDirectory.info` method 39 | to see which ones are available. 40 | 41 | .. autoclass:: GlobalCoefficients() 42 | :members: 43 | :inherited-members: PostProcessing 44 | 45 | 46 | .. autoclass:: SurfaceEvolution() 47 | :members: 48 | :inherited-members: PostProcessing 49 | 50 | 51 | .. autoclass:: Slice() 52 | :members: 53 | 54 | 55 | .. autoclass:: SurfaceVTP() 56 | :members: 57 | 58 | 59 | .. autoclass:: SurfaceVTPTDLocation() 60 | :members: 61 | 62 | 63 | .. autoclass:: VolumeVTU() 64 | :members: 65 | 66 | 67 | .. autoclass:: CustomVolumePointCloud() 68 | :members: 69 | 70 | 71 | Helpers 72 | ------- 73 | 74 | .. autoclass:: DownloadableResult() 75 | :members: -------------------------------------------------------------------------------- /doc/source/api_reference/predictions.rst: -------------------------------------------------------------------------------- 1 | .. _predictions: 2 | 3 | Predictions 4 | =========== 5 | 6 | .. py:module:: ansys.simai.core.data.predictions 7 | 8 | The ``Prediction`` module is in charge of running the *SimAI-powered 9 | predictions* on the :py:class:`geometries` 10 | that you have uploaded. 11 | 12 | A prediction represents a numerical prediction with geometry and boundary conditions. 13 | The arguments to the :py:meth:`predictions.run()` method 14 | depend on your model. 15 | 16 | .. code-block:: python 17 | 18 | # Run a prediction on a given geometry with the specified velocity. 19 | velocity = 10.0 20 | prediction = geometry.run_prediction(Vx=velocity) 21 | 22 | 23 | Directory 24 | --------- 25 | 26 | .. autoclass:: PredictionDirectory() 27 | :members: 28 | 29 | Model 30 | ----- 31 | 32 | .. autoclass:: Prediction() 33 | :members: 34 | :inherited-members: 35 | -------------------------------------------------------------------------------- /doc/source/api_reference/projects.rst: -------------------------------------------------------------------------------- 1 | .. _projects: 2 | 3 | Projects 4 | ======== 5 | 6 | .. py:module:: ansys.simai.core.data.projects 7 | 8 | Projects are a selection of training data used to train a model. 9 | 10 | Directory 11 | --------- 12 | 13 | .. autoclass:: ProjectDirectory() 14 | :members: 15 | 16 | Model 17 | ----- 18 | 19 | .. autoclass:: Project() 20 | :members: 21 | :inherited-members: 22 | 23 | IsTrainableInfo 24 | --------------- 25 | 26 | .. autoclass:: IsTrainableInfo() 27 | :members: 28 | :exclude-members: is_trainable, reason 29 | 30 | TrainingCapabilities 31 | -------------------- 32 | 33 | .. autoclass:: TrainingCapabilities() 34 | :members: 35 | 36 | ContinuousLearningCapabilities 37 | ------------------------------ 38 | 39 | .. autoclass:: ContinuousLearningCapabilities() 40 | :members: 41 | -------------------------------------------------------------------------------- /doc/source/api_reference/selections.rst: -------------------------------------------------------------------------------- 1 | .. _selections: 2 | 3 | Selections 4 | ========== 5 | 6 | Selection basics 7 | ---------------- 8 | .. py:module:: ansys.simai.core.data.selections 9 | 10 | The :class:`Selection` class allows you 11 | to run a large number of operations in parallel by manipulating whole collections of SimAI models 12 | (:class:`Geometries `, 13 | :class:`Predictions `, and 14 | :py:class:`Post-Processings ` instances). 15 | 16 | You create a selection by combining a list of :class:`Geometry ` 17 | instances with a list of :class:`~ansys.simai.core.data.types.BoundaryConditions` instances: 18 | 19 | .. code-block:: python 20 | 21 | from ansys.simai.core.data.selections import Selection 22 | 23 | geometries = simai.geometries.list()[:4] 24 | boundary_conditions = [dict(Vx=vx) for vx in [12.2, 12.4, 12.6]] 25 | selection = Selection(geometries, boundary_conditions) 26 | 27 | 28 | The resulting selection contains all possible combinations between the geometries and 29 | boundary conditions. Each of those combinations is a :class:`~ansys.simai.core.data.selections.Point` 30 | instance, which can be viewed as a potential :class:`~ansys.simai.core.data.predictions.Prediction` 31 | instance. 32 | 33 | At first, all predictions may not exist. However, you can use the :meth:`~Selection.run_predictions` 34 | method to run them: 35 | 36 | .. code-block:: python 37 | 38 | # run all predictions 39 | selection.run_predictions() 40 | 41 | all_predictions = selection.predictions 42 | 43 | 44 | Selection API reference 45 | ------------------------ 46 | 47 | In essence, a :class:`~ansys.simai.core.data.selections.Selection` instance is a 48 | collection of :class:`points ` instances. 49 | 50 | .. autoclass:: Point() 51 | :members: 52 | :inherited-members: 53 | 54 | 55 | .. autoclass:: Selection() 56 | :members: 57 | :inherited-members: 58 | 59 | 60 | Postprocessing basics 61 | ---------------------- 62 | 63 | The :attr:`~Selection.post` namespace allows you to run and access all postprocessings 64 | for existing predictions. For available postprocessings, see the 65 | :py:class:`~ansys.simai.core.data.selection_post_processings.SelectionPostProcessingsMethods` 66 | class. 67 | 68 | .. code-block:: python 69 | 70 | coeffs = selection.post.global_coefficients() 71 | 72 | coeffs.data # is a list of results of each post-processings. 73 | 74 | 75 | You can use the :meth:`~ansys.simai.core.data.lists.ExportablePPList.export()` 76 | method to export results in batch for exportable postprocessings 77 | (:py:class:`~ansys.simai.core.data.post_processings.GlobalCoefficients` 78 | and :py:class:`~ansys.simai.core.data.post_processings.SurfaceEvolution` instances): 79 | 80 | .. code-block:: python 81 | 82 | selection.post.surface_evolution(axis="x", delta=13.5).export("xlsx").download( 83 | "/path/to/file.xlsx" 84 | ) 85 | 86 | Note that a CSV export generates a ZIP file containing multiple CSV files. 87 | You can read them directly using Python's `zipfile` 88 | module: 89 | 90 | .. code-block:: python 91 | 92 | import zipfile 93 | import csv 94 | from io import TextIOWrapper 95 | 96 | data = selection.post.global_coefficients().export("csv.zip").in_memory() 97 | 98 | with zipfile.ZipFile(data) as archive: 99 | csv_data = csv.reader(TextIOWrapper(archive.open("Global_Coeffs.csv"))) 100 | 101 | # or with pandas: 102 | import pandas as pd 103 | 104 | df_geom = pd.read_csv(archive.open("Geometries.csv")) 105 | 106 | 107 | You can download binary postprocessings results by looping on the list: 108 | 109 | .. code-block:: python 110 | 111 | for vtu in selection.post.volume_vtu(): 112 | vtu.data.download(f"/path/to/vtu_{vtu.id}") 113 | 114 | 115 | Postprocessing API reference 116 | ----------------------------- 117 | 118 | .. py:module:: ansys.simai.core.data.selection_post_processings 119 | 120 | .. autoclass:: SelectionPostProcessingsMethods() 121 | :members: 122 | :inherited-members: 123 | 124 | 125 | Collections 126 | ----------- 127 | 128 | .. py:module:: ansys.simai.core.data.lists 129 | 130 | .. autoclass:: PPList() 131 | :members: 132 | 133 | .. autoclass:: ExportablePPList() 134 | :members: 135 | 136 | 137 | Helpers 138 | ------- 139 | 140 | .. py:module:: ansys.simai.core.data.downloads 141 | 142 | .. autoclass:: DownloadableResult() 143 | :members: 144 | -------------------------------------------------------------------------------- /doc/source/api_reference/training_data.rst: -------------------------------------------------------------------------------- 1 | .. _training_data: 2 | 3 | TrainingData 4 | ============ 5 | 6 | .. py:module:: ansys.simai.core.data.training_data 7 | 8 | A :class:`TrainingData` instance is a 9 | collection of :class:`TrainingDataPart` 10 | instances representing a prediction that can be used as input for the training of models. 11 | 12 | Directory 13 | --------- 14 | 15 | .. autoclass:: TrainingDataDirectory() 16 | :members: 17 | 18 | Model 19 | ----- 20 | 21 | .. autoclass:: TrainingData() 22 | :members: 23 | :inherited-members: 24 | -------------------------------------------------------------------------------- /doc/source/api_reference/training_data_parts.rst: -------------------------------------------------------------------------------- 1 | .. _training_data_parts: 2 | 3 | TrainingDataParts 4 | ================= 5 | 6 | .. py:module:: ansys.simai.core.data.training_data_parts 7 | 8 | A :class:`TrainingDataPart` instance 9 | is a singular file that is part of a :class:`~ansys.simai.core.data.training_data.TrainingData` 10 | instance. 11 | 12 | Directory 13 | --------- 14 | 15 | .. autoclass:: TrainingDataPartDirectory 16 | :members: 17 | 18 | Model 19 | ----- 20 | 21 | .. autoclass:: TrainingDataPart 22 | :members: 23 | :inherited-members: 24 | -------------------------------------------------------------------------------- /doc/source/api_reference/workspaces.rst: -------------------------------------------------------------------------------- 1 | .. _workspaces: 2 | 3 | Workspaces 4 | ========== 5 | 6 | .. py:module:: ansys.simai.core.data.workspaces 7 | 8 | Workspaces are a set of specific geometries, predictions, and postprocessings. 9 | Each workspace uses a specific kernel. 10 | 11 | You use the :meth:`SimAIClient.set_current_workspace()` 12 | method to set the workspace that the client is configured for. 13 | 14 | Directory 15 | --------- 16 | 17 | .. autoclass:: WorkspaceDirectory() 18 | :members: 19 | 20 | Model 21 | ----- 22 | 23 | .. autoclass:: Workspace() 24 | :members: 25 | :inherited-members: 26 | 27 | ModelManifest 28 | ------------- 29 | 30 | .. autoclass:: ModelManifest() 31 | :members: 32 | -------------------------------------------------------------------------------- /doc/source/changelog.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | .. _changelog: 4 | 5 | .. include:: ../../CHANGELOG.rst 6 | -------------------------------------------------------------------------------- /doc/source/examples/index.rst: -------------------------------------------------------------------------------- 1 | .. _ref_examples: 2 | 3 | ======== 4 | Examples 5 | ======== 6 | 7 | 8 | .. include:: ../_examples/pysimai_ex/index.rst 9 | :start-line: 2 10 | -------------------------------------------------------------------------------- /doc/source/examples/pysimai_ex/00-model_configuration_reuse.py: -------------------------------------------------------------------------------- 1 | """.. _ref_model_configuration_reuse: 2 | 3 | Model configuration reuse 4 | ========================= 5 | 6 | This example demonstrates how to retrieve the latest model configuration 7 | of a project and use it to launch a model build in another project. 8 | 9 | """ 10 | 11 | ############################################################################### 12 | # Import necessary libraries 13 | # -------------------------- 14 | 15 | import ansys.simai.core as asc 16 | 17 | simai = asc.from_config() 18 | 19 | ############################################################################### 20 | # Create a project and allocate training data 21 | # ------------------------------------------- 22 | # Define the project name. 23 | new_project_name = "new-project" 24 | 25 | ############################################################################### 26 | # Create the project. 27 | new_project = simai.projects.create(new_project_name) 28 | 29 | ############################################################################### 30 | # Set the names of the data samples to be associated with the created project. 31 | training_samples_name = [ 32 | "TrainingData_001", 33 | "TrainingData_002", 34 | "TrainingData_003", 35 | "TrainingData_004", 36 | ] 37 | 38 | ############################################################################### 39 | # Retrieve the desired training data samples and associate them with 40 | # the new project. 41 | for td_name in training_samples_name: 42 | filt = {"name": td_name} 43 | td = simai.training_data.list(filters=filt) 44 | td[0].add_to_project(new_project) 45 | 46 | ############################################################################### 47 | # Select a model configuration and associate it with the newly created project 48 | # ---------------------------------------------------------------------------- 49 | # Retrieve the model configuration from another project that you wish to reuse. 50 | old_project = "old-ps" 51 | my_project = simai.projects.get(name=old_project) 52 | 53 | last_build_config = my_project.last_model_configuration 54 | 55 | ############################################################################### 56 | # If the new project meets the requirements for training, associate 57 | # the project's ID with the configuration and launch a model build. 58 | if new_project.is_trainable(): 59 | # Assign the new project's ID to the configuration to transfer the 60 | # configuration from the old project to the new one 61 | last_build_config.project_id = new_project.id 62 | 63 | # Launch a model build for the new project 64 | new_model = simai.models.build(last_build_config) 65 | -------------------------------------------------------------------------------- /doc/source/examples/pysimai_ex/01-model_recomputation.py: -------------------------------------------------------------------------------- 1 | """ 2 | .. _ref_model_recomputation: 3 | 4 | Model recomputation 5 | =================== 6 | 7 | This example demonstrates how to relaunch a model build using the latest 8 | model configuration in a same project. 9 | 10 | """ 11 | 12 | ############################################################################### 13 | # Import necessary libraries 14 | # -------------------------- 15 | 16 | import ansys.simai.core as asc 17 | 18 | simai = asc.from_config() 19 | 20 | ############################################################################### 21 | # Get the project from the server 22 | # ------------------------------- 23 | 24 | my_project = simai.projects.get(name="old-ps") 25 | 26 | ############################################################################### 27 | # Get the model configuration 28 | # --------------------------- 29 | # Get the latest model configuration of the project. 30 | 31 | last_build_config = my_project.last_model_configuration 32 | 33 | ############################################################################### 34 | # Verify the project requirements 35 | # ------------------------------- 36 | # Verify that the project meets the requirements for training (model building). 37 | 38 | is_trainable_check = my_project.is_trainable() 39 | 40 | ############################################################################### 41 | # If the project met the requirements, launch a model build. 42 | # Otherwise, print the reasons the project does not meet the requirements. 43 | if is_trainable_check: 44 | new_model = simai.models.build(last_build_config) 45 | else: 46 | print(is_trainable_check.reason) 47 | -------------------------------------------------------------------------------- /doc/source/examples/pysimai_ex/02-subset_assignment.py: -------------------------------------------------------------------------------- 1 | """ 2 | .. _ref_subset_assignment: 3 | 4 | Subset assignment 5 | ================= 6 | 7 | This example demonstrates how to assign a subset 8 | to a training data. 9 | 10 | """ 11 | 12 | ############################################################################### 13 | # Import necessary libraries 14 | # -------------------------- 15 | 16 | import ansys.simai.core as asc 17 | 18 | simai = asc.from_config() 19 | 20 | ############################################################################### 21 | # Select a training data 22 | # ---------------------- 23 | # Example of a training_data_id associated with a project_id. 24 | 25 | training_data_id = "k4z77qzq" 26 | project_id = "k9756vw0" 27 | 28 | ############################################################################### 29 | # Get subset assignment 30 | # --------------------- 31 | # Get and print the current subset assigned for this training_data_id. 32 | 33 | current_subset = simai.training_data.get(id=training_data_id).get_subset(project=project_id) 34 | print(current_subset) 35 | 36 | ############################################################################### 37 | # Assign a new subset (two options) 38 | # --------------------------------- 39 | # Manually assign a new subset to the training data. 40 | 41 | simai.training_data.get(id=training_data_id).assign_subset(project=project_id, subset="Test") 42 | 43 | ############################################################################### 44 | # Alternatively, use SubsetEnum to assign a valid enum value to the training data. 45 | 46 | from ansys.simai.core.data.types import SubsetEnum 47 | 48 | simai.training_data.get(id=training_data_id).assign_subset( 49 | project=project_id, subset=SubsetEnum.TEST 50 | ) 51 | -------------------------------------------------------------------------------- /doc/source/examples/pysimai_ex/03-list_based_subset_assignment.py: -------------------------------------------------------------------------------- 1 | """ 2 | .. _ref_list_based_subset_assignment: 3 | 4 | List-based subset assignment 5 | ============================ 6 | 7 | This example demonstrates how to distribute your dataset between Test 8 | and Training subsets using lists. 9 | 10 | """ 11 | 12 | ############################################################################### 13 | # Import necessary libraries 14 | # -------------------------- 15 | 16 | import ansys.simai.core 17 | 18 | ############################################################################### 19 | # Create lists 20 | # ------------ 21 | # List the data to be used for the test set. 22 | 23 | # CAUTION: 24 | # All training data that are not included into the following list will be 25 | # assigned the training subset 26 | TEST_LIST = ["My_td_1", "My_td_2"] 27 | 28 | ############################################################################### 29 | # Connect to the platform 30 | # ------------------------ 31 | # Connect to the SimAI platform. Refer to the :ref:`anchor-credentials` 32 | # section of the documentation to adapt the connection type. 33 | 34 | simai = ansys.simai.core.SimAIClient(organization="My_organization_name") 35 | project = simai.projects.get(name="My_project_name") 36 | 37 | ############################################################################### 38 | # Assign subsets 39 | # -------------- 40 | # Assign a subset to each dataset (list) you created. 41 | 42 | td_list = project.data 43 | for td in td_list: 44 | if td.name in TEST_LIST: 45 | td.assign_subset(project, "Test") 46 | else: 47 | td.assign_subset(project, "Training") 48 | -------------------------------------------------------------------------------- /doc/source/examples/pysimai_ex/README.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | PySimAI examples 4 | ---------------- 5 | 6 | This section provides a collection of practical script examples illustrating SimAI functionalities and use cases. 7 | They serve as a reference guide for users to implement similar solutions in their projects. 8 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | .. _index: 2 | 3 | PySimAI documentation 4 | ===================== 5 | 6 | Release v\ |version| (:ref:`Changelog `) 7 | 8 | PySimAI is part of the `PyAnsys `_ ecosystem that allows you to use SimAI within 9 | a Python environment of your choice in conjunction with other PyAnsys libraries and external Python 10 | libraries. With PySimAI, you can manage and access your data on the platform from within Python apps and 11 | scripts. 12 | 13 | The following illustration depicts the Ansys SimAI platform user workflow. 14 | 15 | .. image:: ../source/_static/I_Workflow_70pct.png 16 | :align: center 17 | 18 | For more information, see the `Ansys SimAI User's guide `_ 19 | 20 | .. grid:: 3 21 | 22 | .. grid-item-card:: :octicon:`tools` User guide 23 | :link: ref_user_guide 24 | :link-type: ref 25 | 26 | Guides on how to achieve specific tasks with PySimAI. 27 | 28 | .. grid-item-card:: :octicon:`file-code` API reference 29 | :link: api_reference 30 | :link-type: ref 31 | 32 | Describes the public Python classes, methods, and functions. 33 | 34 | .. grid-item-card:: :octicon:`play` Examples 35 | :link: examples/index 36 | :link-type: doc 37 | 38 | A collection of examples demonstrating the capabilities of PySimAI. 39 | 40 | 41 | Requirements 42 | ------------ 43 | 44 | PySimAI requires Python 3.9 or later. 45 | 46 | 47 | .. toctree:: 48 | :maxdepth: 1 49 | :hidden: 50 | 51 | Home 52 | user_guide 53 | api_reference 54 | examples/index 55 | -------------------------------------------------------------------------------- /doc/source/user_guide.rst: -------------------------------------------------------------------------------- 1 | .. _ref_user_guide: 2 | 3 | User guide 4 | ========== 5 | 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | 10 | user_guide/installation 11 | user_guide/configuration 12 | user_guide/config_file 13 | user_guide/proxy 14 | user_guide/pysimai_ug/index 15 | -------------------------------------------------------------------------------- /doc/source/user_guide/config_file.rst: -------------------------------------------------------------------------------- 1 | .. _config_file: 2 | 3 | Configuration file 4 | ================== 5 | 6 | To create a :class:`~ansys.simai.core.client.SimAIClient` 7 | instance from a configuration file, you use the 8 | :py:meth:`~ansys.simai.core.client.SimAIClient.from_config` method: 9 | 10 | .. code-block:: python 11 | 12 | simai = ansys.simai.core.from_config() 13 | 14 | Location 15 | -------- 16 | 17 | If no path is given, the :class:`~ansys.simai.core.client.SimAIClient` 18 | instance looks at default locations. These locations differ according to 19 | your operating system. 20 | 21 | **Linux/MacOS** 22 | 23 | For UNIX systems, the default locations are, in order: 24 | 25 | * ``$XDG_CONFIG_HOME/ansys_simai.conf`` 26 | * ``$XDG_CONFIG_HOME/ansys/simai.conf`` 27 | * ``~/.ansys_simai.conf`` 28 | * ``~/.ansys/simai.conf`` 29 | * ``/etc/ansys_simai.conf`` 30 | * ``/etc/ansys/simai.conf`` 31 | 32 | .. note :: 33 | 34 | The first location found is used. ``$XDG_CONFIG_HOME`` defaults to ``~/.config``. 35 | 36 | **Windows XP** 37 | 38 | * ``C:\Documents and Settings\\Local Settings\Application Data\Ansys\simai.conf`` 39 | 40 | **Windows 7 to 11** 41 | 42 | * ``C:\Users\\AppData\Roaming\Ansys\simai.conf`` 43 | 44 | Optionally, you can specify the path yourself: 45 | 46 | .. code-block:: python 47 | 48 | simai = ansys.simai.core.from_config(path="/path/to/my/config") 49 | 50 | Content 51 | ------- 52 | 53 | You write the configuration file in `TOML `_. 54 | From this file, you can pass parameters for configuring 55 | the :class:`~ansys.simai.core.client.SimAIClient` instance (see :ref:`configuration`). 56 | 57 | 58 | Example 59 | """"""" 60 | 61 | .. code-block:: TOML 62 | 63 | [default] 64 | organization = "company" 65 | 66 | [default.credentials] 67 | username = "user@company.com" 68 | password = "hunter12" 69 | totp_enabled = true 70 | 71 | 72 | Profiles 73 | -------- 74 | 75 | The :class:`~ansys.simai.core.client.SimAIClient` instance supports having multiple 76 | configurations in a single file through profiles, which are loaded like this: 77 | 78 | .. code-block:: TOML 79 | 80 | [default] 81 | organization = "company" 82 | workspace = "my-usual-workspace" 83 | 84 | [alternative] 85 | organization = "company" 86 | workspace = "some-other-workspace" 87 | project = "red herring" 88 | 89 | .. code-block:: python 90 | 91 | simai = ansys.simai.core.from_config(profile="alternative") 92 | -------------------------------------------------------------------------------- /doc/source/user_guide/configuration.rst: -------------------------------------------------------------------------------- 1 | .. _configuration: 2 | .. py:module:: ansys.simai.core.utils.configuration 3 | 4 | Client configuration 5 | ==================== 6 | 7 | Where to start 8 | -------------- 9 | 10 | You start by creating a :class:`~ansys.simai.core.client.SimAIClient` 11 | instance: 12 | 13 | .. code-block:: python 14 | 15 | import ansys.simai.core 16 | 17 | simai = ansys.simai.core.SimAIClient(organization="my-company") 18 | 19 | 20 | As demonstrated in the preceding code, you configure the instance by 21 | passing the required parameters on client creation. You are prompted 22 | for any missing parameters. 23 | 24 | Once you understand how creating an instance works, you can look into using a 25 | :ref:`configuration file` for creating a client instance. 26 | 27 | Configuration options 28 | --------------------- 29 | 30 | Descriptions follow of all configuration options for the :class:`~ansys.simai.core.client.SimAIClient` 31 | class: 32 | 33 | .. autopydantic_model:: ClientConfig 34 | :model-show-config-summary: False 35 | :model-show-validator-summary: False 36 | :model-show-json: False 37 | :model-show-field-summary: False 38 | :model-show-validator-members: False 39 | :field-list-validators: False 40 | 41 | 42 | .. _anchor-credentials: 43 | 44 | Credentials 45 | ----------- 46 | 47 | To use the SimAI API, your :class:`~ansys.simai.core.client.SimAIClient` 48 | instance must be authenticated. By default, you are prompted to log in 49 | via your web browser. However, you can pass your credentials as parameters 50 | on client creation: 51 | 52 | .. code-block:: python 53 | 54 | simai = ansys.simai.core.SimAIClient( 55 | organization="company", 56 | credentials={ 57 | # neither of these are required, but if they are missing you will be 58 | # prompted to input them 59 | "username": "user@company.com", 60 | "password": "hunter12", 61 | }, 62 | ) 63 | 64 | Credential options 65 | ------------------ 66 | 67 | Descriptions follow of all credential options for the :class:`~ansys.simai.core.client.SimAIClient` 68 | class: 69 | 70 | .. autopydantic_model:: Credentials 71 | :model-show-config-summary: False 72 | :model-show-validator-summary: False 73 | :model-show-validator-members: False 74 | :model-show-json: False 75 | :field-list-validators: False 76 | 77 | .. _Interactive mode: 78 | 79 | Interactive mode 80 | ------------------ 81 | 82 | When the property `interactive` is set to `true`, the users are prompted for the missing configuration 83 | properties. 84 | When the property is `false`, the interactive mode is turned off, and errors would be raised 85 | in case of missing configuration properties. 86 | Default behavior is `interactive=true`. 87 | 88 | It is important to note that login through web browser is turned off and credentials become required when `interactive=false`. 89 | This means that if the credentials are missing, the users won't be prompted to enter them 90 | from the terminal, and an error would be raised instead. 91 | -------------------------------------------------------------------------------- /doc/source/user_guide/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | .. note:: 5 | PySimAI requires Python 3.9 or later. 6 | 7 | 8 | Install PySimAI with this command: 9 | 10 | .. code-block:: bash 11 | 12 | pip install ansys-simai-core --upgrade 13 | 14 | Use this same command every time you want to update PySimAI. -------------------------------------------------------------------------------- /doc/source/user_guide/proxy.rst: -------------------------------------------------------------------------------- 1 | .. _proxy: 2 | 3 | Working behind a proxy 4 | ====================== 5 | 6 | By default, the SimAI client attempts to get your proxy configuration, if any, from your system. 7 | 8 | SimAI client configuration 9 | ++++++++++++++++++++++++++ 10 | 11 | You can manually set a proxy when creating the :ref:`SimAIClient` 12 | instance: 13 | 14 | .. code-block:: python 15 | 16 | import ansys.simai.core 17 | 18 | simai = ansys.simai.core.SimAIClient(https_proxy="http://company_proxy:3128") 19 | 20 | Alternatively, you can store the proxy information in your :ref:`configuration file`. 21 | 22 | .. note:: 23 | Setting this parameter overrides the default configuration retrieved from your system. 24 | 25 | 26 | Troubleshooting 27 | ~~~~~~~~~~~~~~~ 28 | 29 | If you get an error of the type ``ProxyError([...], SSLCertVerificationError([...]``, 30 | it is likely that your proxy setup looks like ``|computer|<-https->|proxy|<-https->|internet|``. 31 | Because your web browser uses a special 32 | `proxy auto-configuration `_ file, the 33 | proxy is not trusted by your computer. 34 | 35 | There are multiple ways to fix this issue: 36 | 37 | 1. Try ``tls_ca_bundle="system"`` (requires ``python>=3.10``, see :ref:`configuration`). 38 | 2. Extract the required CA certificate: 39 | 40 | a. Extract the certificates used by your company-configured browser on ``https://simai.ansys.com``. 41 | b. Set ``tls_ca_bundle`` (or the ``REQUESTS_CA_BUNDLE`` environment variable): 42 | 43 | .. code-block:: TOML 44 | 45 | [default] 46 | organization = "company" 47 | tls_ca_bundle = "/home/username/Documents/my_company_proxy_ca_bundle.pem" 48 | 3. As a temporary last resort, one can use ``tls_ca_bundle="unsecure-none"`` (contact your IT department). 49 | -------------------------------------------------------------------------------- /doc/source/user_guide/pysimai_ug/best_practices.rst: -------------------------------------------------------------------------------- 1 | .. _best_practices: 2 | 3 | Best practices 4 | ============== 5 | 6 | Asynchronicity 7 | -------------- 8 | 9 | While the SimAI client doesn't use async/await mechanics, it is somewhat asynchronous in nature. 10 | While uploading geometries is a blocking method, running a prediction or a postprocessing returns 11 | the created object immediately, before the result is computed on the servers or available locally. 12 | This behavior makes it possible to request that multiple computations be run on the SimAI platform 13 | without waiting for any of the data to be available. 14 | 15 | To wait for an object to be fully available, you can call the ``wait()`` method on the object. 16 | For example, you can call the :meth:`Prediction.wait()` 17 | method on a prediction. Or, you can call the global :meth:`SimAIClient.wait()` 18 | method to wait for all requests to complete. 19 | 20 | Alternatively, you can try to access the object's data, in which case the SimAI client automatically 21 | waits for the data to be ready if needed. Because of this behavior, when running a large number of 22 | computations, you should send all requests before accessing any of the data. 23 | 24 | This example requests the predictions and postprocessings sequentially, which requires waiting 25 | for the data to be available and used before requesting the next one. 26 | 27 | .. code-block:: python 28 | :name: sequential-way 29 | 30 | import ansys.simai.core 31 | 32 | simai = ansys.simai.core.from_config() 33 | speeds = [5.9, 5.10, 5.11] 34 | 35 | for geom in simai.geometries.list(): 36 | for vx in speeds: 37 | # Run prediction 38 | pred = geom.run_prediction(Vx=vx) 39 | # Request global coefficients postprocessing 40 | # Because you are accessing the data, you must wait for the computation to finish 41 | coeffs = pred.post.global_coefficients().data 42 | # do something with the data 43 | print(coeffs) 44 | 45 | 46 | This more efficient example requests all the predictions and postprocessings right away 47 | and then processes the data once they are all available. 48 | 49 | .. code-block:: python 50 | :name: requests-first 51 | 52 | import ansys.simai.core 53 | 54 | simai = ansys.simai.core.from_config() 55 | speeds = [5.9, 5.10, 5.11] 56 | predictions = [] 57 | 58 | for geom in simai.geometries.list(): 59 | for vx in speeds: 60 | # Run prediction 61 | pred = geom.run_prediction(Vx=vx) 62 | # Request global coefficients postprocessing 63 | # Because you are not accessing the data, you are not blocked 64 | pred.post.global_coefficients() 65 | predictions.append(pred) 66 | 67 | simai.wait() # Wait for all objects requested locally to be complete 68 | for pred in predictions: 69 | # do something with the data 70 | print(pred.post.global_coefficients().data) 71 | 72 | -------------------------------------------------------------------------------- /doc/source/user_guide/pysimai_ug/building_a_model.rst: -------------------------------------------------------------------------------- 1 | Building a model 2 | ================ 3 | 4 | .. _building_a_model: 5 | 6 | .. note:: 7 | 8 | Building a model with PySimAI is still experimental and subject to API changes. 9 | 10 | Rebuilding a model using the last configuration of a project is supported for models created 11 | after v0.1.5 (April 15, 2024). 12 | 13 | SimAI allows you to build AI models using past simulation data. This first step to building such models is to upload 14 | your simulation data into a global pool of :class:`training data` instances. 15 | Then, you assign the imported data to different :class:`Project` instances, 16 | which you will eventually configure in order to build your AI model. 17 | 18 | Create a project and upload data 19 | -------------------------------- 20 | 21 | #. Create a :class:`~ansys.simai.core.client.SimAIClient` instance:: 22 | 23 | import ansys.simai.core 24 | 25 | simai = ansys.simai.core.SimAIClient() 26 | 27 | You are prompted for your credentials. 28 | 29 | If desired, you can create an instance using a configuration file. For more 30 | information, see :ref:`configuration`. 31 | 32 | #. Create a 33 | :class:`TrainingData` instance 34 | and upload your simulation data into it:: 35 | 36 | td = simai.training_data.create("my-first-data") 37 | td.upload_folder("/path/to/folder/where/files/are/stored") 38 | 39 | #. Create a project:: 40 | 41 | project = simai.projects.create("my-first-project") 42 | 43 | #. Associate the created training data with the created project:: 44 | 45 | td.add_to_project(project) 46 | 47 | Your project is created and your simulation data is associated with it. You can now configure and build your AI model. 48 | 49 | Configure and build the model 50 | ----------------------------- 51 | 52 | #. Import the modules related to model building:: 53 | 54 | from ansys.simai.core.data.model_configuration import ( 55 | DomainOfAnalysis, 56 | ModelConfiguration, 57 | ModelInput, 58 | ModelOutput, 59 | ) 60 | 61 | #. Set the inputs (:class:`ModelInput`) and outputs (:class:`ModelOutput`) of the model:: 62 | 63 | model_input = ModelInput(surface=["wallShearStress"], boundary_conditions=["Vx"]) 64 | 65 | model_output = ModelOutput(surface=["alpha.water"], volume=["p", "p_rgh"]) 66 | 67 | #. Set the Global Coefficients:: 68 | 69 | global_coefficients = [('min("alpha.water")', "minalpha")] 70 | 71 | #. Set the Domain of Analysis of the model using the :class:`DomainOfAnalysis` instance:: 72 | 73 | doa = DomainOfAnalysis( 74 | length=("relative_to_min", 15.321, 183.847), 75 | width=("relative_to_min", 1.034, 12.414), 76 | height=("relative_to_min", 2.046, 24.555), 77 | ) 78 | 79 | 80 | #. Configure the model using the :class:`ModelConfiguration` instance:: 81 | 82 | mdl_conf = ModelConfiguration( 83 | project=project, # project of the model configuration 84 | build_preset="debug", # duration of the build 85 | build_on_top=False, # build on top of previous model 86 | input=model_input, # model input 87 | output=model_output, # model output 88 | global_coefficients=global_coefficients, # Global Coefficients 89 | domain_of_analysis=doa # Domain of Analysis 90 | ) 91 | 92 | #. Verify if the project meets the requirements for training and launch a build:: 93 | 94 | if project.is_trainable(): 95 | new_model = simai.models.build(mdl_conf) 96 | 97 | Your AI model is configured and building. 98 | 99 | Learn more 100 | ---------- 101 | 102 | For more information on the actions available to you, see :ref:`training_data`, 103 | :ref:`training_data_parts`, :ref:`projects`, :ref:`model_configuration`, and :ref:`models` 104 | -------------------------------------------------------------------------------- /doc/source/user_guide/pysimai_ug/data_exploration.rst: -------------------------------------------------------------------------------- 1 | .. _data_exploration: 2 | 3 | Data exploration 4 | ================ 5 | 6 | The SimAI client provides utilities to help you run a large number of predictions and 7 | postprocessings, explore your data, and gather insights from it. 8 | 9 | Selections 10 | ---------- 11 | 12 | :ref:`selections` enable you to manipulate a large number of geometries and boundary conditions 13 | simultaneously. They also allow you to easily run many predictions or postprocessings in parallel. 14 | 15 | You create a selection by combining a list of geometries with a list of boundary conditions: 16 | 17 | .. code-block:: python 18 | 19 | import ansys.simai.core 20 | from ansys.simai.core.data.selections import Selection 21 | 22 | simai = ansys.simai.core.from_config() 23 | geometries = simai.geometries.list()[:4] 24 | boundary_conditions = [dict(Vx=vx) for vx in [12.2, 12.4, 12.6]] 25 | selection = Selection(geometries, boundary_conditions) 26 | 27 | # run all predictions 28 | selection.run_predictions() 29 | 30 | # compute all global coefficients 31 | selection.post.global_coefficients() 32 | 33 | # get all results 34 | all_selection_coefficients = [ 35 | global_coefficients.data 36 | for global_coefficients in selection.post.global_coefficients() 37 | ] 38 | 39 | To help build selections, the Sim AI client exposes two methods that are useful for 40 | different strategies: 41 | 42 | - The :meth:`geometry.sweep` method, which 43 | is described in :ref:`sweeping`. 44 | - The :meth:`GeometryDirectory.list` 45 | method, which is described in :ref:`filtering_geometries`. 46 | 47 | For more information on selections and geometry exploration methods, see :ref:`selections` 48 | and :ref:`geometries`. 49 | 50 | .. _sweeping: 51 | 52 | Sweeping 53 | -------- 54 | 55 | The :meth:`geometry.sweep` method allows you 56 | to explore the surroundings of a given geometry, which can help with local optimization or 57 | gradient descent. This method, only for numerical metadata, finds geometries that have 58 | metadata closest to the candidate geometry. 59 | 60 | .. code-block:: python 61 | 62 | geometry = simai.geometries.list()[0] 63 | neighbour_geometries = geometry.sweep(swept_metadata=["height", "length"]) 64 | 65 | # with which a selection can be built: 66 | selection = Selection(neighbour_geometries, [dict(Vx=13.4)]) 67 | 68 | .. _filtering_geometries: 69 | 70 | Filtering geometries 71 | -------------------- 72 | 73 | The :meth:`GeometryDirectory.list` method 74 | allows you to take a more brute-force approach. With this method, you can select large swaths of 75 | geometries with range filters. 76 | 77 | .. code-block:: python 78 | 79 | from ansys.simai.core.data.types import Range 80 | 81 | geometries = simai.geometries.list(filters={"SINK": Range(-5.1, -4.8)}) 82 | 83 | -------------------------------------------------------------------------------- /doc/source/user_guide/pysimai_ug/index.rst: -------------------------------------------------------------------------------- 1 | .. _ref_pysimai_guide: 2 | 3 | PySimAI guide 4 | ============== 5 | 6 | These sections teach you how to use particular features of PySimAI. 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | 11 | building_a_model 12 | data_exploration 13 | best_practices -------------------------------------------------------------------------------- /doc/styles/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !config 3 | !config/vocabularies 4 | !config/vocabularies/** 5 | !.gitignore -------------------------------------------------------------------------------- /doc/styles/config/vocabularies/ANSYS/accept.txt: -------------------------------------------------------------------------------- 1 | (?i)ansys 2 | (?i)simai 3 | [Aa]synchronicity 4 | csv 5 | DOE 6 | https_proxy 7 | [Nn]amespace 8 | [Pp]ostprocessing 9 | PySimAI 10 | zipfile 11 | TrainingData 12 | CheckGlobalCoefficient 13 | CheckGlobalCoefficientDirectory 14 | ComputeGlobalCoefficient 15 | ComputeGlobalCoefficientDirectory 16 | TrialRuns 17 | GlobalCoefficientDefinition 18 | DomainAxisDefinition 19 | DomainOfAnalysis 20 | ModelConfiguration 21 | ModelConfiguration 22 | ModelManifest 23 | ModelOutput -------------------------------------------------------------------------------- /doc/styles/config/vocabularies/ANSYS/reject.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansys/pysimai/a7c55f0399a895014a9b4edbb5025297c04d2e03/doc/styles/config/vocabularies/ANSYS/reject.txt -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "ansys-simai-core" 3 | version = "0.3.0" 4 | description = "A python wrapper for Ansys SimAI" 5 | authors = [ 6 | {name = "ANSYS, Inc.", email = "pyansys.core@ansys.com"}, 7 | ] 8 | maintainers = [ 9 | {name = "PyAnsys developers", email = "pyansys.maintainers@ansys.com"}, 10 | ] 11 | license = {file = "LICENSE"} 12 | readme = "README.rst" 13 | requires-python = ">=3.9,<4" 14 | classifiers = [ 15 | "Development Status :: 3 - Alpha", 16 | "Intended Audience :: Science/Research", 17 | "Topic :: Scientific/Engineering :: Artificial Intelligence", 18 | "Topic :: Scientific/Engineering :: Information Analysis", 19 | "Topic :: Scientific/Engineering :: Physics", 20 | "License :: OSI Approved :: MIT License", 21 | "Operating System :: OS Independent", 22 | "Programming Language :: Python :: 3.9", 23 | "Programming Language :: Python :: 3.10", 24 | "Programming Language :: Python :: 3.11", 25 | "Programming Language :: Python :: 3.12", 26 | "Programming Language :: Python :: 3.13" 27 | ] 28 | dependencies = [ 29 | "niquests>=2.31.0", 30 | "requests_toolbelt>=1.0.0,<2", 31 | "tomli>=2.0.1,<3", 32 | "pydantic>=2.5.1,<3", 33 | "semver>=3.0.2", 34 | "sseclient-py>=1.8.0,<3", 35 | "wakepy>=0.8.0", 36 | "tqdm>=4.66.1", 37 | "filelock>=3.10.7", 38 | "typing_extensions>=4.12.0", 39 | "truststore>=0.10.0; python_version >= '3.10'", 40 | ] 41 | 42 | [dependency-groups] 43 | dev = [ 44 | "ruff>=0.2", 45 | "blacken-docs", 46 | "pre-commit", 47 | "poethepoet", 48 | { include-group = "docs" }, 49 | { include-group = "tests" }, 50 | ] 51 | docs = [ 52 | "sphinx>=7.2.0", 53 | "autodoc-pydantic>=2.0.0", 54 | "ansys-sphinx-theme>=0.12.5", 55 | "sphinx-gallery", 56 | "blacken-docs>=1.16.0", 57 | "sphinx_design>=0.6.1" 58 | ] 59 | tests = [ 60 | "pytest>=7.0.0", 61 | "pytest-cov>=4.1.0", 62 | "pytest-asyncio>=0.21.0", 63 | "pytest-mock>=3.6.1", 64 | "responses>=0.23.0", 65 | "coverage[toml]>=7.0", 66 | "pyjwt>=2.4.0" 67 | ] 68 | 69 | [project.urls] 70 | Bugs = "https://github.com/ansys/pysimai/issues" 71 | Documentation = "https://simai.docs.pyansys.com" 72 | Source = "https://github.com/ansys/pysimai" 73 | Discussions = "https://github.com/ansys/pysimai/discussions" 74 | Releases = "https://github.com/ansys/pysimai/releases" 75 | 76 | [tool.poe.tasks] 77 | lint = "pre-commit run --all-files" 78 | test = "pytest --cov=ansys --cov-report=term --cov-report=html:./cov/html tests -vv" 79 | doc = "sphinx-build doc/source _build/html --color -vW -bhtml" 80 | 81 | [tool.ruff] 82 | line-length = 100 83 | target-version = "py39" 84 | 85 | [tool.ruff.lint] 86 | select = [ 87 | # "A", # flake8-builtins 88 | # "ANN", # flake8-annotations 89 | # "ARG", # flake8-unused-arguments 90 | "ASYNC", # flake8-async 91 | "B", # flake8-bugbear 92 | # "BLE", # flake8-blind-except 93 | "C4", # flake8-comprehensions 94 | # "C90", # mccabe 95 | "CPY", # flake8-copyright 96 | "D", # pydocstyle 97 | # "DJ", # flake8-django 98 | # "DTZ", # flake8-datetimez 99 | "E", # pycodestyle errors 100 | # "EM", # flake8-errmsg 101 | "ERA", # eradicate 102 | "EXE", # flake8-executable 103 | "F", # pyflakes 104 | # "FA", # flake8-future-annotations 105 | # "FBT", # flake8-boolean-trap 106 | # "FIX", # flake8-fixme 107 | "FLY", # flying 108 | "FURB", # refurb 109 | # "G", # flake8-logging-format 110 | "I", # isort 111 | "ICN", # flake8-import-conventions 112 | "ISC", # flake8-implicit-str-concat 113 | "INP", # flake8-no-pep420 114 | "LOG", # flake8-logging 115 | # "N", # pep8-naming 116 | # "PD", # pandas-vet 117 | "PIE", # flake8-pie 118 | "PLC", # pylint - convention 119 | "PLE", # pylint - error 120 | # "PLR", # pylint - refactor 121 | "PLW", # pylint - warning 122 | # "PT", # flake8-pytest-style 123 | # "PTH", # flake8-use-pathlib 124 | "PYI", # flake8-pyi 125 | "Q", # flake8-quotes 126 | # "RET", # flake8-return 127 | "RSE", # flake8-raise 128 | # "RUF", # Ruff-specific rules 129 | "S", # flake8-bandit 130 | "SIM", # flake8-simplify 131 | # "SLF", # flake8-self 132 | "SLOT", # flake8-slot 133 | "T10", # flake8-debugger 134 | "T20", # flake8-print 135 | "TCH", # flake8-type-checking 136 | # "TD", # flake8-todos 137 | "TID", # flake8-tidy-imports 138 | # "TRIO", # flake8-trio 139 | # "TRY", # tryceratops 140 | # "UP", # pyupgrade 141 | "W", # pycodestyle - warning 142 | "YTT", # flake8-2020 143 | ] 144 | ignore = [ 145 | "D100", # pydocstyle - missing docstring in public module 146 | "D101", # pydocstyle - missing docstring in public class 147 | "D103", # pydocstyle - missing docstring in public function 148 | "D104", # pydocstyle - missing docstring in public package 149 | "D105", # pydocstyle - missing docstring in magic method 150 | "D106", # pydocstyle - missing docstring in public nested class 151 | "D107", # pydocstyle - missing docstring in __init__ 152 | "D202", # pydocstyle - no blank lines allowed after function docstring 153 | "D205", # pydocstyle - 1 blank line required between summary line and description 154 | "E501", # pycodestyle line too long, handled by formatting 155 | "ISC001", # Ruff formatter incompatible 156 | "S101", # flake8-bandit - use of assert 157 | ] 158 | 159 | [tool.ruff.lint.per-file-ignores] 160 | "tests/*" = ["D", "INP001"] 161 | "src/ansys/simai/core/{api,utils}/*" = ["D102"] 162 | "doc/source/examples/*" = ["D", "INP001", "T", "E402"] 163 | 164 | [tool.ruff.lint.pydocstyle] 165 | convention = "google" 166 | 167 | [tool.ruff.lint.isort] 168 | known-first-party = ["ansys.simai.core"] 169 | 170 | 171 | [tool.coverage.run] 172 | source = ["ansys.simai.core"] 173 | [tool.coverage.report] 174 | show_missing = true 175 | exclude_also = ["if TYPE_CHECKING:"] 176 | 177 | 178 | [tool.pytest.ini_options] 179 | asyncio_default_fixture_loop_scope = "function" 180 | 181 | 182 | [tool.hatch.build.targets.wheel] 183 | packages = ["src/ansys"] 184 | 185 | [build-system] 186 | requires = ["hatchling"] 187 | build-backend = "hatchling.build" 188 | -------------------------------------------------------------------------------- /src/ansys/simai/core/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | # 23 | # ruff: noqa: F401 24 | 25 | from importlib.metadata import version 26 | 27 | try: 28 | __version__ = version("ansys-simai-core") 29 | except Exception: 30 | __version__ = "n/a" 31 | 32 | import ansys.simai.core.errors 33 | from ansys.simai.core.client import SimAIClient, from_config 34 | from ansys.simai.core.data.post_processings import ( 35 | CustomVolumePointCloud, 36 | GlobalCoefficients, 37 | Slice, 38 | SurfaceEvolution, 39 | SurfaceVTP, 40 | SurfaceVTPTDLocation, 41 | VolumeVTU, 42 | ) 43 | -------------------------------------------------------------------------------- /src/ansys/simai/core/api/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/ansys/simai/core/api/client.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from ansys.simai.core.api.geometry import GeometryClientMixin 24 | from ansys.simai.core.api.optimization import OptimizationClientMixin 25 | from ansys.simai.core.api.point_cloud import PointCloudClientMixin 26 | from ansys.simai.core.api.post_processing import PostProcessingClientMixin 27 | from ansys.simai.core.api.prediction import PredictionClientMixin 28 | from ansys.simai.core.api.project import ProjectClientMixin 29 | from ansys.simai.core.api.sse import SSEMixin 30 | from ansys.simai.core.api.training_data import TrainingDataClientMixin 31 | from ansys.simai.core.api.training_data_part import TrainingDataPartClientMixin 32 | from ansys.simai.core.api.workspace import WorkspaceClientMixin 33 | 34 | 35 | class ApiClient( 36 | GeometryClientMixin, 37 | PointCloudClientMixin, 38 | OptimizationClientMixin, 39 | PostProcessingClientMixin, 40 | ProjectClientMixin, 41 | PredictionClientMixin, 42 | SSEMixin, 43 | TrainingDataClientMixin, 44 | TrainingDataPartClientMixin, 45 | WorkspaceClientMixin, 46 | ): 47 | """Provides the low-level client that handles direct communication with the server.""" 48 | -------------------------------------------------------------------------------- /src/ansys/simai/core/api/geometry.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import json 24 | import logging 25 | from typing import Any, BinaryIO, Dict, List, Optional, Union 26 | from urllib.parse import quote 27 | 28 | from ansys.simai.core.api.mixin import ApiClientMixin 29 | from ansys.simai.core.data.types import File, MonitorCallback 30 | 31 | logger = logging.getLogger(__name__) 32 | 33 | 34 | class GeometryClientMixin(ApiClientMixin): 35 | """Provides the client for the Geometry ("/geometries/") part of the API.""" 36 | 37 | def geometries(self, workspace_id: str, filters: Optional[Dict[str, Any]] = None): 38 | """Get list of all geometries.""" 39 | if filters is not None: 40 | params = {"filters": json.dumps(filters), "workspace": workspace_id} 41 | else: 42 | params = {"workspace": workspace_id} 43 | return self._get("geometries/", params=params) 44 | 45 | def get_geometry(self, geometry_id: str): 46 | """Get information on a single geometry. 47 | 48 | Args: 49 | geometry_id: ID of the geometry. 50 | """ 51 | return self._get(f"geometries/{geometry_id}") 52 | 53 | def get_geometry_by_name(self, name: str, workspace_id: str): 54 | """Get information on a single geometry by name instead of ID. 55 | 56 | Args: 57 | name: Name of the geometry. 58 | workspace_id: ID of the workspace that the geometry belongs to. 59 | """ 60 | return self._get(f"geometries/name/{quote(name)}", params={"workspace": workspace_id}) 61 | 62 | def delete_geometry(self, geometry_id: str): 63 | """Delete a single geometry. 64 | 65 | All objects associated with that geometry are also deleted. 66 | 67 | Args: 68 | geometry_id: ID of the geometry. 69 | """ 70 | # TODO: Have user confirm or delete confirmation from API ? 71 | return self._delete( 72 | f"geometries/{geometry_id}", 73 | params={"confirm": True}, 74 | return_json=False, 75 | ) 76 | 77 | def update_geometry( 78 | self, 79 | geometry_id: str, 80 | name: Optional[str] = None, 81 | metadata: Optional[dict] = None, 82 | ): 83 | """Update the information for a given geometry. 84 | 85 | Args: 86 | geometry_id: ID of the geometry. 87 | name: New name to give to the geometries. 88 | metadata: Metadata to update the geometry with. 89 | """ 90 | request_json = {} 91 | if name is not None: 92 | request_json["name"] = name 93 | if metadata is not None: 94 | request_json["metadata"] = metadata 95 | self._patch(f"geometries/{geometry_id}", json=request_json, return_json=False) 96 | 97 | def create_geometry( 98 | self, 99 | workspace_id: str, 100 | name: Optional[str] = None, 101 | extension: Optional[str] = None, 102 | metadata: Optional[Dict[str, Any]] = None, 103 | ): 104 | """Create a geometry without pushing the data. 105 | 106 | Args: 107 | workspace_id: ID of the workspace to assign the geometry to. 108 | name: Name to give to the geometry. 109 | extension: Extension to give to the file. 110 | metadata: Metadata to apply to the geometry on creation. 111 | 112 | Returns: 113 | Tuple containing the geometry object and a 'presigned post' 114 | dictionary, which contains the URL to upload the data and fields to 115 | that was included in the request. 116 | """ 117 | post_data = { 118 | "name": name, 119 | "workspace": workspace_id, 120 | "file_extension": extension, 121 | } 122 | if metadata: 123 | post_data.update({"metadata": json.dumps(metadata)}) 124 | response = self._post( 125 | "geometries/", 126 | json=post_data, 127 | ) 128 | return (response["geometry"], response["upload_id"]) 129 | 130 | def complete_geometry_upload(self, id: str, upload_id: str, parts: List[Dict[str, Any]]): 131 | self._post(f"geometries/{id}/complete", json={"upload_id": upload_id, "parts": parts}) 132 | 133 | def download_geometry( 134 | self, 135 | geometry_id: str, 136 | file: Optional[File] = None, 137 | monitor_callback: Optional[MonitorCallback] = None, 138 | ) -> Union[None, BinaryIO]: 139 | """Download the input geometry into the file at the given path. 140 | 141 | Args: 142 | geometry_id: ID of the geometry to download. 143 | file: Binary file-object or the path of the file to put the content into. 144 | monitor_callback: Function or method to pass the ``bytes_read`` delta to. 145 | This delta can be used to monitor progress. 146 | """ 147 | return self.download_file(f"geometries/{geometry_id}/download", file, monitor_callback) 148 | 149 | def get_geometry_predictions(self, geometry_id: str): 150 | """Get predictions associated with a geometry. 151 | 152 | Args: 153 | geometry_id: ID of the geometry. 154 | """ 155 | return self._get(f"geometries/{geometry_id}/predictions") 156 | -------------------------------------------------------------------------------- /src/ansys/simai/core/api/optimization.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from typing import Dict 24 | 25 | from ansys.simai.core.api.mixin import ApiClientMixin 26 | 27 | 28 | class OptimizationClientMixin(ApiClientMixin): 29 | def define_optimization(self, workspace_id: str, optimization_parameters: Dict): 30 | return self._post(f"workspaces/{workspace_id}/optimizations", json=optimization_parameters) 31 | 32 | def run_optimization_trial(self, optimization_id: str, parameters: Dict): 33 | return self._post( 34 | f"optimizations/{optimization_id}/trial-runs", 35 | json=parameters, 36 | ) 37 | 38 | def get_optimization(self, optimization_id: str): 39 | return self._get(f"optimizations/{optimization_id}") 40 | 41 | def get_optimization_trial_run(self, trial_run_id: str): 42 | return self._get(f"optimizations/trial-runs/{trial_run_id}") 43 | -------------------------------------------------------------------------------- /src/ansys/simai/core/api/point_cloud.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2024 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from typing import Any, Dict, List 24 | 25 | from ansys.simai.core.api.mixin import ApiClientMixin 26 | from ansys.simai.core.data.types import APIResponse 27 | 28 | 29 | class PointCloudClientMixin(ApiClientMixin): 30 | def create_point_cloud(self, geometry_id: str, name: str, extension: str) -> APIResponse: 31 | """Create a point cloud without pushing the data. 32 | 33 | Args: 34 | geometry_id: ID of the geometry to assign the point cloud to. 35 | name: Name to give to the geometry. 36 | extension: Extension to give to the file. 37 | """ 38 | post_data = {"name": name, "file_extension": extension} 39 | return self._post(f"geometries/{geometry_id}/point-cloud", json=post_data) 40 | 41 | def complete_point_cloud_upload( 42 | self, point_cloud_id: str, upload_id: str, parts: List[Dict[str, Any]] 43 | ): 44 | """Complete the upload of a point cloud. 45 | 46 | Args: 47 | point_cloud_id: ID of the point cloud to complete 48 | upload_id: ID used to upload the point cloud 49 | parts: List of the uploaded file parts 50 | """ 51 | self._post( 52 | f"point-clouds/{point_cloud_id}/complete", 53 | json={"upload_id": upload_id, "parts": parts}, 54 | return_json=False, 55 | ) 56 | 57 | def delete_point_cloud(self, point_cloud_id: str): 58 | """Delete the specific point cloud file. 59 | 60 | Args: 61 | point_cloud_id: ID of the input cloud to delete 62 | """ 63 | self._delete(f"point-clouds/{point_cloud_id}", return_json=False) 64 | -------------------------------------------------------------------------------- /src/ansys/simai/core/api/post_processing.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import json 24 | import logging 25 | from typing import Any, Dict, List, Optional 26 | 27 | from ansys.simai.core.api.mixin import ApiClientMixin 28 | 29 | logger = logging.getLogger(__name__) 30 | 31 | 32 | class PostProcessingClientMixin(ApiClientMixin): 33 | def run_post_processing( 34 | self, 35 | prediction_id: str, 36 | post_processing_type: str, 37 | params: Optional[Dict[str, Any]] = None, 38 | ) -> Dict[str, Any]: 39 | """Run a postprocessing on the given prediction. 40 | 41 | If the result of the requested postprocessing already exists, the postprocessing 42 | is not rerun. 43 | 44 | Args: 45 | prediction_id: ID of the prediction. 46 | post_processing_type: Type of postprocessing to run on the prediction. 47 | params: Additional JSON parameters as dictionary if required. 48 | 49 | Returns: 50 | JSON with the created or existing postprocessing. 51 | """ 52 | return self._post( 53 | f"predictions/{prediction_id}/post-processings/{post_processing_type}", 54 | json=params, 55 | ) 56 | 57 | def get_post_processing_result(self, post_processing_id: str) -> Dict[str, Any]: 58 | """Get the result of a postprocessing. 59 | 60 | Args: 61 | post_processing_id: ID of the postprocessing. 62 | 63 | Returns: 64 | JSON with the result of the postprocessing. 65 | """ 66 | return self._get(f"post-processings/{post_processing_id}") 67 | 68 | def delete_post_processing(self, post_processing_id: str): 69 | """Delete a postprocessing. 70 | 71 | Args: 72 | post_processing_id: ID of the postprocessing. 73 | 74 | Raises: 75 | NotFoundError: If a postprocessing with this ID is not found on the server. 76 | """ 77 | return self._delete( 78 | f"post-processings/{post_processing_id}", 79 | return_json=False, 80 | ) 81 | 82 | def get_post_processings_in_workspace( 83 | self, workspace_id: str, pp_type: Optional[str] 84 | ) -> List[Dict[str, Any]]: 85 | """Get all postprocessings in the given workspace. 86 | 87 | Args: 88 | workspace_id: ID of the target workspace. 89 | pp_type: Type of postprocessings to return. If this parameter is empty, all 90 | postprocessings are returned. 91 | """ 92 | endpoint = f"post-processings/type/{pp_type}" if pp_type else "post-processings/" 93 | # The content of this endpoint can be paginated 94 | initial_request = self._get(endpoint, params={"workspace": workspace_id}, return_json=False) 95 | pagination_info = json.loads(initial_request.headers.get("X-Pagination", "{}")) 96 | post_processings = initial_request.json() 97 | 98 | for page in range(2, pagination_info.get("total_pages", 1) + 1): 99 | page_request = self._get(endpoint, params={"workspace": workspace_id, "page": page}) 100 | post_processings.extend(page_request) 101 | return post_processings 102 | 103 | def get_post_processings_for_prediction( 104 | self, 105 | prediction_id: str, 106 | pp_type: Optional[str], 107 | filters: Optional[Dict[str, Any]] = None, 108 | ) -> List[Dict[str, Any]]: 109 | """Get all postprocessings belonging to the given prediction. 110 | 111 | Args: 112 | prediction_id: ID of the target prediction. 113 | pp_type: Type of postprocessings to return. If this parameter is empty, all 114 | postprocessings are returned. 115 | filters: Filters to apply to the query, if any. 116 | """ 117 | endpoint = f"predictions/{prediction_id}/post-processings/" 118 | if pp_type: 119 | endpoint = endpoint + pp_type 120 | params = {"filters": json.dumps(filters)} if filters else None 121 | return self._get(endpoint, params=params) 122 | 123 | def post_processings_export_url(self): 124 | return self.build_full_url_for_endpoint("post-processings/export") 125 | -------------------------------------------------------------------------------- /src/ansys/simai/core/api/prediction.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import logging 24 | 25 | from ansys.simai.core.api.mixin import ApiClientMixin 26 | 27 | logger = logging.getLogger(__name__) 28 | 29 | 30 | class PredictionClientMixin(ApiClientMixin): 31 | """Provides the client for the Prediction ("/predictions") part of the API.""" 32 | 33 | def predictions(self, workspace_id: str): 34 | """Get a list of all predictions.""" 35 | return self._get("predictions/", params={"workspace": workspace_id}) 36 | 37 | def get_prediction(self, prediction_id: str): 38 | """Get information on a single prediction. 39 | 40 | Args: 41 | prediction_id: ID of the prediction. 42 | """ 43 | return self._get(f"predictions/{prediction_id}") 44 | 45 | def delete_prediction(self, prediction_id: str): 46 | """Delete a single prediction. 47 | 48 | Args: 49 | prediction_id: ID of the prediction. 50 | """ 51 | return self._delete( 52 | f"predictions/{prediction_id}", 53 | params={"confirm": True}, 54 | return_json=False, 55 | ) 56 | 57 | def run_prediction(self, geometry_id: str, **kwargs): # noqa: D417 58 | """Run a prediction on a given geometry. 59 | 60 | Args: 61 | geometry_id: ID of the target geometry. 62 | 63 | Keyword Arguments: 64 | boundary_conditions dict: Constraints of the problem in dictionary form. 65 | tolerance float: Delta under which two boundary condition components 66 | are considered equal. The default is ``10**-6``. 67 | """ 68 | return self._post(f"geometries/{geometry_id}/predictions", json=kwargs) 69 | -------------------------------------------------------------------------------- /src/ansys/simai/core/api/training_data.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import json 24 | from typing import TYPE_CHECKING, Any, Dict, Optional 25 | from urllib.parse import urlencode 26 | 27 | from ansys.simai.core.api.mixin import ApiClientMixin 28 | from ansys.simai.core.utils.pagination import PaginatedAPIRawIterator 29 | 30 | if TYPE_CHECKING: 31 | from ansys.simai.core.data.types import RawFilters 32 | 33 | 34 | class TrainingDataClientMixin(ApiClientMixin): 35 | def delete_training_data(self, id: str) -> None: 36 | self._delete(f"training-data/{id}", return_json=False) 37 | 38 | def get_training_data(self, id: str) -> Dict[str, Any]: 39 | return self._get(f"training-data/{id}") 40 | 41 | def iter_training_data(self, filters: Optional["RawFilters"]) -> PaginatedAPIRawIterator: 42 | query = urlencode( 43 | [("filter[]", json.dumps(f, separators=(",", ":"))) for f in (filters or [])] 44 | ) 45 | return PaginatedAPIRawIterator(self, f"training-data?{query}") 46 | 47 | def create_training_data(self, name: str, project_id: Optional[str] = None) -> Dict[str, Any]: 48 | args = {"name": name} 49 | if project_id: 50 | args["project"] = project_id 51 | return self._post( 52 | "training-data/", 53 | json=args, 54 | ) 55 | 56 | def add_training_data_to_project(self, training_data_id: str, project_id: str): 57 | return self._put( 58 | f"training-data/{training_data_id}/project/{project_id}/association", 59 | return_json=False, 60 | ) 61 | 62 | def remove_training_data_from_project(self, training_data_id: str, project_id: str): 63 | return self._delete( 64 | f"training-data/{training_data_id}/project/{project_id}/association", 65 | return_json=False, 66 | ) 67 | 68 | def compute_training_data(self, training_data_id: str) -> None: 69 | self._post(f"training-data/{training_data_id}/compute") 70 | 71 | def get_training_data_subset(self, project_id: str, training_data_id: str) -> Dict[str, Any]: 72 | return self._get(f"projects/{project_id}/data/{training_data_id}/subset") 73 | 74 | def put_training_data_subset( 75 | self, project_id: str, training_data_id: str, subset: Optional[str] 76 | ) -> None: 77 | return self._put( 78 | f"projects/{project_id}/data/{training_data_id}/subset", json={"subset": subset} 79 | ) 80 | -------------------------------------------------------------------------------- /src/ansys/simai/core/api/training_data_part.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from typing import Any, Dict, List, Tuple 24 | 25 | from ansys.simai.core.api.mixin import ApiClientMixin 26 | 27 | 28 | class TrainingDataPartClientMixin(ApiClientMixin): 29 | def create_training_data_part( 30 | self, training_data_id: str, name: str, extension: str 31 | ) -> Tuple[Dict[str, Any], str]: 32 | """Create a part under the given training data without uploading the data. 33 | 34 | Args: 35 | training_data_id: ID of the parent training data. 36 | name: Name of the part to create. 37 | extension: Extension of the file or part. 38 | 39 | Returns: 40 | Tuple containing the ``TrainingDataPart`` object and the upload ID 41 | to use for further requests. 42 | """ 43 | post_data = {"name": name, "file_extension": extension} 44 | response = self._post(f"training-data/{training_data_id}/parts/", json=post_data) 45 | return (response["training_data_part"], response["upload_id"]) 46 | 47 | def get_training_data_part(self, id: str) -> Dict[str, Any]: 48 | return self._get(f"training-data-parts/{id}") 49 | 50 | def complete_training_data_part_upload( 51 | self, id: str, upload_id: str, parts: List[Dict[str, Any]] 52 | ): 53 | self._post( 54 | f"training-data-parts/{id}/complete", 55 | json={"upload_id": upload_id, "parts": parts}, 56 | ) 57 | -------------------------------------------------------------------------------- /src/ansys/simai/core/api/workspace.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from typing import Any, Dict, Optional 24 | from urllib.parse import quote 25 | 26 | from ansys.simai.core.api.mixin import ApiClientMixin 27 | from ansys.simai.core.data.types import File 28 | 29 | 30 | class WorkspaceClientMixin(ApiClientMixin): 31 | """Provides the client for the Workspace ("/workspaces") part of the API.""" 32 | 33 | def workspaces(self): 34 | """List all workspaces.""" 35 | return self._get("workspaces/") 36 | 37 | def get_workspace(self, workspace_id: str) -> Dict[str, Any]: 38 | """Get information on a single workspace. 39 | 40 | Args: 41 | workspace_id: ID of the workspace. 42 | 43 | Returns: 44 | JSON representation of the workspace. 45 | 46 | Raises: 47 | ansys.simai.core.errors.NotFoundError: If no workspace with that ID exists. 48 | ansys.simai.core.errors.ApiClientError: On other HTTP errors. 49 | """ 50 | return self._get(f"workspaces/{workspace_id}") 51 | 52 | def get_workspace_by_name(self, name: str): 53 | """Get information on a single workspace by name instead of ID. 54 | 55 | Args: 56 | name: Name of the workspace. 57 | """ 58 | return self._get(f"workspaces/name/{quote(name)}") 59 | 60 | def get_workspace_model_manifest(self, workspace_id): 61 | """Get the public part of the manifest for the given workspace. 62 | 63 | Args: 64 | workspace_id: ID of the workspace. 65 | 66 | Raises: 67 | ansys.simai.core.errors.NotFoundError: If no workspace with that ID exists. 68 | ansys.simai.core.errors.ApiClientError: On other HTTP errors. 69 | """ 70 | return self._get(f"workspaces/{workspace_id}/model/manifest/public") 71 | 72 | def create_workspace(self, name: str, model_id: str, **kwargs): 73 | """Create a workspace. 74 | 75 | Args: 76 | name: Name to give to the new workspace. 77 | model_id: ID of the model that the workspace is to use. 78 | **kwargs: Additional arguments for the workspace creation. 79 | 80 | Returns: 81 | JSON representation of the new workspace. 82 | """ 83 | return self._post("workspaces/", json={"name": name, "model": model_id, **kwargs}) 84 | 85 | def delete_workspace(self, workspace_id: str): 86 | """Delete a workspace. 87 | 88 | Args: 89 | workspace_id: ID of the workspace. 90 | 91 | Raises: 92 | ansys.simai.core.errors.NotFoundError: If no workspace with that ID exists. 93 | ansys.simai.core.errors.ApiClientError: On other HTTP errors. 94 | """ 95 | return self._delete(f"workspaces/{workspace_id}") 96 | 97 | def download_workspace_model_evaluation_report(self, workspace_id: str, file: Optional[File]): 98 | return self.download_file(f"workspaces/{workspace_id}/model-evaluation-report", file) 99 | 100 | def download_workspace_mer_data(self, workspace_id: str, file: Optional[File]): 101 | return self.download_file(f"workspaces/{workspace_id}/mer-data", file) 102 | -------------------------------------------------------------------------------- /src/ansys/simai/core/data/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/ansys/simai/core/data/downloads.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from io import BytesIO 24 | from typing import TYPE_CHECKING, Any, Dict, Optional 25 | 26 | from ansys.simai.core.data.types import File 27 | 28 | if TYPE_CHECKING: 29 | import ansys.simai.core.client 30 | 31 | 32 | class DownloadableResult: 33 | """Provides the object representing a result data for a postprocessing in binary format.""" 34 | 35 | def __init__( 36 | self, 37 | download_url: Optional[str], 38 | client: "ansys.simai.core.client.SimAIClient", 39 | request_method: str = "GET", 40 | request_json_body: Optional[Dict[str, Any]] = None, 41 | ): 42 | self.url = download_url 43 | self._client = client 44 | self._request_method = request_method 45 | self._request_json_body = request_json_body 46 | 47 | def download(self, file: File) -> None: 48 | """Download the postprocessing data to the specified file or path. 49 | 50 | Args: 51 | file: Binary file-object or path of the file to download the data into. 52 | """ 53 | self._download_file(self.url, file) 54 | 55 | def in_memory(self) -> BytesIO: 56 | """Load the postprocessing data in memory. 57 | 58 | Returns: 59 | :class:`io.BytesIO` object containing the postprocessing data. 60 | """ 61 | return self._download_file(self.url) 62 | 63 | def _download_file(self, url: str, file: Optional[File] = None) -> Optional[BytesIO]: 64 | return self._client._api.download_file( 65 | url, 66 | file, 67 | request_method=self._request_method, 68 | request_json_body=self._request_json_body, 69 | ) 70 | -------------------------------------------------------------------------------- /src/ansys/simai/core/data/lists.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import warnings 24 | from typing import TYPE_CHECKING, Callable, Dict, Generic, List, Optional, TypeVar, Union 25 | 26 | from ansys.simai.core.data.downloads import DownloadableResult 27 | from ansys.simai.core.data.post_processings import PostProcessing 28 | 29 | if TYPE_CHECKING: 30 | from ansys.simai.core.data.predictions import Prediction 31 | from ansys.simai.core.data.selections import Selection 32 | 33 | from ansys.simai.core.errors import InvalidArguments, _foreach_despite_errors, _map_despite_errors 34 | 35 | T = TypeVar("T", bound=PostProcessing) 36 | 37 | 38 | class PPList(List, Generic[T]): 39 | """Provides a subclass of the :class:`list` class for storing postprocessings and adding a few shortcut methods. 40 | 41 | As a :class:`list` subclass, the ``PPList`` class supports any list operation. 42 | Its elements can be iterated on and accessed by index. 43 | """ 44 | 45 | def __init__(self, selection: "Selection", post: Callable[["Prediction"], PostProcessing]): # noqa: D107 46 | self._selection = selection 47 | # Even in case of errors, all post-processings will be queued. 48 | # but if some fail, an exception will be raised. 49 | post_processings = _map_despite_errors(lambda pred: post(pred), self._selection.predictions) 50 | super().__init__(post_processings) 51 | 52 | @property 53 | def data(self) -> Union[List[Dict[str, List]], List[DownloadableResult]]: 54 | """List containing the data of the underlying postprocessings. 55 | 56 | This is a blocking method, which returns once the data of all 57 | postprocessings is ready. 58 | """ 59 | return [pp.data for pp in self] 60 | 61 | def wait(self): 62 | """Wait for all concerned postprocessings to finish.""" 63 | _foreach_despite_errors(lambda pp: pp.wait(), self) 64 | 65 | 66 | class ExportablePPList(PPList, Generic[T]): 67 | """Provides a subclass of the :class:`PPList` class for downloading the results of a group of postprocessings. 68 | 69 | As a :class:`list` subclass, the ``ExportablePPList`` class supports any list operation. 70 | Its elements can be iterated on and accessed by index. 71 | """ 72 | 73 | def export(self, format: Optional[str] = "json") -> DownloadableResult: 74 | """Export the postprocessing results in the desired format. 75 | 76 | Accessing this property blocks until the data is ready. 77 | 78 | Args: 79 | format: format to exported data in. The default is ``'json'``. 80 | Options are ``'csv.zip'``, ``'json'``, and ``'xlsx'``. 81 | Note that the ``'csv.zip'`` option exports a ZIP file containing 82 | multiple CSV sheets. 83 | 84 | Returns: 85 | :class:`~ansys.simai.core.data.downloads.DownloadableResult` object for 86 | downloading the exported data into a file or access it in memory. 87 | """ 88 | if format not in ["json", "csv.zip", "xlsx", "csv"]: 89 | raise InvalidArguments( 90 | f"Export format must be json, csv.zip, or xlsx (passed {format})." 91 | ) 92 | if format == "csv": 93 | warnings.warn( 94 | "The ``csv`` option is being deprecated. Use the ``csv.zip`` option instead", 95 | PendingDeprecationWarning, 96 | stacklevel=1, 97 | ) 98 | if len(self) < 1: 99 | raise InvalidArguments("Selection contains no exportable postprocessing.") 100 | # Wait for all concerned post-processings to finish (and raise if errors) 101 | self.wait() 102 | client = self[0]._client 103 | return DownloadableResult( 104 | client._api.post_processings_export_url(), 105 | client, 106 | request_method="POST", 107 | request_json_body={"ids": [pp.id for pp in self], "format": format}, 108 | ) 109 | -------------------------------------------------------------------------------- /src/ansys/simai/core/data/models.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2024 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | 24 | from ansys.simai.core.data.base import ComputableDataModel, Directory 25 | from ansys.simai.core.data.model_configuration import ModelConfiguration 26 | from ansys.simai.core.errors import InvalidArguments 27 | 28 | 29 | class Model(ComputableDataModel): 30 | """Training model representation.""" 31 | 32 | def __repr__(self) -> str: 33 | return f"" 34 | 35 | @property 36 | def project_id(self) -> str: 37 | """The ID of the project where the model exists.""" 38 | return self.fields["project_id"] 39 | 40 | @property 41 | def configuration(self) -> ModelConfiguration: 42 | """Build configuration of a model.""" 43 | return ModelConfiguration._from_payload( 44 | project=self._client.projects.get(self.fields["project_id"]), 45 | **self.fields["configuration"], 46 | ) 47 | 48 | 49 | class ModelDirectory(Directory[Model]): 50 | """Provides a collection of methods related to building models.""" 51 | 52 | _data_model = Model 53 | 54 | def get(self, model_id: str) -> Model: 55 | """[Do not use] Get a model by project ID. 56 | 57 | Args: 58 | model_id: ID of the model. 59 | """ 60 | 61 | raise NotImplementedError("The method 'get' of the class Model is not implemented yet.") 62 | 63 | def build( 64 | self, 65 | configuration: ModelConfiguration, 66 | dismiss_data_with_fields_discrepancies: bool = False, 67 | dismiss_data_with_volume_overflow: bool = False, 68 | dismiss_data_input_with_nan: bool = False, 69 | ): 70 | """Launches a build given a configuration. 71 | 72 | Args: 73 | configuration: a ModelConfiguration object that contains the properties to be used in the build 74 | dismiss_data_with_fields_discrepancies: set to True for omitting data with missing properties 75 | dismiss_data_with_volume_overflow: set to True for omitting data outside the Domain of Analysis 76 | dismiss_data_input_with_nan: set to True for omitting data with inputs containing NaN values 77 | 78 | Example: 79 | Use a previous configuration for a new build in the same project 80 | 81 | .. code-block:: python 82 | 83 | a_project = simai.projects.get("project_A") 84 | 85 | build_conf = a_project.last_model_configuration 86 | 87 | new_model = simai.models.build(build_conf) 88 | 89 | Use a previous configuration for a new build in another project 90 | 91 | .. code-block:: python 92 | 93 | a_project = simai.projects.get("project_A") 94 | 95 | build_conf = a_project.last_model_configuration 96 | 97 | b_project = simai.projects.get("project_B") 98 | 99 | # set the id of b_project as the project_id of the configuration 100 | build_conf.project = b_project 101 | 102 | new_model = simai.models.build(build_conf) 103 | 104 | """ 105 | if not configuration.project: 106 | raise InvalidArguments("The model configuration does not have a project set") 107 | 108 | is_trainable = configuration.project.is_trainable() 109 | if not is_trainable: 110 | raise InvalidArguments(f"Cannot train model because: {is_trainable.reason}") 111 | 112 | if configuration.build_on_top: 113 | response = self._client._api.launch_build_on_top( 114 | configuration.project.id, 115 | dismiss_data_with_fields_discrepancies, 116 | dismiss_data_with_volume_overflow, 117 | dismiss_data_input_with_nan, 118 | ) 119 | else: 120 | response = self._client._api.launch_build( 121 | configuration.project.id, 122 | configuration._to_payload(), 123 | dismiss_data_with_fields_discrepancies, 124 | dismiss_data_with_volume_overflow, 125 | dismiss_data_input_with_nan, 126 | ) 127 | return self._model_from(response) 128 | -------------------------------------------------------------------------------- /src/ansys/simai/core/data/training_data_parts.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from ansys.simai.core.data.base import DataModel, Directory, UploadableResourceMixin 24 | 25 | 26 | class TrainingDataPart(UploadableResourceMixin, DataModel): 27 | """Provides the local representation of a training data part object.""" 28 | 29 | def __repr__(self) -> str: 30 | return f"" 31 | 32 | @property 33 | def name(self) -> str: 34 | """Name of the file.""" 35 | return self.fields["name"] 36 | 37 | @property 38 | def size(self) -> int: 39 | """Size of the file in bytes.""" 40 | return self.fields["size"] 41 | 42 | 43 | class TrainingDataPartDirectory(Directory[TrainingDataPart]): 44 | """Provides the collection of methods related to training data parts. 45 | 46 | This class is accessed through ``client.training_data_parts``. 47 | """ 48 | 49 | _data_model = TrainingDataPart 50 | 51 | def get(self, id: str) -> TrainingDataPart: 52 | """Get a :class:`TrainingDataPart` object from the server.""" 53 | return self._model_from(self._client._api.get_training_data_part(id)) 54 | -------------------------------------------------------------------------------- /src/ansys/simai/core/errors.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from typing import Any, Callable, Iterable, List, TypeVar 24 | 25 | import niquests 26 | 27 | 28 | class SimAIError(Exception): 29 | """Provides the base exception for all errors of the SimAI client. 30 | 31 | To catch any expected error that the client might throw, use this exception. 32 | """ 33 | 34 | 35 | class ApiClientError(SimAIError, niquests.exceptions.HTTPError): 36 | """HTTP error from the SimAI API.""" 37 | 38 | def __init__(self, message: str, response=None): 39 | super(ApiClientError, self).__init__(message, response=response) 40 | 41 | @property 42 | def status_code(self): # noqa: D102 43 | if self.response is not None: 44 | return self.response.status_code 45 | 46 | 47 | class NotFoundError(ApiClientError): 48 | """Required resource was not found on the server.""" 49 | 50 | 51 | class ConnectionError(SimAIError, niquests.exceptions.ConnectionError): 52 | """Could not communicate with the server.""" 53 | 54 | 55 | class ConfigurationError(SimAIError): 56 | """Client could not be configured properly.""" 57 | 58 | 59 | class ConfigurationNotFoundError(ConfigurationError): 60 | """Configuration file does not exist.""" 61 | 62 | 63 | class InvalidConfigurationError(ConfigurationError, ValueError): 64 | """Given configuration is not valid.""" 65 | 66 | 67 | class ProcessingError(SimAIError): 68 | """Data could not be processed.""" 69 | 70 | 71 | class InvalidArguments(SimAIError, ValueError): 72 | """Invalid arguments were provided.""" 73 | 74 | 75 | class InvalidClientStateError(SimAIError): 76 | """Client's state is invalid.""" 77 | 78 | 79 | class InvalidServerStateError(SimAIError): 80 | """Server's state is invalid.""" 81 | 82 | 83 | class InvalidOperationError(SimAIError): 84 | """This operation is not possible in the current state.""" 85 | 86 | 87 | class MultipleErrors(SimAIError): 88 | """Multiple errors occurred.""" 89 | 90 | def __init__(self, exceptions: List[SimAIError]): 91 | self.exceptions = exceptions 92 | 93 | 94 | T = TypeVar("T") 95 | 96 | 97 | def _map_despite_errors( 98 | function: Callable[[T], Any], 99 | iterable: Iterable[T], 100 | ): 101 | """Like the map() method, this method applies the function for 102 | each item in the iterable and returns the result. On an exception, 103 | it continue with the next items. At the end, it raises either the 104 | exception or the ``MultipleError`` exception. 105 | """ 106 | results: List[T] = [] 107 | errors: List[SimAIError] = [] 108 | for item in iterable: 109 | try: 110 | res = function(item) 111 | results.append(res) 112 | except SimAIError as e: 113 | errors.append(e) 114 | if errors: 115 | if len(errors) == 1: 116 | raise errors[0] 117 | raise MultipleErrors(errors) 118 | return results 119 | 120 | 121 | def _foreach_despite_errors( 122 | procedure: Callable[[T], None], 123 | iterable: Iterable[T], 124 | ): 125 | """This method applies the procedure for each item in the 126 | iterable. On an exception, it continues with the next items. 127 | At the end, it raises either the exception or the ``MultipleError`` 128 | exception. 129 | """ 130 | errors = [] 131 | for item in iterable: 132 | try: 133 | procedure(item) 134 | except SimAIError as e: 135 | errors.append(e) 136 | if errors: 137 | if len(errors) == 1: 138 | raise errors[0] 139 | raise MultipleErrors(errors) 140 | -------------------------------------------------------------------------------- /src/ansys/simai/core/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansys/pysimai/a7c55f0399a895014a9b4edbb5025297c04d2e03/src/ansys/simai/core/py.typed -------------------------------------------------------------------------------- /src/ansys/simai/core/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/ansys/simai/core/utils/config_file.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import logging 24 | import os 25 | import platform 26 | from inspect import cleandoc 27 | from pathlib import Path 28 | from typing import Any, Dict, Optional 29 | 30 | import tomli 31 | from pydantic.v1.utils import deep_update 32 | 33 | from ansys.simai.core.data.types import Path as PathType 34 | from ansys.simai.core.errors import ConfigurationNotFoundError, InvalidConfigurationError 35 | 36 | logger = logging.getLogger(__name__) 37 | 38 | 39 | def _scan_defaults_config_paths() -> Optional[Path]: 40 | """Look for configuration files in the default locations. 41 | 42 | Returns: 43 | Path of the first configuration file from the list of existing configuration files. 44 | """ 45 | system = platform.system() 46 | if system == "Windows": 47 | base_folder = Path(os.environ.get("APPDATA", "~")).expanduser() 48 | config_file = base_folder / "Ansys" / "simai.conf" 49 | if config_file.is_file(): 50 | return config_file 51 | elif system == "Linux" or system == "Darwin": 52 | xdg_config = Path(os.getenv("XDG_CONFIG_HOME") or "~/.config") 53 | conf_paths = [ 54 | xdg_config / "ansys_simai.conf", 55 | xdg_config / "ansys/simai.conf", 56 | "~/.ansys_simai.conf", 57 | "~/.ansys/simai.conf", 58 | "/etc/ansys_simai.conf", 59 | "/etc/ansys/simai.conf", 60 | ] 61 | for path in conf_paths: 62 | path = Path(path).expanduser() # noqa: PLW2901 63 | if path.is_file(): 64 | logger.debug(f"Found a configuration file at {path}.") 65 | return path 66 | else: 67 | raise ConfigurationNotFoundError("Could not determine OS.") 68 | # If no file was found 69 | return None 70 | 71 | 72 | def get_config( 73 | path: Optional[PathType] = None, 74 | profile: str = "default", 75 | ignore_missing=False, 76 | **kwargs, 77 | ) -> Dict[Any, Any]: 78 | """Get configuration file, validate it, and return it as a flat dictionary. 79 | 80 | Args: 81 | path: Path of the configuration file. The default is ``None, in which 82 | case the method looks in default locations. 83 | profile: Profile to load. If no oath is specified, the method looks for ``[default]``. 84 | ignore_missing: Whether to raise an exception if no path to a configuration 85 | file is found. The default is ``False``. 86 | **kwargs: Overrides to apply to the configuration. 87 | """ 88 | config_path = path or _scan_defaults_config_paths() 89 | if config_path is None: 90 | if ignore_missing: 91 | return kwargs 92 | raise ConfigurationNotFoundError 93 | config_path = Path(config_path).expanduser() 94 | if not os.path.isfile(config_path): 95 | raise ConfigurationNotFoundError 96 | with open(config_path, "rb") as f: 97 | try: 98 | all_config = tomli.load(f) 99 | except tomli.TOMLDecodeError as e: 100 | raise InvalidConfigurationError(f"Invalid configuration: {e}") from None 101 | config = all_config.get(profile, None) 102 | if config is None: 103 | raise InvalidConfigurationError( 104 | cleandoc( # Takes care of the indentation 105 | f""" 106 | Did not find the [{profile}] profile section in the configuration file. 107 | Make sure that you have a '[default]' section or specify the name of the profile in your 'from_config' call. 108 | """ 109 | ) 110 | ) 111 | config["_config_file_profile"] = profile 112 | # Apply overrides from kwargs if any 113 | config = deep_update(config, kwargs) 114 | return config 115 | -------------------------------------------------------------------------------- /src/ansys/simai/core/utils/files.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import logging 24 | import os 25 | import pathlib 26 | import platform 27 | import time 28 | from typing import IO, TYPE_CHECKING, Any 29 | 30 | logger = logging.getLogger(__name__) 31 | 32 | if TYPE_CHECKING: 33 | from ansys.simai.core.data.types import Path 34 | 35 | 36 | def _expand_user_path(file_path: "Path") -> pathlib.Path: 37 | """Convert string inputs to ``Path`` and expand the user. 38 | 39 | This method supports paths starting with ``~`` on Linux. 40 | """ 41 | return pathlib.Path(str(file_path)).expanduser() 42 | 43 | 44 | def file_path_to_obj_file(file_path: "Path", mode: str) -> IO[Any]: 45 | """Take a file path and return a file-object opened in the given mode.""" 46 | file_path = _expand_user_path(file_path) 47 | file_path.parent.mkdir(parents=True, exist_ok=True) 48 | logger.debug(f"Opening file {file_path}") 49 | return open(file_path, mode=mode) # noqa: SIM115 50 | 51 | 52 | def get_cache_dir() -> pathlib.Path: 53 | system = platform.system() 54 | if system == "Windows": 55 | cache_dir = pathlib.Path(os.getenv("APPDATA", "~")) / "Ansys" / "cache" / "pysimai" 56 | elif system == "Linux": 57 | cache_dir = pathlib.Path(os.getenv("XDG_CACHE_HOME", "~/.cache")) / "ansys" / "pysimai" 58 | elif system == "Darwin": 59 | cache_dir = pathlib.Path("~/Library/Caches/Ansys/pysimai") 60 | else: 61 | raise RuntimeError(f"Unknown OS: {system}") 62 | cache_dir = cache_dir.expanduser() 63 | cache_dir.mkdir(exist_ok=True, parents=True) 64 | a_week_ago = time.time() - 7 * 86400 65 | for cache_entry in pathlib.Path(cache_dir).glob("*"): 66 | if cache_entry.is_file(): 67 | itemTime = cache_entry.stat().st_mtime 68 | if itemTime < a_week_ago: 69 | cache_entry.unlink() 70 | return cache_dir 71 | -------------------------------------------------------------------------------- /src/ansys/simai/core/utils/grouping.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from typing import Any, Callable, Optional 24 | 25 | from ansys.simai.core.utils.numerical import is_equal_with_tolerance 26 | 27 | 28 | class _ToleranceGroup: 29 | """Provides a group of approximately equal values, 30 | as returned by ``itertools.groupby`` when using ``_ToleranceGrouper``. 31 | 32 | Note that with the way ``itertools.groupby`` works, 33 | accessing the center should be done only after accessing (iterating) the bucket's elements. 34 | """ 35 | 36 | def __init__( 37 | self, 38 | initial_value: float, 39 | forced_central_value: Optional[float], 40 | tolerance: Optional[float], 41 | ): 42 | self.min = initial_value 43 | self.center = initial_value 44 | self.forced_central_value = forced_central_value 45 | self.tolerance = tolerance 46 | self.center_is_fixed = False 47 | 48 | def matches(self, new_value): 49 | if not is_equal_with_tolerance(new_value, self.center, self.tolerance): 50 | # The new value is just too far 51 | return False 52 | if new_value == self.forced_central_value: 53 | # Accept forced value to current bucket only if it's at reach for min 54 | # (else, refuse new value in this bucket) 55 | return is_equal_with_tolerance(new_value, self.min, self.tolerance) 56 | # Value is at reach of center (and not forced to be a new center): 57 | # it's all good 58 | return True 59 | 60 | def collect_value(self, new_value): 61 | # Check if the new value can be its center: 62 | # it must be at reach of bucket min, 63 | # and current center must not be fixed 64 | if not self.center_is_fixed and is_equal_with_tolerance( 65 | self.min, new_value, self.tolerance 66 | ): 67 | self.center = new_value 68 | # Fix the center of this bucket if it's the forced center 69 | if new_value == self.forced_central_value: 70 | self.center_is_fixed = True 71 | 72 | 73 | class _ToleranceGrouper: 74 | """Provides a grouping class destined to be used by ``itertools.groupby`` to create 75 | groups of approximately equal value (according to tolerance). 76 | 77 | The ``_ToleranceGrouper`` class is meant to be passed as `key` argument for ``itertools.groupby``. 78 | It creates buckets defined by a central value and contains all values around it, plus or minus 79 | the tolerance. Each bucket is an instance of the ``_ToleranceGroup`` class. 80 | 81 | Args: 82 | key_func: Optional function computing a key value for each passed element. 83 | If no element is passed, the element itself is used. 84 | tolerance: Optional tolerance. The default is ``10**-6``. 85 | forced_central_value: Optional value that is forced to be at the 86 | center of its group. This means that the group spans maximally 87 | from forced_central_value - tolerance to forced_central_value + tolerance. 88 | """ 89 | 90 | def __init__( 91 | self, 92 | key_func: Optional[Callable[[Any], float]] = None, 93 | tolerance: Optional[float] = None, 94 | forced_central_value: Optional[float] = None, 95 | ): 96 | if key_func is None: 97 | # if none passed, use identity function 98 | self.key_func = lambda x: x 99 | else: 100 | self.key_func = key_func 101 | self.tolerance = tolerance 102 | self.forced_central_value = forced_central_value 103 | self.current_bucket: Optional[_ToleranceGroup] = None 104 | 105 | def __call__(self, item: Any) -> float: 106 | new_value: float = self.key_func(item) 107 | if self.current_bucket is None or not self.current_bucket.matches(new_value): 108 | # Create a new bucket 109 | self.current_bucket = _ToleranceGroup( 110 | new_value, 111 | forced_central_value=self.forced_central_value, 112 | tolerance=self.tolerance, 113 | ) 114 | self.current_bucket.collect_value(new_value) 115 | 116 | # Return bucket, which is used by groupby 117 | # to group all values together 118 | return self.current_bucket 119 | -------------------------------------------------------------------------------- /src/ansys/simai/core/utils/misc.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import getpass 24 | import logging 25 | from typing import Any, Dict, Optional 26 | 27 | from semver.version import Version 28 | 29 | logger = logging.getLogger(__name__) 30 | 31 | 32 | def prompt_for_input(name: str, hide_input: Optional[bool] = False): 33 | return input(f"{name}:") if not hide_input else getpass.getpass(f"{name}:") 34 | 35 | 36 | def build_boundary_conditions(boundary_conditions: Optional[Dict[str, Any]] = None, **kwargs): 37 | bc = boundary_conditions if boundary_conditions else {} 38 | bc.update(**kwargs) 39 | if not bc: 40 | raise ValueError("No boundary condition was specified.") 41 | return bc 42 | 43 | 44 | def dict_get(obj: dict, *keys: str, default=None): 45 | """Get the requested key of the dictionary or opt to the default.""" 46 | for k in keys: 47 | obj = obj.get(k, {}) or {} 48 | return obj or default 49 | 50 | 51 | def notify_if_package_outdated(package: str, current_version: str, latest_version: str): 52 | try: 53 | version_current = Version.parse(current_version) 54 | version_latest = Version.parse(latest_version) 55 | except ValueError as e: 56 | logger.debug(f"Could not parse package version: {e}") 57 | else: 58 | if version_current < version_latest: 59 | warn_template = ( 60 | f"A new version of {package} is %s. " 61 | "Upgrade to get new features and ensure compatibility with the server." 62 | ) 63 | if ( 64 | version_current.major < version_latest.major 65 | or version_current.minor < version_latest.minor 66 | ): 67 | logger.critical(warn_template % "required") 68 | else: 69 | logger.warning(warn_template % "available") 70 | -------------------------------------------------------------------------------- /src/ansys/simai/core/utils/numerical.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import logging 24 | import math 25 | from numbers import Number 26 | from typing import Any, Optional, Tuple 27 | 28 | from ansys.simai.core.errors import SimAIError 29 | 30 | DEFAULT_COMPARISON_EPSILON = 10**-6 31 | 32 | logger = logging.getLogger(__name__) 33 | 34 | 35 | def is_number(value: Any): 36 | return isinstance(value, Number) and not math.isnan(value) 37 | 38 | 39 | def is_smaller_with_tolerance(a: float, b: float, tolerance: Optional[float] = None): 40 | """Run the `less than`(<) comparison with a tolerance. 41 | 42 | The default for the tolerance is ``.000001``. 43 | 44 | If the difference between the two numbers is less 45 | than the tolerance, ``a`` is considered equal, not smaller. 46 | """ 47 | if tolerance is None: 48 | tolerance = DEFAULT_COMPARISON_EPSILON 49 | return a < b - tolerance 50 | 51 | 52 | def is_bigger_with_tolerance(a: float, b: float, tolerance: Optional[float] = None): 53 | """Run the `greater than` (>) comparison with a tolerance. 54 | 55 | The default for the tolerance is ``.000001``. 56 | 57 | If the difference between the two numbers is greater 58 | than the tolerance, ``b`` is considered equal, not larger. 59 | """ 60 | if tolerance is None: 61 | tolerance = DEFAULT_COMPARISON_EPSILON 62 | return a > b + tolerance 63 | 64 | 65 | def is_smaller_or_equal_with_tolerance(a: float, b: float, tolerance: Optional[float] = None): 66 | """Run the `less than or equal to` (<=) comparison with a tolerance. 67 | 68 | The default for the tolerance is ``.000001``. 69 | 70 | If the difference between the two numbers is smaller 71 | than the tolerance, ``b`` is considered equal. 72 | """ 73 | return not is_bigger_with_tolerance(a, b, tolerance) 74 | 75 | 76 | def is_bigger_or_equal_with_tolerance(a: float, b: float, tolerance: Optional[float] = None): 77 | """Run the `greater than or equal to` (>=) comparison with a tolerance. 78 | 79 | The default for the tolerance is ``.000001``. 80 | 81 | If the difference between the two numbers is smaller 82 | than the tolerance, ``b`` is considered equal. 83 | """ 84 | return not is_smaller_with_tolerance(a, b, tolerance) 85 | 86 | 87 | def is_equal_with_tolerance(a: float, b: float, tolerance: Optional[float] = None): 88 | """Compare the equality of two numbers with a tolerance. 89 | 90 | The default toleranance is ``.000001``. 91 | 92 | If the difference between the two numbers is smaller 93 | than the tolerance, they are considered equal. 94 | """ 95 | if tolerance is None: 96 | tolerance = DEFAULT_COMPARISON_EPSILON 97 | 98 | return abs(a - b) <= tolerance 99 | 100 | 101 | def convert_axis_and_coordinate_to_plane_eq_coeffs( 102 | axis: str, coordinate: float 103 | ) -> Tuple[float, float, float, float]: 104 | default_coeffs = [0, 0, 0, coordinate] 105 | if axis == "x": 106 | default_coeffs[0] = 1 107 | elif axis == "y": 108 | default_coeffs[1] = 1 109 | elif axis == "z": 110 | default_coeffs[2] = 1 111 | return tuple(default_coeffs) 112 | 113 | 114 | def validate_tolerance_parameter(tolerance: Optional[Number]): 115 | if tolerance is None: 116 | return 117 | if not is_number(tolerance): 118 | raise TypeError(f"The tolerance argument must be a number >= 0 (passed {tolerance})") 119 | if tolerance < 0: 120 | raise ValueError(f"The tolerance argument must be a number >= 0 (passed {tolerance})") 121 | 122 | 123 | # Recursively go through dict to cast values to float, so that we support NaN and inf 124 | # Only use if all the values are expected to be numbers 125 | def cast_values_to_float(data: Any): 126 | if isinstance(data, dict): 127 | return {k: cast_values_to_float(v) for k, v in data.items()} 128 | else: 129 | try: 130 | return float(data) 131 | except ValueError as err: 132 | raise SimAIError(f"received invalid float value: {data!r}") from err 133 | -------------------------------------------------------------------------------- /src/ansys/simai/core/utils/pagination.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2025 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | """Helpers to handle paginated responses.""" 24 | 25 | import json 26 | import logging 27 | from typing import Any, Dict, Iterator, Sized, TypeVar 28 | 29 | from ansys.simai.core.api.mixin import ApiClientMixin 30 | from ansys.simai.core.data.base import DataModel, Directory 31 | 32 | logger = logging.getLogger(__name__) 33 | M = TypeVar("M", bound=DataModel) 34 | 35 | 36 | class PaginatedAPIRawIterator(Sized, Iterator[Dict[str, Any]]): 37 | """Iterator over a raw paginated response with a known length.""" 38 | 39 | def __init__(self, client: ApiClientMixin, uri: str): 40 | self.client = client 41 | self._first_page = client._get(uri, return_json=False) 42 | try: 43 | x_pagination = self._first_page.headers["X-Pagination"] 44 | self.len = json.loads(x_pagination)["total"] 45 | except (KeyError, json.JSONDecodeError) as e: 46 | logger.warning(f"Could not fetch item count from pagination header: {e}") 47 | self.len = 0 48 | self.__it = self.__gen() 49 | 50 | def __gen(self): 51 | yield from self._first_page.json() 52 | next_page = self._first_page.links.get("next", {}).get("url") 53 | del self._first_page 54 | while next_page: 55 | page_request = self.client._get(next_page, return_json=False) 56 | next_page = page_request.links.get("next", {}).get("url") 57 | yield from page_request.json() 58 | 59 | def __next__(self) -> Dict[str, Any]: 60 | elem = next(self.__it) 61 | self.len -= 1 62 | return elem 63 | 64 | def __len__(self) -> int: 65 | return self.len 66 | 67 | 68 | class DataModelIterator(Sized, Iterator[M]): 69 | """Iterator over paginated objects with a known length.""" 70 | 71 | def __init__(self, raw: PaginatedAPIRawIterator, directory: Directory[M]): 72 | self.raw = raw 73 | self.directory = directory 74 | 75 | def __next__(self) -> M: 76 | entry = next(self.raw) 77 | return self.directory._model_from(entry) 78 | 79 | def __len__(self) -> int: 80 | return len(self.raw) 81 | -------------------------------------------------------------------------------- /src/ansys/simai/core/utils/requests.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import logging 24 | from json.decoder import JSONDecodeError 25 | from typing import Literal, overload 26 | 27 | import niquests 28 | 29 | from ansys.simai.core.data.types import JSON, APIResponse 30 | from ansys.simai.core.errors import ApiClientError, NotFoundError 31 | 32 | logger = logging.getLogger(__name__) 33 | 34 | 35 | def handle_http_errors(response: niquests.Response) -> None: 36 | """Raise an error if the response status code is an error. 37 | 38 | Args: 39 | response: Response to check for errors. 40 | 41 | Raises: 42 | NotFoundError: If the response is a 404 error. 43 | ApiClientError: If the response is a 4xx or 5xx error other than the 404 error. 44 | """ 45 | logger.debug("Checking for HTTP errors.") 46 | try: 47 | response.raise_for_status() 48 | except niquests.exceptions.HTTPError as e: 49 | try: 50 | json_response = response.json() 51 | except (ValueError, JSONDecodeError): 52 | # raise the errors from None 53 | # as we want to ignore the JSONDecodeError 54 | if response.status_code == 404: 55 | raise NotFoundError("Not Found", response) from e 56 | else: 57 | raise ApiClientError( 58 | f"{response.status_code} {response.reason}", response 59 | ) from None 60 | if isinstance(json_response, dict): 61 | message = ( 62 | json_response.get("errors") 63 | or json_response.get("message") 64 | or json_response.get("status") 65 | or json_response.get("error_description") 66 | or response.reason 67 | ) 68 | 69 | if response.status_code == 404: 70 | raise NotFoundError(f"{message}", response) from e 71 | else: 72 | error_message = f"{response.status_code} {message}" 73 | if resolution := json_response.get("resolution", ""): 74 | error_message += f"\n{resolution}" 75 | raise ApiClientError( 76 | error_message, 77 | response, 78 | ) from e 79 | else: 80 | raise ApiClientError(f"{response.status_code}: {response.reason}", response) from e 81 | 82 | 83 | @overload 84 | def handle_response(response: niquests.Response, return_json: Literal[True]) -> JSON: ... 85 | 86 | 87 | @overload 88 | def handle_response( 89 | response: niquests.Response, return_json: Literal[False] 90 | ) -> niquests.Response: ... 91 | 92 | 93 | @overload 94 | def handle_response(response: niquests.Response, return_json: bool) -> APIResponse: ... 95 | 96 | 97 | def handle_response(response: niquests.Response, return_json: bool = True) -> APIResponse: 98 | """Handle HTTP errors and return the relevant data from the response. 99 | 100 | Args: 101 | response: Response to handle 102 | return_json: Whether to return the JSON content or the whole response. 103 | 104 | Returns: 105 | JSON dict of the response if :py:args:`return_json` is ``True`` or the raw 106 | :py:class:`requests.Response` otherwise. 107 | """ 108 | handle_http_errors(response) 109 | 110 | logger.debug("Returning response.") 111 | if return_json: 112 | try: 113 | return response.json() 114 | except (ValueError, JSONDecodeError): 115 | logger.debug("Failed to read JSON response.") 116 | raise ApiClientError( 117 | "Expected a JSON response but did not receive one.", response 118 | ) from None 119 | else: 120 | return response 121 | -------------------------------------------------------------------------------- /src/ansys/simai/core/utils/sse_client.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import logging 24 | import time 25 | from typing import Callable, Optional 26 | 27 | import niquests 28 | import sseclient 29 | 30 | logger = logging.getLogger(__name__) 31 | 32 | 33 | # take an optional last-event-id to be sent in request headers 34 | EventSourceFactoryType = Callable[[Optional[str]], sseclient.SSEClient] 35 | 36 | 37 | class ReconnectingSSERequestsClient: 38 | def __init__( 39 | self, 40 | event_source_factory: EventSourceFactoryType, 41 | char_enc: Optional[str] = None, 42 | ): 43 | self._sseclient = None 44 | self._event_source_factory = event_source_factory 45 | self._char_enc = char_enc 46 | self._last_event_id = None 47 | self._retry_timeout_sec = 3 48 | self._connect() 49 | 50 | def _connect(self): 51 | self._disconnect_client() 52 | logger.info(f"Will connect to SSE with last event id {self._last_event_id}.") 53 | event_source = self._event_source_factory(self._last_event_id) 54 | logger.info("Create SSEClient with event source.") 55 | self._sseclient = sseclient.SSEClient(event_source) 56 | 57 | def _disconnect_client(self): 58 | if self._sseclient is not None: 59 | try: 60 | self.close() 61 | except Exception: 62 | logger.error("Failed to close SSEClient ", exc_info=True) 63 | 64 | def close(self): 65 | self._sseclient.close() 66 | self._sseclient = None 67 | 68 | def events(self): 69 | while True: 70 | try: 71 | for event in self._sseclient.events(): 72 | yield event 73 | self._last_event_id = event.id 74 | except ( 75 | StopIteration, 76 | niquests.exceptions.ChunkedEncodingError, 77 | niquests.RequestException, 78 | EOFError, 79 | ) as e: 80 | logger.info(f"SSEClient disconnected: {e}") 81 | logger.info(f"Will try to reconnect after {self._retry_timeout_sec}s") 82 | time.sleep(self._retry_timeout_sec) 83 | self._connect() 84 | -------------------------------------------------------------------------------- /src/ansys/simai/core/utils/typing.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast 24 | 25 | if TYPE_CHECKING: 26 | from typing_extensions import Concatenate, ParamSpec 27 | 28 | P = ParamSpec("P") 29 | T = TypeVar("T") 30 | 31 | 32 | def steal_kwargs_type_on_method( 33 | original_fn: "Callable[P, Any]", 34 | ) -> "Callable[[Callable], Callable[Concatenate[Any, P], T]]": 35 | """Takes type hints from ``original_fn`` and apply them to the decorated method. 36 | The ``self`` argument of the method is typed ``Any``. 37 | """ 38 | 39 | def return_func(func: "Callable[..., T]") -> "Callable[Concatenate[Any, P], T]": 40 | return cast("Callable[Concatenate[Any, P], T]", func) 41 | 42 | return return_func 43 | -------------------------------------------------------------------------------- /src/ansys/simai/core/utils/validation.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from typing import Any, Callable, List, TypeVar, Union 24 | 25 | T = TypeVar("T") 26 | 27 | 28 | def _list_elements_pass_predicate(items_list: List[T], predicate: Callable[[Any], bool]) -> bool: 29 | return isinstance(items_list, List) and all(predicate(item) for item in items_list) 30 | 31 | 32 | def _enforce_as_list_passing_predicate( 33 | parameter: Union[T, List[T]], predicate: Callable[[Any], bool], error_message: str 34 | ) -> List[T]: 35 | """Make sure that the passed parameter is either a single element passing a predicate 36 | or a list of elements that are all passing predicates. 37 | 38 | In both case, a list is returned. In other cases, raise a TypeError with the 39 | error message. 40 | 41 | This method is useful for validating a type of parameter. For example, accepting either 42 | a geometry or a list of geometries. 43 | """ 44 | if predicate(parameter): 45 | return [parameter] 46 | if _list_elements_pass_predicate(parameter, predicate): 47 | return parameter 48 | raise TypeError(error_message) 49 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from pathlib import Path 24 | 25 | import pytest 26 | import responses 27 | 28 | import ansys.simai.core.errors as err 29 | from ansys.simai.core import SimAIClient 30 | 31 | 32 | def test_client_creation_invalid_path(): 33 | with pytest.raises(err.ConfigurationNotFoundError): 34 | SimAIClient.from_config(path="/") 35 | 36 | 37 | def test_client_creation_invalid_config(): 38 | with pytest.raises(err.InvalidConfigurationError): 39 | SimAIClient.from_config(path=Path(__file__).resolve()) 40 | 41 | 42 | @pytest.mark.parametrize( 43 | "local_ver,latest_ver,expected", 44 | [ 45 | ("1.1.0", "1.1.1", "available."), 46 | ("1.0.9-rc8", "1.0.9", "available."), 47 | ("1.0.9", "1.9.0", "required."), 48 | ], 49 | ) 50 | @responses.activate 51 | def test_client_version_auto_warn(caplog, mocker, local_ver, latest_ver, expected): 52 | """WHEN the SDK version is slightly outdated compared to what the API responds 53 | THEN a warning is printed 54 | """ 55 | mocker.patch( 56 | "ansys.simai.core.client.__version__", 57 | local_ver, 58 | ) 59 | responses.add( 60 | responses.GET, 61 | "https://pypi.org/pypi/ansys-simai-core/json", 62 | json={"info": {"version": latest_ver}}, 63 | status=200, 64 | ) 65 | SimAIClient( 66 | url="https://test.test", 67 | organization="dummy", 68 | _disable_authentication=True, 69 | no_sse_connection=True, 70 | skip_version_check=False, 71 | ) 72 | assert f"A new version of ansys-simai-core is {expected}" in caplog.text 73 | -------------------------------------------------------------------------------- /tests/test_geometries.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import json 24 | import urllib 25 | from io import BytesIO 26 | 27 | import responses 28 | 29 | 30 | @responses.activate 31 | def test_geometries_list_no_parameter(simai_client): 32 | expected_params = urllib.parse.urlencode( 33 | { 34 | "filters": {}, 35 | "workspace": simai_client._current_workspace.id, 36 | } 37 | ) 38 | responses.add( 39 | responses.GET, 40 | f"https://test.test/geometries/?{expected_params}", 41 | json={}, 42 | status=200, 43 | ) 44 | simai_client.geometries.list() 45 | 46 | 47 | @responses.activate 48 | def test_geometries_filter(simai_client): 49 | expected_params = urllib.parse.urlencode( 50 | { 51 | "filters": json.dumps( 52 | { 53 | "DIAMETER": 12.5, 54 | "SAUCE": "cream", 55 | } 56 | ), 57 | "workspace": simai_client._current_workspace.id, 58 | } 59 | ) 60 | responses.add( 61 | responses.GET, 62 | f"https://test.test/geometries/?{expected_params}", 63 | json={}, 64 | status=200, 65 | ) 66 | simai_client.geometries.filter(DIAMETER=12.5, SAUCE="cream") 67 | 68 | 69 | @responses.activate 70 | def test_geometries_run_prediction(geometry_factory): 71 | responses.add( 72 | responses.POST, 73 | "https://test.test/geometries/geom-0/predictions", 74 | json={"id": "pred-0"}, 75 | status=200, 76 | ) 77 | geometry = geometry_factory(id="geom-0") 78 | geometry.run_prediction(Vx=10.5) 79 | 80 | 81 | @responses.activate 82 | def test_geometries_run_prediction_dict_bc(geometry_factory): 83 | responses.add( 84 | responses.POST, 85 | "https://test.test/geometries/geom-0/predictions", 86 | json={"id": "pred-0"}, 87 | status=200, 88 | ) 89 | geometry = geometry_factory(id="geom-0") 90 | geometry.run_prediction({"Vx": 10.5}) 91 | 92 | 93 | @responses.activate 94 | def test_geometries_run_prediction_no_bc(geometry_factory): 95 | responses.add( 96 | responses.POST, 97 | "https://test.test/geometries/geom-0/predictions", 98 | json={"id": "pred-0"}, 99 | status=200, 100 | ) 101 | geometry = geometry_factory(id="geom-0") 102 | geometry.run_prediction() 103 | 104 | 105 | @responses.activate 106 | def test_geometries_upload_point_cloud(geometry_factory): 107 | responses.add( 108 | responses.POST, 109 | "https://test.test/geometries/geom-0/point-cloud", 110 | json={"point_cloud": {"id": "pc-0"}, "upload_id": "123"}, 111 | status=200, 112 | ) 113 | responses.add( 114 | responses.PUT, 115 | "https://test.test/point-clouds/pc-0/part", 116 | json={"url": "https://s3.test/pc-0/part"}, 117 | status=200, 118 | ) 119 | responses.add( 120 | responses.PUT, "https://s3.test/pc-0/part", headers={"ETag": "SaladeTomateOignon"} 121 | ) 122 | responses.add(responses.POST, "https://test.test/point-clouds/pc-0/complete", status=204) 123 | 124 | geometry = geometry_factory(id="geom-0") 125 | file = BytesIO(b"Hello World") 126 | geometry.upload_point_cloud((file, "my-point-cloud.vtp")) 127 | assert geometry.point_cloud == {"id": "pc-0"} 128 | 129 | 130 | @responses.activate 131 | def test_geometries_delete_point_cloud(geometry_factory): 132 | responses.add(responses.DELETE, "https://test.test/point-clouds/point-cloud-0", status=204) 133 | 134 | geometry = geometry_factory(id="geom-0", point_cloud={"id": "point-cloud-0"}) 135 | geometry.delete_point_cloud() 136 | assert geometry.point_cloud is None 137 | 138 | 139 | @responses.activate 140 | def test_geometries_delete_point_cloud_cleares_pp_cache( 141 | geometry_factory, prediction_factory, post_processing_factory 142 | ): 143 | responses.add(responses.DELETE, "https://test.test/point-clouds/point-cloud-0", status=204) 144 | 145 | custom_volume_point_cloud = post_processing_factory( 146 | type="CustomVolumePointCloud", prediction_id="prediction-0" 147 | ) 148 | prediction = prediction_factory( 149 | id="prediction-0", post_processings=[custom_volume_point_cloud], geometry_id="geom-0" 150 | ) 151 | geometry = geometry_factory( 152 | id="geom-0", point_cloud={"id": "point-cloud-0"}, predictions=[prediction] 153 | ) 154 | 155 | assert custom_volume_point_cloud in prediction.post._local_post_processings 156 | geometry.delete_point_cloud() 157 | assert custom_volume_point_cloud not in prediction.post._local_post_processings 158 | -------------------------------------------------------------------------------- /tests/test_model_configuration.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2024 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | 24 | import pytest 25 | 26 | from ansys.simai.core.data.model_configuration import ModelConfiguration, SupportedBuildPresets 27 | from ansys.simai.core.errors import InvalidArguments 28 | 29 | 30 | def test_build_preset_error(simai_client): 31 | """WHEN build_preset gets a non-supported value 32 | THEN an InvalidArgument is raised.""" 33 | 34 | raw_project = { 35 | "id": "xX007Xx", 36 | "name": "fifi", 37 | } 38 | 39 | project = simai_client._project_directory._model_from(raw_project) 40 | 41 | with pytest.raises(InvalidArguments) as excinfo: 42 | ModelConfiguration( 43 | project=project, 44 | build_preset="not_valid_value", 45 | ) 46 | 47 | assert f"{list(SupportedBuildPresets)}" in excinfo.value 48 | 49 | 50 | def test_model_input_not_none(simai_client): 51 | """WHEN ModelConfiguration.input gets a None value 52 | THEN an InvalidArgument is raised.""" 53 | 54 | raw_project = { 55 | "id": "xX007Xx", 56 | "name": "fifi", 57 | } 58 | 59 | project = simai_client._project_directory._model_from(raw_project) 60 | 61 | bld_conf = ModelConfiguration( 62 | project=project, 63 | ) 64 | 65 | with pytest.raises(InvalidArguments): 66 | bld_conf.input = None 67 | 68 | 69 | def test_model_output_not_none(simai_client): 70 | """WHEN ModelConfiguration.input gets a None value 71 | THEN an InvalidArgument is raised.""" 72 | 73 | raw_project = { 74 | "id": "xX007Xx", 75 | "name": "fifi", 76 | } 77 | 78 | project = simai_client._project_directory._model_from(raw_project) 79 | 80 | bld_conf = ModelConfiguration( 81 | project=project, 82 | ) 83 | 84 | with pytest.raises(InvalidArguments): 85 | bld_conf.output = None 86 | -------------------------------------------------------------------------------- /tests/test_post_processing_actions.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import responses 24 | 25 | 26 | @responses.activate 27 | def test_post_processing_delete(simai_client, post_processing_factory): 28 | """WHEN Calling delete() on a post-processing 29 | THEN the /delete endpoint is called 30 | ALSO the post-processing is not anymore registered in the Directory 31 | """ 32 | responses.add( 33 | responses.DELETE, 34 | "https://test.test/post-processings/uninteresting-coeffs", 35 | status=204, 36 | ) 37 | 38 | global_coefficients = post_processing_factory( 39 | id="uninteresting-coeffs", type="GlobalCoefficients" 40 | ) 41 | global_coefficients.delete() 42 | assert "uninteresting-coeffs" not in simai_client._post_processing_directory._registry 43 | 44 | 45 | @responses.activate 46 | def test_post_processing_global_coefficients_delete(prediction_factory): 47 | """WHEN deleting a GlobalCoefficients post-processing from a prediction 48 | THEN there is a call to the DELETE endpoint 49 | ALSO a new call to pred.post.global_coefficients() re-runs the post-processing 50 | """ 51 | pred = prediction_factory() 52 | responses.add( 53 | responses.POST, 54 | f"https://test.test/predictions/{pred.id}/post-processings/GlobalCoefficients", 55 | json={ 56 | "id": "pp-instance-one", 57 | "state": "successful", 58 | "type": "GlobalCoefficients", 59 | }, 60 | status=200, 61 | ) 62 | responses.add( 63 | responses.POST, 64 | f"https://test.test/predictions/{pred.id}/post-processings/GlobalCoefficients", 65 | json={ 66 | "id": "pp-instance-two", 67 | "state": "successful", 68 | "type": "GlobalCoefficients", 69 | }, 70 | status=200, 71 | ) 72 | responses.add( 73 | responses.DELETE, 74 | "https://test.test/post-processings/pp-instance-one", 75 | status=204, 76 | ) 77 | 78 | global_coefficients = pred.post.global_coefficients() 79 | assert global_coefficients.id == "pp-instance-one" 80 | 81 | global_coefficients.delete() 82 | 83 | global_coefficients = pred.post.global_coefficients() 84 | assert global_coefficients.id == "pp-instance-two" 85 | 86 | 87 | @responses.activate 88 | def test_post_processing_surface_evolution_delete(prediction_factory): 89 | """WHEN deleting a SurfaceEvolution post-processing from a prediction 90 | THEN there is a call to the DELETE endpoint 91 | ALSO a new call to pred.post.surface_evolution() re-runs the post-processing 92 | """ 93 | pred = prediction_factory() 94 | responses.add( 95 | responses.POST, 96 | f"https://test.test/predictions/{pred.id}/post-processings/SurfaceEvol", 97 | json={ 98 | "id": "im-the-first-one", 99 | "state": "successful", 100 | "type": "SurfaceEvol", 101 | "location": {"axis": "y", "delta": 9.5}, 102 | }, 103 | status=200, 104 | ) 105 | responses.add( 106 | responses.POST, 107 | f"https://test.test/predictions/{pred.id}/post-processings/SurfaceEvol", 108 | json={ 109 | "id": "im-the-second-one", 110 | "state": "successful", 111 | "type": "SurfaceEvol", 112 | "location": {"axis": "y", "delta": 9.5}, 113 | }, 114 | status=200, 115 | ) 116 | responses.add( 117 | responses.DELETE, 118 | "https://test.test/post-processings/im-the-first-one", 119 | status=204, 120 | ) 121 | 122 | surface_evolution = pred.post.surface_evolution(axis="y", delta=9.5) 123 | assert surface_evolution.id == "im-the-first-one" 124 | 125 | surface_evolution.delete() 126 | 127 | surface_evolution = pred.post.surface_evolution(axis="y", delta=9.5) 128 | assert surface_evolution.id == "im-the-second-one" 129 | -------------------------------------------------------------------------------- /tests/test_post_processing_export.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import json 24 | 25 | import pytest 26 | import responses 27 | 28 | from ansys.simai.core.data.downloads import DownloadableResult 29 | from ansys.simai.core.data.post_processings import GlobalCoefficients 30 | from ansys.simai.core.data.selection_post_processings import ExportablePPList 31 | from ansys.simai.core.data.selections import Selection 32 | 33 | 34 | @pytest.fixture() 35 | def selection_factory(simai_client) -> Selection: 36 | """Returns a function to create a :py:class:`Selection`.""" 37 | 38 | def _factory(predictions=None, **kwargs) -> Selection: 39 | selection = Selection(geometries=[], boundary_conditions=[]) 40 | selection.get_predictions = lambda: predictions or [] 41 | return selection 42 | 43 | return _factory 44 | 45 | 46 | @responses.activate 47 | def test_post_processing_export_global_coefficients(simai_client): 48 | """WHEN I call export() on a GlobalCoefficients post-processing 49 | THEN I get a DownloadableResult object allowing me to download the content. 50 | """ 51 | 52 | pp = simai_client._post_processing_directory._model_from( 53 | {"id": "zebu", "type": "GlobalCoefficients", "state": "successful"} 54 | ) 55 | 56 | def request_callback(request): 57 | payload = json.loads(request.body) 58 | # check export format and id are as expected 59 | assert payload["format"] == "json" 60 | assert payload["ids"] == ["zebu"] 61 | return (200, {}, json.dumps({"data": "it's here"})) 62 | 63 | responses.add_callback( 64 | responses.POST, 65 | "https://test.test/post-processings/export", 66 | callback=request_callback, 67 | ) 68 | 69 | res = pp.export() 70 | assert isinstance(res, DownloadableResult) 71 | data = res.in_memory().readline() 72 | assert json.loads(data.decode("ascii")) == {"data": "it's here"} 73 | 74 | 75 | @responses.activate 76 | def test_post_processing_export_surface_evolution_excel(simai_client): 77 | """WHEN I call export() on a SurfaceEvolution post-processing 78 | THEN I get a DownloadableResult object allowing me to download the content. 79 | """ 80 | 81 | pp = simai_client._post_processing_directory._model_from( 82 | {"id": "mozeu", "type": "SurfaceEvolution", "state": "successful"} 83 | ) 84 | 85 | def request_callback(request): 86 | payload = json.loads(request.body) 87 | # check export format and id are as expected 88 | assert payload["format"] == "xlsx" 89 | assert payload["ids"] == ["mozeu"] 90 | return (200, {}, b"some binary excel data") 91 | 92 | responses.add_callback( 93 | responses.POST, 94 | "https://test.test/post-processings/export", 95 | callback=request_callback, 96 | ) 97 | 98 | res = pp.export(format="xlsx") 99 | assert isinstance(res, DownloadableResult) 100 | data = res.in_memory().readline() 101 | assert data == b"some binary excel data" 102 | 103 | 104 | @responses.activate 105 | def test_post_processing_selection_export( 106 | selection_factory, prediction_factory, post_processing_factory 107 | ): 108 | """WHEN I call export() on a selection.post.global_coefficients() 109 | THEN the post-processing/export is called with the ids of expected post-processings 110 | ALSO I get a DownloadableResult in return 111 | """ 112 | pp1 = post_processing_factory(type=GlobalCoefficients._api_name()) 113 | pp2 = post_processing_factory(type=GlobalCoefficients._api_name()) 114 | selection = selection_factory( 115 | predictions=[ 116 | prediction_factory( 117 | post_processings=[ 118 | pp1, 119 | ] 120 | ), 121 | prediction_factory( 122 | post_processings=[ 123 | pp2, 124 | ] 125 | ), 126 | ] 127 | ) 128 | 129 | def request_callback(request): 130 | payload = json.loads(request.body) 131 | # check export format and id are as expected 132 | assert payload["format"] == "xlsx" 133 | assert payload["ids"] == [pp1.id, pp2.id] 134 | return (200, {}, b"some binary excel data") 135 | 136 | responses.add_callback( 137 | responses.POST, 138 | "https://test.test/post-processings/export", 139 | callback=request_callback, 140 | ) 141 | post_processings = selection.post.global_coefficients() 142 | assert isinstance(post_processings, ExportablePPList) 143 | res = post_processings.export(format="xlsx") 144 | assert isinstance(res, DownloadableResult) 145 | data = res.in_memory().readline() 146 | assert data == b"some binary excel data" 147 | -------------------------------------------------------------------------------- /tests/test_tolerance_grouper.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import itertools 24 | 25 | from ansys.simai.core.utils.grouping import _ToleranceGroup, _ToleranceGrouper 26 | from ansys.simai.core.utils.numerical import is_equal_with_tolerance 27 | 28 | 29 | def test_tolerance_grouper_basic(): 30 | """WHEN using _ToleranceGrouper with a basic list and default or negligible tolerance, 31 | THEN each group contain distinct elements in the data list 32 | ALSO, forced_central_value has no effect. 33 | """ 34 | data = [1, 1, 2, 3, 3, 3, 4, 5] 35 | for tolerance in [None, 10**-7, 10**-1]: 36 | for forced_central_value in [None, 2, 2.1, 22]: 37 | grouper = _ToleranceGrouper( 38 | forced_central_value=forced_central_value, tolerance=tolerance 39 | ) 40 | result_groups = itertools.groupby(data, grouper) 41 | 42 | for i, group in enumerate(result_groups): 43 | num = i + 1 44 | bucket, items = group 45 | assert isinstance(bucket, _ToleranceGroup) 46 | assert bucket.center == num 47 | 48 | if num == 1: 49 | assert list(items) == 2 * [num] 50 | elif num == 3: 51 | assert list(items) == 3 * [num] 52 | else: 53 | assert list(items) == [num] 54 | 55 | 56 | def test_tolerance_grouper_tolerance(): 57 | """WHEN using tolerance grouper with tolerance 58 | THEN groups of values that can all be grouped around a central value 59 | (approx. equal to this central value) are grouped together 60 | """ 61 | data = [1, 1.001, 1.996, 2, 2.005, 4, 4, 4.005, 4.011, 4.019, 4.996, 5.002] 62 | tolerance = 10**-2 63 | grouper = _ToleranceGrouper(tolerance=tolerance) 64 | result_groups = itertools.groupby(data, grouper) 65 | 66 | for i, group in enumerate(result_groups): 67 | bucket, items = group 68 | assert isinstance(bucket, _ToleranceGroup) 69 | items = list(items) 70 | 71 | if i == 0: 72 | assert items == [1, 1.001] 73 | assert is_equal_with_tolerance(bucket.center, 1, tolerance=tolerance) 74 | elif i == 1: 75 | assert items == [1.996, 2, 2.005] 76 | assert is_equal_with_tolerance(bucket.center, 2, tolerance=tolerance) 77 | elif i == 2: 78 | # This bucket contains all those items, 79 | # as we can create a bucket around 4.005 : 4 ~= 4.005 ~= 4.011 80 | assert items == [4, 4, 4.005, 4.011] 81 | assert bucket.center == 4.005 82 | elif i == 3: 83 | assert items == [4.019] 84 | assert bucket.center == 4.019 85 | else: 86 | assert items == [4.996, 5.002] 87 | assert is_equal_with_tolerance(bucket.center, 5, tolerance=tolerance) 88 | 89 | 90 | def test_tolerance_grouper_tolerance_with_forced_central_value(): 91 | """WHEN using tolerance grouper with forced central value 92 | THEN those forced value must not be further than epsilon 93 | from their own bucket boundaries 94 | """ 95 | data = [4, 4, 4.005, 4.011, 4.019] 96 | tolerance = 10**-2 97 | for forced_central_value in [4, 4.011]: 98 | grouper = _ToleranceGrouper(tolerance=tolerance, forced_central_value=forced_central_value) 99 | result_groups = itertools.groupby(data, grouper) 100 | 101 | for i, group in enumerate(result_groups): 102 | bucket, items = group 103 | assert isinstance(bucket, _ToleranceGroup) 104 | 105 | if i == 0: 106 | assert list(items) == [4, 4, 4.005] 107 | if forced_central_value == 4: 108 | assert bucket.center == 4 109 | else: 110 | assert list(items) == [4.011, 4.019] 111 | -------------------------------------------------------------------------------- /tests/test_workspace.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2024 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from typing import TYPE_CHECKING 24 | 25 | import responses 26 | 27 | if TYPE_CHECKING: 28 | from ansys.simai.core.data.workspaces import Workspace 29 | 30 | 31 | @responses.activate 32 | def test_workspace_download_mer_data(simai_client): 33 | """WHEN downloading mer csv file 34 | THEN the content of the file matches the content of the response. 35 | """ 36 | 37 | workspace: Workspace = simai_client._workspace_directory._model_from( 38 | {"id": "0011", "name": "riri"} 39 | ) 40 | responses.add( 41 | responses.GET, 42 | f"https://test.test/workspaces/{workspace.id}/mer-data", 43 | body=b"mer-data-geometries", 44 | status=200, 45 | ) 46 | 47 | in_memory = workspace.download_mer_data() 48 | data_in_file = in_memory.readline() 49 | assert data_in_file.decode("ascii") == "mer-data-geometries" 50 | -------------------------------------------------------------------------------- /tests/utils/test_config.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import platform 24 | from pathlib import Path 25 | 26 | import pytest 27 | 28 | import ansys.simai.core.errors as err 29 | from ansys.simai.core.utils.config_file import _scan_defaults_config_paths, get_config 30 | 31 | 32 | @pytest.mark.skipif(platform.system() == "Windows", reason="XDG_CONFIG_HOME is for unix systems") 33 | def test_xdg_config_home_is_respected(monkeypatch, tmpdir): 34 | monkeypatch.setenv("XDG_CONFIG_HOME", str(tmpdir)) 35 | config_file_path = Path(tmpdir) / "ansys_simai.conf" 36 | config_file_path.touch() 37 | assert _scan_defaults_config_paths() == config_file_path 38 | 39 | 40 | def test_get_config_invalid_path(): 41 | with pytest.raises(err.ConfigurationNotFoundError): 42 | get_config(path="/") 43 | 44 | 45 | def test_get_config_invalid_profile(tmpdir): 46 | path_config = Path(tmpdir) / "config" 47 | with open(path_config, "w+") as f: 48 | f.write("[default]\n") 49 | f.flush() 50 | with pytest.raises(err.InvalidConfigurationError): 51 | get_config(profile="kaboom", path=path_config) 52 | 53 | 54 | def test_get_config_ignore_missing(mocker): 55 | mocker.patch( 56 | "ansys.simai.core.utils.config_file._scan_defaults_config_paths", 57 | return_value=None, 58 | ) 59 | config = get_config(ignore_missing=True, organization="kangarooooo") 60 | assert config == {"organization": "kangarooooo"} 61 | -------------------------------------------------------------------------------- /tests/utils/test_files.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import os 24 | import platform 25 | import time 26 | from pathlib import Path 27 | 28 | import pytest 29 | 30 | from ansys.simai.core.utils.files import get_cache_dir 31 | 32 | 33 | @pytest.mark.skipif( 34 | platform.system() != "Linux", 35 | reason="This test uses XDG_CACHE_HOME to avoid touching the host system", 36 | ) 37 | def test_get_cache_dir_linux(tmpdir, monkeypatch): 38 | monkeypatch.setenv("XDG_CACHE_HOME", str(tmpdir)) 39 | cache_dir = get_cache_dir() 40 | expected_cache_dir = Path(tmpdir).absolute() / "ansys" / "pysimai" 41 | assert cache_dir.absolute() == expected_cache_dir 42 | old_file = cache_dir / "old" 43 | new_file = cache_dir / "new" 44 | new_file.touch() 45 | old_file.touch() 46 | now = time.time() 47 | a_year_ago = now - 30000000 48 | os.utime(old_file, times=(now, a_year_ago)) 49 | cache_dir = get_cache_dir() 50 | assert new_file.is_file() 51 | assert not old_file.is_file() 52 | -------------------------------------------------------------------------------- /tests/utils/test_requests.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 ANSYS, Inc. and/or its affiliates. 2 | # SPDX-License-Identifier: MIT 3 | # 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import niquests 24 | import pytest 25 | from niquests.models import Response 26 | 27 | from ansys.simai.core.errors import ApiClientError, SimAIError 28 | from ansys.simai.core.utils.requests import handle_http_errors, handle_response 29 | 30 | 31 | def test_handle_http_errors(mocker): 32 | response_json_mock = mocker.patch("niquests.models.Response.json") 33 | raise_for_status_mock = mocker.patch("niquests.models.Response.raise_for_status") 34 | 35 | # Error without json body 36 | raise_for_status_mock.side_effect = niquests.exceptions.HTTPError(Response()) 37 | response_json_mock.side_effect = ValueError() 38 | 39 | with pytest.raises(SimAIError): 40 | handle_http_errors(Response()) 41 | 42 | # Error with json body 43 | raise_for_status_mock.side_effect = niquests.exceptions.HTTPError(Response()) 44 | response_json_mock.side_effect = None 45 | response_json_mock.return_value = {"status": "rekt"} 46 | 47 | with pytest.raises(SimAIError, match="rekt"): 48 | handle_http_errors(Response()) 49 | 50 | 51 | def test_handle_response(mocker): 52 | mocker.patch("ansys.simai.core.utils.requests.handle_http_errors") 53 | response_json_mock = mocker.patch("niquests.models.Response.json") 54 | 55 | # JSON response -> return json 56 | response_json_mock.return_value = {"status": "succeeding"} 57 | assert handle_response(Response(), return_json=True) == {"status": "succeeding"} 58 | 59 | # Non json response -> return response 60 | assert isinstance(handle_response(Response(), return_json=False), Response) 61 | 62 | # Malformed JSON -> error 63 | response_json_mock.side_effect = ValueError 64 | with pytest.raises(ApiClientError): 65 | handle_response(Response(), return_json=True) 66 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | description = Default tox environments list 3 | envlist = 4 | style,{py39,py310,py311,py312}{,-coverage},doc 5 | skip_missing_interpreters = true 6 | isolated_build = true 7 | isolated_build_env = build 8 | 9 | [testenv] 10 | description = Checks for project unit tests and coverage (if desired) 11 | runner = uv-venv-lock-runner 12 | dependency-groups = tests 13 | basepython = 14 | py39: python3.9 15 | py310: python3.10 16 | py311: python3.11 17 | py312: python3.12 18 | py313: python3.13 19 | py: python3 20 | {style,reformat,doc,build}: python3 21 | setenv = 22 | PYTHONUNBUFFERED = yes 23 | coverage: PYTEST_EXTRA_ARGS = --cov=ansys.simai --cov-report=term --cov-report=xml:.cov/xml --cov-report=html:.cov/html 24 | commands = 25 | pytest {env:PYTEST_MARKERS:} {env:PYTEST_EXTRA_ARGS:} {posargs:-vv} 26 | 27 | [testenv:style] 28 | description = Checks project code style 29 | skip_install = true 30 | deps = 31 | pre-commit 32 | commands = 33 | pre-commit install 34 | pre-commit run --all-files --show-diff-on-failure 35 | 36 | [testenv:doc] 37 | description = Check if documentation generates properly 38 | runner = uv-venv-lock-runner 39 | dependency-groups = docs 40 | commands = 41 | sphinx-build -d "{toxworkdir}/doc_doctree" doc/source "{toxinidir}/_build/html" --color -vW -bhtml 42 | --------------------------------------------------------------------------------