├── .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 |
--------------------------------------------------------------------------------