ansys-additive-core
Advanced tools
| # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| # SPDX-License-Identifier: MIT | ||
| # | ||
| # | ||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| # of this software and associated documentation files (the "Software"), to deal | ||
| # in the Software without restriction, including without limitation the rights | ||
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| # copies of the Software, and to permit persons to whom the Software is | ||
| # furnished to do so, subject to the following conditions: | ||
| # | ||
| # The above copyright notice and this permission notice shall be included in all | ||
| # copies or substantial portions of the Software. | ||
| # | ||
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| # SOFTWARE. | ||
| """Provides a class to run parametric study simulations.""" | ||
| from __future__ import annotations | ||
| import numpy as np | ||
| import pandas as pd | ||
| from ansys.additive.core import ( | ||
| LOG, | ||
| Additive, | ||
| AdditiveMachine, | ||
| AdditiveMaterial, | ||
| MachineConstants, | ||
| MicrostructureInput, | ||
| MicrostructureSummary, | ||
| PorosityInput, | ||
| PorositySummary, | ||
| SimulationStatus, | ||
| SimulationType, | ||
| SingleBeadInput, | ||
| SingleBeadSummary, | ||
| ) | ||
| from ansys.additive.core.parametric_study.constants import ColumnNames | ||
| class ParametricRunner: | ||
| """Provides methods to run parametric study simulations.""" | ||
| @staticmethod | ||
| def simulate( | ||
| df: pd.DataFrame, | ||
| additive: Additive, | ||
| type: list[SimulationType] = None, | ||
| priority: int = None, | ||
| iteration: int = None, | ||
| ) -> list[SingleBeadSummary, PorositySummary, MicrostructureSummary]: | ||
| """Run the simulations in the parametric study with ``Status`` equal to ``Pending``. | ||
| Execution order is determined by the ``Priority`` value assigned to the simulations. | ||
| Lower values are interpreted as having higher priority and are run first. | ||
| Parameters | ||
| ---------- | ||
| df : pd.DataFrame | ||
| Parametric study data frame. | ||
| additive : Additive | ||
| Additive service connection to use for running simulations. | ||
| type : list, default: None | ||
| List of the simulation types to run. The default is ``None``, in which case all | ||
| simulation types are run. | ||
| priority : int, default: None | ||
| Priority of simulations to run. The default is ``None``, in which case | ||
| all priorities are run. | ||
| iteration : int, default: None | ||
| Iteration number of simulations to run. The default is ``None``, in which case | ||
| all iterations are run. | ||
| Returns | ||
| ------- | ||
| list[SingleBeadSummary, PorositySummary, MicrostructureSummary] | ||
| List of simulation summaries. | ||
| """ | ||
| if type is None: | ||
| type = [ | ||
| SimulationType.SINGLE_BEAD, | ||
| SimulationType.POROSITY, | ||
| SimulationType.MICROSTRUCTURE, | ||
| ] | ||
| elif not isinstance(type, list): | ||
| type = [type] | ||
| view = df[ | ||
| (df[ColumnNames.STATUS] == SimulationStatus.PENDING) & df[ColumnNames.TYPE].isin(type) | ||
| ] | ||
| if priority is not None: | ||
| view = view[view[ColumnNames.PRIORITY] == priority] | ||
| view = view.sort_values(by=ColumnNames.PRIORITY, ascending=True) | ||
| if iteration is not None: | ||
| view = df[(df[ColumnNames.ITERATION] == iteration)] | ||
| inputs = [] | ||
| # NOTICE: We use iterrows() instead of itertuples() here to | ||
| # access values by column name | ||
| for _, row in view.iterrows(): | ||
| try: | ||
| material = additive.material(row[ColumnNames.MATERIAL]) | ||
| except Exception: | ||
| print( | ||
| f"Material {row[ColumnNames.MATERIAL]} not found, skipping {row[ColumnNames.ID]}" | ||
| ) | ||
| continue | ||
| machine = ParametricRunner._create_machine(row) | ||
| sim_type = row[ColumnNames.TYPE] | ||
| if sim_type == SimulationType.SINGLE_BEAD: | ||
| inputs.append(ParametricRunner._create_single_bead_input(row, material, machine)) | ||
| elif sim_type == SimulationType.POROSITY: | ||
| inputs.append(ParametricRunner._create_porosity_input(row, material, machine)) | ||
| elif sim_type == SimulationType.MICROSTRUCTURE: | ||
| inputs.append(ParametricRunner._create_microstructure_input(row, material, machine)) | ||
| else: # pragma: no cover | ||
| print( | ||
| f"Invalid simulation type: {row[ColumnNames.TYPE]} for {row[ColumnNames.ID]}, skipping" | ||
| ) | ||
| continue | ||
| if len(inputs) == 0: | ||
| LOG.warning("None of the input simulations meet the criteria selected") | ||
| return [] | ||
| summaries = additive.simulate(inputs) | ||
| # TODO: Return the summaries one at a time, possibly as an iterator, | ||
| # so that the data frame can be updated as each simulation completes. | ||
| return summaries | ||
| @staticmethod | ||
| def _create_machine(row: pd.Series) -> AdditiveMachine: | ||
| return AdditiveMachine( | ||
| laser_power=row[ColumnNames.LASER_POWER], | ||
| scan_speed=row[ColumnNames.SCAN_SPEED], | ||
| layer_thickness=row[ColumnNames.LAYER_THICKNESS], | ||
| beam_diameter=row[ColumnNames.BEAM_DIAMETER], | ||
| heater_temperature=row[ColumnNames.HEATER_TEMPERATURE], | ||
| starting_layer_angle=( | ||
| row[ColumnNames.START_ANGLE] | ||
| if not np.isnan(row[ColumnNames.START_ANGLE]) | ||
| else MachineConstants.DEFAULT_STARTING_LAYER_ANGLE | ||
| ), | ||
| layer_rotation_angle=( | ||
| row[ColumnNames.ROTATION_ANGLE] | ||
| if not np.isnan(row[ColumnNames.ROTATION_ANGLE]) | ||
| else MachineConstants.DEFAULT_LAYER_ROTATION_ANGLE | ||
| ), | ||
| hatch_spacing=( | ||
| row[ColumnNames.HATCH_SPACING] | ||
| if not np.isnan(row[ColumnNames.HATCH_SPACING]) | ||
| else MachineConstants.DEFAULT_HATCH_SPACING | ||
| ), | ||
| slicing_stripe_width=( | ||
| row[ColumnNames.STRIPE_WIDTH] | ||
| if not np.isnan(row[ColumnNames.STRIPE_WIDTH]) | ||
| else MachineConstants.DEFAULT_SLICING_STRIPE_WIDTH | ||
| ), | ||
| ) | ||
| @staticmethod | ||
| def _create_single_bead_input( | ||
| row: pd.Series, material: AdditiveMaterial, machine: AdditiveMachine | ||
| ) -> SingleBeadInput: | ||
| return SingleBeadInput( | ||
| id=row[ColumnNames.ID], | ||
| material=material, | ||
| machine=machine, | ||
| bead_length=row[ColumnNames.SINGLE_BEAD_LENGTH], | ||
| ) | ||
| @staticmethod | ||
| def _create_porosity_input( | ||
| row: pd.Series, material: AdditiveMaterial, machine: AdditiveMachine | ||
| ) -> PorosityInput: | ||
| return PorosityInput( | ||
| id=row[ColumnNames.ID], | ||
| material=material, | ||
| machine=machine, | ||
| size_x=row[ColumnNames.POROSITY_SIZE_X], | ||
| size_y=row[ColumnNames.POROSITY_SIZE_Y], | ||
| size_z=row[ColumnNames.POROSITY_SIZE_Z], | ||
| ) | ||
| @staticmethod | ||
| def _create_microstructure_input( | ||
| row: pd.Series, material: AdditiveMaterial, machine: AdditiveMachine | ||
| ) -> MicrostructureInput: | ||
| use_provided_thermal_param = ( | ||
| not np.isnan(row[ColumnNames.COOLING_RATE]) | ||
| or not np.isnan(row[ColumnNames.THERMAL_GRADIENT]) | ||
| or not np.isnan(row[ColumnNames.MICRO_MELT_POOL_WIDTH]) | ||
| or not np.isnan(row[ColumnNames.MICRO_MELT_POOL_DEPTH]) | ||
| ) | ||
| return MicrostructureInput( | ||
| id=row[ColumnNames.ID], | ||
| material=material, | ||
| machine=machine, | ||
| sample_size_x=row[ColumnNames.MICRO_SIZE_X], | ||
| sample_size_y=row[ColumnNames.MICRO_SIZE_Y], | ||
| sample_size_z=row[ColumnNames.MICRO_SIZE_Z], | ||
| sensor_dimension=row[ColumnNames.MICRO_SENSOR_DIM], | ||
| use_provided_thermal_parameters=use_provided_thermal_param, | ||
| sample_min_x=( | ||
| row[ColumnNames.MICRO_MIN_X] | ||
| if not np.isnan(row[ColumnNames.MICRO_MIN_X]) | ||
| else MicrostructureInput.DEFAULT_POSITION_COORDINATE | ||
| ), | ||
| sample_min_y=( | ||
| row[ColumnNames.MICRO_MIN_Y] | ||
| if not np.isnan(row[ColumnNames.MICRO_MIN_Y]) | ||
| else MicrostructureInput.DEFAULT_POSITION_COORDINATE | ||
| ), | ||
| sample_min_z=( | ||
| row[ColumnNames.MICRO_MIN_Z] | ||
| if not np.isnan(row[ColumnNames.MICRO_MIN_Z]) | ||
| else MicrostructureInput.DEFAULT_POSITION_COORDINATE | ||
| ), | ||
| cooling_rate=( | ||
| row[ColumnNames.COOLING_RATE] | ||
| if not np.isnan(row[ColumnNames.COOLING_RATE]) | ||
| else MicrostructureInput.DEFAULT_COOLING_RATE | ||
| ), | ||
| thermal_gradient=( | ||
| row[ColumnNames.THERMAL_GRADIENT] | ||
| if not np.isnan(row[ColumnNames.THERMAL_GRADIENT]) | ||
| else MicrostructureInput.DEFAULT_THERMAL_GRADIENT | ||
| ), | ||
| melt_pool_width=( | ||
| row[ColumnNames.MICRO_MELT_POOL_WIDTH] | ||
| if not np.isnan(row[ColumnNames.MICRO_MELT_POOL_WIDTH]) | ||
| else MicrostructureInput.DEFAULT_MELT_POOL_WIDTH | ||
| ), | ||
| melt_pool_depth=( | ||
| row[ColumnNames.MICRO_MELT_POOL_DEPTH] | ||
| if not np.isnan(row[ColumnNames.MICRO_MELT_POOL_DEPTH]) | ||
| else MicrostructureInput.DEFAULT_MELT_POOL_DEPTH | ||
| ), | ||
| random_seed=( | ||
| row[ColumnNames.RANDOM_SEED] | ||
| if not np.isnan(row[ColumnNames.RANDOM_SEED]) | ||
| else MicrostructureInput.DEFAULT_RANDOM_SEED | ||
| ), | ||
| ) |
+11
-8
| Metadata-Version: 2.4 | ||
| Name: ansys-additive-core | ||
| Version: 0.19.2 | ||
| Version: 0.18.3 | ||
| Summary: A Python client for the Ansys Additive service | ||
| Author-email: "ANSYS, Inc." <pyansys.core@ansys.com> | ||
| Maintainer-email: "ANSYS, Inc." <pyansys.core@ansys.com> | ||
| Requires-Python: >=3.10,<4 | ||
| Requires-Python: >=3.9,<4 | ||
| Description-Content-Type: text/x-rst | ||
@@ -18,3 +18,3 @@ Classifier: Development Status :: 4 - Beta | ||
| License-File: LICENSE | ||
| Requires-Dist: ansys-api-additive==2.2.1 | ||
| Requires-Dist: ansys-api-additive==1.7.2 | ||
| Requires-Dist: ansys-platform-instancemanagement>=1.1.1 | ||
@@ -29,9 +29,8 @@ Requires-Dist: ansys-tools-common>=0.4.0 | ||
| Requires-Dist: numpy>=1.20.3 | ||
| Requires-Dist: pandas>=2.2.2 | ||
| Requires-Dist: pandas>=1.3.2 | ||
| Requires-Dist: platformdirs>=3.8.0 | ||
| Requires-Dist: protobuf>=3.20.2,<6 | ||
| Requires-Dist: protobuf>=3.20.2,<5 | ||
| Requires-Dist: six>=1.16.0 | ||
| Requires-Dist: tqdm>=4.45.0 | ||
| Requires-Dist: pydantic>=2.6.3 | ||
| Requires-Dist: ipython>=7.0.0 | ||
| Requires-Dist: ansys-sphinx-theme[autoapi]==1.5.0 ; extra == "doc" | ||
@@ -69,4 +68,4 @@ Requires-Dist: enum-tools==0.12.0 ; extra == "doc" | ||
| Requires-Dist: pandas==2.2.3 ; extra == "tests" | ||
| Requires-Dist: platformdirs==4.3.6 ; extra == "tests" | ||
| Requires-Dist: protobuf==5.28.3 ; extra == "tests" | ||
| Requires-Dist: platformdirs==4.5.1 ; extra == "tests" | ||
| Requires-Dist: protobuf==4.25.3 ; extra == "tests" | ||
| Requires-Dist: six==1.16.0 ; extra == "tests" | ||
@@ -85,2 +84,4 @@ Requires-Dist: tqdm==4.66.5 ; extra == "tests" | ||
| .. _ref_readme: | ||
| ########## | ||
@@ -90,2 +91,4 @@ PyAdditive | ||
| .. readme_start | ||
| |pyansys| |python| |pypi| |GH-CI| |codecov| |MIT| |black| | ||
@@ -92,0 +95,0 @@ |
+44
-149
| [build-system] | ||
| requires = ["flit_core >=3.2,<4"] | ||
| build-backend = "flit_core.buildapi" | ||
| requires = ["flit_core >=3.2,<4"] | ||
| [project] | ||
| # Check https://flit.readthedocs.io/en/latest/pyproject_toml.html for all available sections | ||
| authors = [{name = "ANSYS, Inc.", email = "pyansys.core@ansys.com"}] | ||
| name = "ansys-additive-core" | ||
| version = "0.18.3" | ||
| description = "A Python client for the Ansys Additive service" | ||
| readme = "README.rst" | ||
| requires-python = ">=3.9,<4" | ||
| license = { file = "LICENSE" } | ||
| authors = [{ name = "ANSYS, Inc.", email = "pyansys.core@ansys.com" }] | ||
| maintainers = [{ name = "ANSYS, Inc.", email = "pyansys.core@ansys.com" }] | ||
| classifiers = [ | ||
| "Development Status :: 4 - Beta", | ||
| "Intended Audience :: Manufacturing", | ||
| "Topic :: Scientific/Engineering", | ||
| "License :: OSI Approved :: MIT License", | ||
| "Operating System :: OS Independent", | ||
| "Programming Language :: Python :: 3.10", | ||
| "Programming Language :: Python :: 3.11", | ||
| "Programming Language :: Python :: 3.12", | ||
| "Development Status :: 4 - Beta", | ||
| "Intended Audience :: Manufacturing", | ||
| "Topic :: Scientific/Engineering", | ||
| "License :: OSI Approved :: MIT License", | ||
| "Operating System :: OS Independent", | ||
| "Programming Language :: Python :: 3.10", | ||
| "Programming Language :: Python :: 3.11", | ||
| "Programming Language :: Python :: 3.12", | ||
| ] | ||
| description = "A Python client for the Ansys Additive service" | ||
| license = {file = "LICENSE"} | ||
| maintainers = [{name = "ANSYS, Inc.", email = "pyansys.core@ansys.com"}] | ||
| name = "ansys-additive-core" | ||
| readme = "README.rst" | ||
| requires-python = ">=3.10,<4" | ||
| version = "0.19.2" | ||
| dependencies = [ | ||
| "ansys-api-additive==2.2.1", | ||
| "ansys-platform-instancemanagement>=1.1.1", | ||
| "ansys-tools-common>=0.4.0", | ||
| "dill>=0.3.7", | ||
| "google-api-python-client>=1.7.11", | ||
| "googleapis-common-protos>=1.52.0", | ||
| "grpcio>=1.63.0", | ||
| "grpcio-health-checking>=1.60.0", | ||
| "importlib-metadata>=4.0", | ||
| "numpy>=1.20.3", | ||
| "pandas>=2.2.2", | ||
| "platformdirs>=3.8.0", | ||
| "protobuf>=3.20.2,<6", | ||
| "six>=1.16.0", | ||
| "tqdm>=4.45.0", | ||
| "pydantic>=2.6.3", | ||
| "ipython>=7.0.0", | ||
| "ansys-api-additive==1.7.2", | ||
| "ansys-platform-instancemanagement>=1.1.1", | ||
| "ansys-tools-common>=0.4.0", | ||
| "dill>=0.3.7", | ||
| "google-api-python-client>=1.7.11", | ||
| "googleapis-common-protos>=1.52.0", | ||
| "grpcio>=1.63.0", | ||
| "grpcio-health-checking>=1.60.0", | ||
| "importlib-metadata>=4.0", | ||
| "numpy>=1.20.3", | ||
| "pandas>=1.3.2", | ||
| "platformdirs>=3.8.0", | ||
| "protobuf>=3.20.2,<5", | ||
| "six>=1.16.0", | ||
| "tqdm>=4.45.0", | ||
| "pydantic>=2.6.3", | ||
| ] | ||
@@ -56,4 +55,4 @@ | ||
| "pandas==2.2.3", | ||
| "platformdirs==4.3.6", | ||
| "protobuf==5.28.3", | ||
| "platformdirs==4.5.1", | ||
| "protobuf==4.25.3", | ||
| "six==1.16.0", | ||
@@ -94,7 +93,7 @@ "tqdm==4.66.5", | ||
| [project.urls] | ||
| Source = "https://github.com/ansys/pyadditive" | ||
| Issues = "https://github.com/ansys/pyadditive/issues" | ||
| Documentation = "https://additive.docs.pyansys.com" | ||
| Discussions = "https://github.com/ansys/pyadditive/discussions" | ||
| Documentation = "https://additive.docs.pyansys.com" | ||
| Issues = "https://github.com/ansys/pyadditive/issues" | ||
| Releases = "https://github.com/ansys/pyadditive/releases" | ||
| Source = "https://github.com/ansys/pyadditive" | ||
@@ -108,6 +107,6 @@ [tool.flit.module] | ||
| [tool.isort] | ||
| default_section = "THIRDPARTY" | ||
| profile = "black" | ||
| force_sort_within_sections = true | ||
| line_length = 100 | ||
| profile = "black" | ||
| default_section = "THIRDPARTY" | ||
| src_paths = ["doc", "src", "tests"] | ||
@@ -122,116 +121,12 @@ | ||
| [tool.pytest.ini_options] | ||
| minversion = "7.1" | ||
| addopts = "-ra --cov=ansys.additive.core --cov-report html:.cov/html --cov-report xml:.cov/xml --cov-report term -vv --cov-fail-under 95" | ||
| testpaths = ["tests"] | ||
| filterwarnings = ["ignore:::.*protoc_gen_swagger*"] | ||
| minversion = "7.1" | ||
| testpaths = ["tests"] | ||
| [tool.interrogate] | ||
| ignore-magic = true | ||
| ignore-semiprivate = true | ||
| ignore-private = true | ||
| ignore-semiprivate = true | ||
| ignore-setters = true | ||
| ignore-magic = true | ||
| verbose = 1 | ||
| [tool.ruff] | ||
| exclude = ["doc", "examples", "tests"] | ||
| line-length = 100 | ||
| target-version = "py310" | ||
| [tool.ruff.lint] | ||
| ignore = [ | ||
| # "D100", # pydocstyle - missing docstring in public module | ||
| # "D101", # pydocstyle - missing docstring in public class | ||
| # "D102", # pydocstyle - missing docstring in public method | ||
| # "D103", # pydocstyle - missing docstring in public function | ||
| "D104", # pydocstyle - missing docstring in public package | ||
| "D105", # pydocstyle - missing docstring in magic method | ||
| "D106", # pydocstyle - missing docstring in public nested class | ||
| # "D107", # pydocstyle - missing docstring in __init__ | ||
| "D202", # pydocstyle - no blank lines allowed after function docstring | ||
| "D203", # pydocstyle - 1 blank line required before class docstring | ||
| "D204", # pydocstyle - 1 blank line required after class docstring | ||
| "D205", # pydocstyle - 1 blank line required between summary line and description | ||
| "D212", # pydocstyle - multi-line docstring summary should start at the first line | ||
| "D213", # pydocstyle - multi-line docstring summary should start at the second line | ||
| "E501", # pycodestyle line too long, handled by formatting | ||
| "ISC001", # Ruff formatter incompatible | ||
| "S101", # flake8-bandit - use of assert | ||
| "ERA001", # eradicate - commented out code | ||
| ] | ||
| select = [ | ||
| # "A", # flake8-builtins | ||
| # "ANN", # flake8-annotations | ||
| # "ARG", # flake8-unused-arguments | ||
| "ASYNC", # flake8-async | ||
| "B", # flake8-bugbear | ||
| # "BLE", # flake8-blind-except | ||
| "C4", # flake8-comprehensions | ||
| # "C90", # mccabe | ||
| # "CPY", # flake8-copyright | ||
| "D", # pydocstyle | ||
| # "DJ", # flake8-django | ||
| # "DTZ", # flake8-datetimez | ||
| "E", # pycodestyle errors | ||
| # "EM", # flake8-errmsg | ||
| "ERA", # eradicate | ||
| "EXE", # flake8-executable | ||
| "F", # pyflakes | ||
| # "FA", # flake8-future-annotations | ||
| # "FBT", # flake8-boolean-trap | ||
| # "FIX", # flake8-fixme | ||
| "FLY", # flying | ||
| # "FURB", # refurb | ||
| # "G", # flake8-logging-format | ||
| "I", # isort | ||
| "ICN", # flake8-import-conventions | ||
| "ISC", # flake8-implicit-str-concat | ||
| "INP", # flake8-no-pep420 | ||
| "LOG", # flake8-logging | ||
| # "N", # pep8-naming | ||
| # "PD", # pandas-vet | ||
| "PIE", # flake8-pie | ||
| "PLC", # pylint - convention | ||
| "PLE", # pylint - error | ||
| # "PLR", # pylint - refactor | ||
| "PLW", # pylint - warning | ||
| # "PT", # flake8-pytest-style | ||
| # "PTH", # flake8-use-pathlib | ||
| "PYI", # flake8-pyi | ||
| "Q", # flake8-quotes | ||
| # "RET", # flake8-return | ||
| "RSE", # flake8-raise | ||
| # "RUF", # Ruff-specific rules | ||
| "S", # flake8-bandit | ||
| "SIM", # flake8-simplify | ||
| # "SLF", # flake8-self | ||
| "SLOT", # flake8-slot | ||
| "T10", # flake8-debugger | ||
| "T20", # flake8-print | ||
| "TCH", # flake8-type-checking | ||
| # "TD", # flake8-todos | ||
| "TID", # flake8-tidy-imports | ||
| # "TRIO", # flake8-trio | ||
| # "TRY", # tryceratops | ||
| # "UP", # pyupgrade | ||
| "W", # pycodestyle - warning | ||
| "YTT", # flake8-2020 | ||
| ] | ||
| [tool.ruff.lint.per-file-ignores] | ||
| "dev/scripts/*" = [ | ||
| "D", # all docstring rules | ||
| "INP001", # implicit namespace package | ||
| ] | ||
| "examples/*" = [ | ||
| "D", # all docstring rules | ||
| "E402", # module level import not at top of file | ||
| "T201", # print statement | ||
| ] | ||
| "src/ansys/additive/core/logger.py" = [ | ||
| "PLW0642", # reassigned self | ||
| ] | ||
| "tests/*" = ["D"] | ||
| [tool.ruff.format] | ||
| indent-style = "space" | ||
| quote-style = "double" |
+4
-0
@@ -0,1 +1,3 @@ | ||
| .. _ref_readme: | ||
| ########## | ||
@@ -5,2 +7,4 @@ PyAdditive | ||
| .. readme_start | ||
| |pyansys| |python| |pypi| |GH-CI| |codecov| |MIT| |black| | ||
@@ -7,0 +11,0 @@ |
@@ -23,3 +23,2 @@ # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| """Python client for the Ansys Additive service.""" | ||
| import os | ||
@@ -79,4 +78,2 @@ | ||
| ) | ||
| from ansys.additive.core.simulation_task import SimulationTask # noqa: F401, E402 | ||
| from ansys.additive.core.simulation_task_manager import SimulationTaskManager # noqa: F401, E402 | ||
| from ansys.additive.core.single_bead import ( # noqa: F401, E402 | ||
@@ -83,0 +80,0 @@ MeltPool, |
@@ -24,32 +24,28 @@ # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| from __future__ import annotations | ||
| from collections.abc import Iterator | ||
| import concurrent.futures | ||
| from datetime import datetime | ||
| import hashlib | ||
| import logging | ||
| import os | ||
| import time | ||
| from pathlib import Path | ||
| import zipfile | ||
| from ansys.api.additive import __version__ as api_version | ||
| from ansys.api.additive.v0.additive_materials_pb2 import GetMaterialRequest | ||
| from ansys.api.additive.v0.additive_simulation_pb2 import UploadFileRequest | ||
| from google.protobuf.empty_pb2 import Empty | ||
| import grpc | ||
| from google.longrunning.operations_pb2 import Operation | ||
| from google.protobuf.empty_pb2 import Empty | ||
| import ansys.additive.core.misc as misc | ||
| from ansys.additive.core import USER_DATA_PATH, __version__ | ||
| from ansys.additive.core.download import download_file | ||
| from ansys.additive.core.exceptions import BetaFeatureNotEnabledError | ||
| from ansys.additive.core.logger import LOG | ||
| from ansys.additive.core.material import RESERVED_MATERIAL_NAMES, AdditiveMaterial | ||
| from ansys.additive.core.material_tuning import ( | ||
| MaterialTuningInput, | ||
| MaterialTuningSummary, | ||
| ) | ||
| from ansys.additive.core.microstructure import ( | ||
| MicrostructureInput, | ||
| MicrostructureSummary, | ||
| ) | ||
| from ansys.additive.core.microstructure_3d import ( | ||
| Microstructure3DInput, | ||
| Microstructure3DSummary, | ||
| ) | ||
| from ansys.additive.core.parametric_study import ParametricStudy | ||
| from ansys.additive.core.parametric_study.parametric_study_progress_handler import ( | ||
| ParametricStudyProgressHandler, | ||
| ) | ||
| from ansys.additive.core.material import AdditiveMaterial | ||
| from ansys.additive.core.material_tuning import MaterialTuningInput, MaterialTuningSummary | ||
| from ansys.additive.core.microstructure import MicrostructureInput, MicrostructureSummary | ||
| from ansys.additive.core.microstructure_3d import Microstructure3DInput, Microstructure3DSummary | ||
| import ansys.additive.core.misc as misc | ||
| from ansys.additive.core.porosity import PorosityInput, PorositySummary | ||
@@ -59,29 +55,10 @@ from ansys.additive.core.progress_handler import ( | ||
| IProgressHandler, | ||
| Progress, | ||
| ProgressState, | ||
| ) | ||
| from ansys.additive.core.server_connection import ( | ||
| DEFAULT_PRODUCT_VERSION, | ||
| ServerConnection, | ||
| ) | ||
| from ansys.additive.core.server_connection import DEFAULT_PRODUCT_VERSION, ServerConnection | ||
| from ansys.additive.core.server_connection.constants import TransportMode | ||
| from ansys.additive.core.simulation import ( | ||
| SimulationError, | ||
| SimulationStatus, | ||
| SimulationType, | ||
| ) | ||
| from ansys.additive.core.simulation_requests import create_request | ||
| from ansys.additive.core.simulation_task import SimulationTask | ||
| from ansys.additive.core.simulation_task_manager import SimulationTaskManager | ||
| from ansys.additive.core.simulation import SimulationError | ||
| from ansys.additive.core.single_bead import SingleBeadInput, SingleBeadSummary | ||
| from ansys.additive.core.thermal_history import ( | ||
| ThermalHistoryInput, | ||
| ThermalHistorySummary, | ||
| ) | ||
| from ansys.api.additive import __version__ as api_version | ||
| from ansys.api.additive.v0.additive_materials_pb2 import ( | ||
| AddMaterialRequest, | ||
| GetMaterialRequest, | ||
| RemoveMaterialRequest, | ||
| ) | ||
| from ansys.api.additive.v0.additive_operations_pb2 import OperationMetadata | ||
| from ansys.api.additive.v0.additive_settings_pb2 import SettingsRequest | ||
| from ansys.additive.core.thermal_history import ThermalHistoryInput, ThermalHistorySummary | ||
@@ -92,8 +69,17 @@ | ||
| In a typical cloud environment, a single Additive service with load balancing and | ||
| auto-scaling is used. The ``Additive`` client connects to the service via a | ||
| single connection. However, for atypical environments or when running on localhost, | ||
| the ``Additive`` client can perform crude load balancing by connecting to multiple | ||
| servers and distributing simulations across them. You can use the ``server_connections``, | ||
| ``nservers``, and ``nsims_per_server`` parameters to control the | ||
| number of servers to connect to and the number of simulations to run on each | ||
| server. | ||
| Parameters | ||
| ---------- | ||
| channel: grpc.Channel, default: None | ||
| Server connection. If provided, it is assumed that the | ||
| :class:`grpc.Channel <grpc.Channel>` object is connected to the server. | ||
| Also, if provided, the ``host`` and ``port`` parameters are ignored. | ||
| server_connections: list[str, grpc.Channel], None | ||
| List of connection definitions for servers. The list may be a combination of strings and | ||
| connected :class:`grpc.Channel <grpc.Channel>` objects. Strings use the format | ||
| ``host:port`` to specify the server IPv4 address. | ||
| host: str, default: None | ||
@@ -105,5 +91,11 @@ Host name or IPv4 address of the server. This parameter is ignored if the | ||
| nsims_per_server: int, default: 1 | ||
| Number of simultaneous simulations to run on the server. Each simulation | ||
| Number of simultaneous simulations to run on each server. Each simulation | ||
| requires a license checkout. If a license is not available, the simulation | ||
| fails. | ||
| nservers: int, default: 1 | ||
| Number of Additive servers to start and connect to. This parameter is only | ||
| applicable in `PyPIM`_-enabled cloud environments and on localhost. For | ||
| this to work on localhost, the Additive portion of the Ansys Structures | ||
| package must be installed. This parameter is ignored if the ``server_connections`` | ||
| parameter or ``host`` parameter is other than ``None``. | ||
| product_version: str | ||
@@ -115,5 +107,4 @@ Version of the Ansys product installation in the form ``"YYR"``, where ``YY`` | ||
| or ``None`` uses the default product version. | ||
| log_level: str, default: "" | ||
| Minimum severity level of messages to log. Valid values are "DEBUG", "INFO", | ||
| "WARNING", "ERROR", and "CRITICAL". The default value equates to "WARNING". | ||
| log_level: str, default: "INFO" | ||
| Minimum severity level of messages to log. | ||
| log_file: str, default: "" | ||
@@ -123,3 +114,3 @@ File name to write log messages to. | ||
| Flag indicating if beta features are enabled. | ||
| linux_install_path: os.PathLike, None, default: None | ||
| linux_install_path: os.PathLike, None default: None | ||
| Path to the Ansys installation directory on Linux. This parameter is only | ||
@@ -130,3 +121,4 @@ required when Ansys has not been installed in the default location. Example: | ||
| transport_mode : TransportMode | str, default: TransportMode.UDS | ||
| The transport mode to use for the connection. Can be a member of the :class:`TransportMode <.constants.TransportMode>` enum or a string | ||
| The transport mode to use for the connection. Can be a member of the :class:`TransportMode | ||
| <.constants.TransportMode>` enum or a string | ||
| ('insecure', 'mtls', or 'uds'). | ||
@@ -158,7 +150,7 @@ certs_dir : Path | str | None | ||
| Start and connect to a server on localhost or in a | ||
| `PyPIM`_-enabled cloud environment. Allow two simultaneous | ||
| simulations on the server. | ||
| Start and connect to two servers on localhost or in a | ||
| `PyPIM`_-enabled cloud environment. Allow each server to run two | ||
| simultaneous simulations. | ||
| >>> additive = Additive(nsims_per_server=2) | ||
| >>> additive = Additive(nsims_per_server=2, nservers=2) | ||
@@ -171,3 +163,2 @@ Start a single server on localhost or in a `PyPIM`_-enabled cloud environment. | ||
| .. _PyPIM: https://pypim.docs.pyansys.com/version/stable/index.html | ||
| """ | ||
@@ -179,8 +170,9 @@ | ||
| self, | ||
| channel: grpc.Channel | None = None, | ||
| server_connections: list[str | grpc.Channel] = None, | ||
| host: str | None = None, | ||
| port: int = DEFAULT_ADDITIVE_SERVICE_PORT, | ||
| nsims_per_server: int = 1, | ||
| nservers: int = 1, | ||
| product_version: str = DEFAULT_PRODUCT_VERSION, | ||
| log_level: str = "", | ||
| log_level: str = "INFO", | ||
| log_file: str = "", | ||
@@ -196,16 +188,15 @@ enable_beta_features: bool = False, | ||
| """Initialize server connections.""" | ||
| if not product_version: | ||
| if product_version is None or product_version == "": | ||
| product_version = DEFAULT_PRODUCT_VERSION | ||
| if log_level: | ||
| LOG.setLevel(log_level) | ||
| if log_file: | ||
| LOG.log_to_file(filename=log_file, level=log_level) | ||
| self._log = Additive._create_logger(log_file, log_level) | ||
| self._log.debug("Logging set to %s", log_level) | ||
| self._server = Additive._connect_to_server( | ||
| channel, | ||
| self._servers = Additive._connect_to_servers( | ||
| server_connections, | ||
| host, | ||
| port, | ||
| nservers, | ||
| product_version, | ||
| LOG, | ||
| self._log, | ||
| linux_install_path, | ||
@@ -218,10 +209,3 @@ transport_mode, | ||
| ) | ||
| # HACK: Set the number of concurrent simulations per server | ||
| # when generating documentation to reduce time. | ||
| if os.getenv("GENERATING_DOCS", None): | ||
| nsims_per_server = 8 | ||
| initial_settings = {"NumConcurrentSims": str(nsims_per_server)} | ||
| LOG.info(self.apply_server_settings(initial_settings)) | ||
| self._nsims_per_server = nsims_per_server | ||
| self._enable_beta_features = enable_beta_features | ||
@@ -236,6 +220,29 @@ | ||
| @staticmethod | ||
| def _connect_to_server( | ||
| channel: grpc.Channel | None = None, | ||
| def _create_logger(log_file, log_level) -> logging.Logger: | ||
| """Instantiate the logger.""" | ||
| format = "%(asctime)s %(message)s" | ||
| datefmt = "%Y-%m-%d %H:%M:%S" | ||
| numeric_level = getattr(logging, log_level.upper(), None) | ||
| if not isinstance(numeric_level, int): | ||
| raise ValueError("Invalid log level: %s" % log_level) | ||
| logging.basicConfig( | ||
| level=numeric_level, | ||
| format=format, | ||
| datefmt=datefmt, | ||
| ) | ||
| log = logging.getLogger(__name__) | ||
| if log_file: | ||
| file_handler = logging.FileHandler(str(log_file)) | ||
| file_handler.setLevel(numeric_level) | ||
| file_handler.setFormatter(logging.Formatter(format)) | ||
| log.file_handler = file_handler | ||
| log.addHandler(file_handler) | ||
| return log | ||
| @staticmethod | ||
| def _connect_to_servers( | ||
| server_connections: list[str | grpc.Channel] = None, | ||
| host: str | None = None, | ||
| port: int = DEFAULT_ADDITIVE_SERVICE_PORT, | ||
| nservers: int = 1, | ||
| product_version: str = DEFAULT_PRODUCT_VERSION, | ||
@@ -249,21 +256,25 @@ log: logging.Logger | None = None, | ||
| allow_remote_host: bool = False, | ||
| ) -> ServerConnection: | ||
| """Connect to an Additive server, starting it if necessary. | ||
| ) -> list[ServerConnection]: | ||
| """Connect to Additive servers. Start them if necessary. | ||
| Parameters | ||
| ---------- | ||
| channel: grpc.Channel, default: None | ||
| Server connection. If provided, it is assumed to be connected | ||
| and the ``host`` and ``port`` parameters are ignored. | ||
| server_connections: list[str, grpc.Channel], None | ||
| List of connection definitions for servers. The list may be a combination of strings and | ||
| connected :class:`grpc.Channel <grpc.Channel>` objects. Strings use the format | ||
| "host:port". | ||
| host: str, default: None | ||
| Host name or IPv4 address of the server. This parameter is ignored if | ||
| the ``channel`` parameter is other than ``None``. | ||
| Host name or IPv4 address of the server. This parameter is ignored if the | ||
| ``server_channels`` or ``channel`` parameters is other than ``None``. | ||
| port: int, default: 50052 | ||
| Port number to use when connecting to the server. | ||
| nservers: int, default: 1 | ||
| Number of servers to connect to or start if necessary. | ||
| product_version: str | ||
| Version of the Ansys product installation in the form ``"YYR"``, where ``YY`` | ||
| is the two-digit year and ``R`` is the release number. For example, "251". | ||
| This parameter is only applicable in `PyPIM`_-enabled cloud environments and | ||
| on localhost. Using an empty string or ``None`` uses the default product version. | ||
| log: logging.Logger, default: None | ||
| Version of the Ansys product installation in the form "YYR", where "YY" is the | ||
| two-digit year and "R" is the release number. For example, the release 2024 R1 | ||
| would be specified as "241". This parameter is only applicable in `PyPIM`-enabled | ||
| cloud environments and on localhost. Using an empty string or ``None`` uses the | ||
| default product version. | ||
| log: logging.Logger, None | ||
| Logger to use for logging messages. | ||
@@ -276,3 +287,4 @@ linux_install_path: os.PathLike, None, default: None | ||
| transport_mode : TransportMode | str | ||
| The transport mode to use for the connection. Can be a member of the :class:`TransportMode <.constants.TransportMode>` enum or a string | ||
| The transport mode to use for the connection. Can be a member of the :class:`TransportMode | ||
| <.constants.TransportMode>` enum or a string | ||
| ('insecure', 'mtls', or 'uds'). | ||
@@ -290,48 +302,74 @@ certs_dir : Path | str | None | ||
| ------- | ||
| ServerConnection | ||
| Connection to the server. | ||
| NOTE: If ``channel`` and ``host`` are not provided and the environment variable | ||
| ``ANSYS_ADDITIVE_ADDRESS`` is set, the client will connect to the server at the | ||
| address specified by the environment variable. The value of the environment variable | ||
| should be in the form ``host:port``. | ||
| list[ServerConnection] | ||
| List of connected servers. | ||
| """ | ||
| if channel: | ||
| if not isinstance(channel, grpc.Channel): | ||
| raise ValueError("channel must be a grpc.Channel object") | ||
| return ServerConnection(channel=channel, log=log, allow_remote_host=allow_remote_host) | ||
| connections = [] | ||
| if server_connections: | ||
| for target in server_connections: | ||
| if isinstance(target, grpc.Channel): | ||
| connections.append( | ||
| ServerConnection( | ||
| channel=target, | ||
| log=log, | ||
| allow_remote_host=allow_remote_host, | ||
| ) | ||
| ) | ||
| else: | ||
| connections.append( | ||
| ServerConnection(addr=target, log=log, allow_remote_host=allow_remote_host) | ||
| ) | ||
| elif host: | ||
| return ServerConnection( | ||
| addr=f"{host}:{port}", | ||
| log=log, | ||
| transport_mode=transport_mode, | ||
| certs_dir=certs_dir, | ||
| uds_dir=uds_dir, | ||
| uds_id=uds_id, | ||
| allow_remote_host=allow_remote_host, | ||
| connections.append( | ||
| ServerConnection( | ||
| addr=f"{host}:{port}", | ||
| log=log, | ||
| transport_mode=transport_mode, | ||
| certs_dir=certs_dir, | ||
| uds_dir=uds_dir, | ||
| uds_id=uds_id, | ||
| allow_remote_host=allow_remote_host, | ||
| ) | ||
| ) | ||
| elif os.getenv("ANSYS_ADDITIVE_ADDRESS"): | ||
| return ServerConnection( | ||
| addr=os.getenv("ANSYS_ADDITIVE_ADDRESS"), | ||
| log=log, | ||
| transport_mode=transport_mode, | ||
| certs_dir=certs_dir, | ||
| uds_dir=uds_dir, | ||
| uds_id=uds_id, | ||
| allow_remote_host=allow_remote_host, | ||
| connections.append( | ||
| ServerConnection( | ||
| addr=os.getenv("ANSYS_ADDITIVE_ADDRESS"), | ||
| log=log, | ||
| transport_mode=transport_mode, | ||
| certs_dir=certs_dir, | ||
| uds_dir=uds_dir, | ||
| uds_id=uds_id, | ||
| allow_remote_host=allow_remote_host, | ||
| ) | ||
| ) | ||
| else: | ||
| return ServerConnection( | ||
| product_version=product_version, | ||
| log=log, | ||
| linux_install_path=linux_install_path, | ||
| transport_mode=transport_mode, | ||
| certs_dir=certs_dir, | ||
| uds_dir=uds_dir, | ||
| uds_id=uds_id, | ||
| allow_remote_host=allow_remote_host, | ||
| ) | ||
| for _ in range(nservers): | ||
| connections.append( | ||
| ServerConnection( | ||
| product_version=product_version, | ||
| log=log, | ||
| linux_install_path=linux_install_path, | ||
| transport_mode=transport_mode, | ||
| certs_dir=certs_dir, | ||
| uds_dir=uds_dir, | ||
| uds_id=uds_id, | ||
| allow_remote_host=allow_remote_host, | ||
| ) | ||
| ) | ||
| return connections | ||
| @property | ||
| def nsims_per_server(self) -> int: | ||
| """Number of simultaneous simulations to run on each server.""" | ||
| return self._nsims_per_server | ||
| @nsims_per_server.setter | ||
| def nsims_per_server(self, value: int) -> None: | ||
| """Set the number of simultaneous simulations to run on each server.""" | ||
| if value < 1: | ||
| raise ValueError("Number of simulations per server must be greater than zero.") | ||
| self._nsims_per_server = value | ||
| @property | ||
| def enable_beta_features(self) -> bool: | ||
@@ -346,65 +384,17 @@ """Flag indicating if beta features are enabled.""" | ||
| @property | ||
| def connected(self) -> bool: | ||
| """Return True if the client is connected to a server.""" | ||
| return self._server.status().connected | ||
| def about(self) -> None: | ||
| """Print information about the client and server.""" | ||
| print(f"Client {__version__}, API version: {api_version}") | ||
| if self._servers is None: | ||
| print("Client is not connected to a server.") | ||
| return | ||
| else: | ||
| for server in self._servers: | ||
| print(server.status()) | ||
| @property | ||
| def uds_file(self) -> Path | None: | ||
| def uds_files(self) -> list[Path] | None: | ||
| """Path to the Unix Domain Socket file if using 'uds' transport mode, otherwise None.""" | ||
| return self._server.uds_file | ||
| return [server.uds_file for server in self._servers if server.uds_file is not None] | ||
| def about(self) -> str: | ||
| """Return information about the client and server. | ||
| Returns | ||
| ------- | ||
| str | ||
| Information about the client and server. | ||
| """ | ||
| about = ( | ||
| f"ansys.additive.core version {__version__}\nClient side API version: {api_version}\n" | ||
| ) | ||
| if self._server is None: | ||
| about += "Client is not connected to a server.\n" | ||
| else: | ||
| about += str(self._server.status()) + "\n" | ||
| return about | ||
| def apply_server_settings(self, settings: dict[str, str]) -> list[str]: | ||
| """Apply settings to each server. | ||
| Current settings include: | ||
| - ``NumConcurrentSims``: number of concurrent simulations per server. | ||
| Parameters | ||
| ---------- | ||
| settings: dict[str, str] | ||
| Dictionary of settings to apply to the server. | ||
| Returns | ||
| ------- | ||
| list[str] | ||
| List of messages from the server. | ||
| """ | ||
| request = SettingsRequest() | ||
| for setting_key, setting_value in settings.items(): | ||
| setting = request.settings.add() | ||
| setting.key = setting_key | ||
| setting.value = setting_value | ||
| response = self._server.settings_stub.ApplySettings(request) | ||
| return response.messages | ||
| def list_server_settings(self) -> dict[str, str]: | ||
| """Get a dictionary of settings for the server.""" | ||
| response = self._server.settings_stub.ListSettings(Empty()) | ||
| settings = {} | ||
| for setting in response.settings: | ||
| settings[setting.key] = setting.value | ||
| return settings | ||
| def simulate( | ||
@@ -434,3 +424,4 @@ self, | ||
| ---------- | ||
| inputs: SingleBeadInput, PorosityInput, MicrostructureInput, ThermalHistoryInput, Microstructure3DInput, list | ||
| inputs: SingleBeadInput, PorosityInput, MicrostructureInput, ThermalHistoryInput, | ||
| Microstructure3DInput, list | ||
| Parameters to use for simulations. A list of inputs may be provided to run multiple | ||
@@ -448,56 +439,9 @@ simulations. | ||
| list is returned. | ||
| """ # noqa: E501 | ||
| summaries = [] | ||
| task_mgr = self.simulate_async(inputs, progress_handler) | ||
| task_mgr.wait_all(progress_handler=progress_handler) | ||
| summaries = task_mgr.summaries() | ||
| for summ in summaries: | ||
| if isinstance(summ, SimulationError): | ||
| LOG.error(f"\nError: {summ.message}") | ||
| return summaries if isinstance(inputs, list) else summaries[0] | ||
| def simulate_async( | ||
| self, | ||
| inputs: ( | ||
| SingleBeadInput | ||
| | PorosityInput | ||
| | MicrostructureInput | ||
| | ThermalHistoryInput | ||
| | Microstructure3DInput | ||
| | list | ||
| ), | ||
| progress_handler: IProgressHandler | None = None, | ||
| ) -> SimulationTaskManager: | ||
| """Execute additive simulations asynchronously. This method does not block while the | ||
| simulations are running on the server. This class stores handles of type | ||
| google.longrunning.Operation to the remote tasks that can be used to communicate with | ||
| the server for status updates. | ||
| Parameters | ||
| ---------- | ||
| inputs: SingleBeadInput, PorosityInput, MicrostructureInput, ThermalHistoryInput, Microstructure3DInput, list | ||
| Parameters to use for simulations. A list of inputs may be provided to run multiple | ||
| simulations. | ||
| progress_handler: IProgressHandler, None, default: None | ||
| Handler for progress updates. If ``None``, and ``inputs`` contains a single | ||
| simulation input, a default progress handler will be assigned. | ||
| Returns | ||
| ------- | ||
| SimulationTaskManager | ||
| A SimulationTaskManager to handle all tasks sent to the server by this function call. | ||
| """ # noqa: E501 | ||
| """ | ||
| self._check_for_duplicate_id(inputs) | ||
| task_manager = SimulationTaskManager() | ||
| if not isinstance(inputs, list): | ||
| if not progress_handler: | ||
| progress_handler = DefaultSingleSimulationProgressHandler() | ||
| simulation_task = self._simulate(inputs, self._server, progress_handler) | ||
| task_manager.add_task(simulation_task) | ||
| return task_manager | ||
| return self._simulate(inputs, self._servers[0], progress_handler) | ||
@@ -507,12 +451,35 @@ if len(inputs) == 0: | ||
| LOG.info(f"Starting {len(inputs)} simulations") | ||
| for sim_input in inputs: | ||
| task = self._simulate(sim_input, self._server, progress_handler) | ||
| task_manager.add_task(task) | ||
| summaries = [] | ||
| LOG.info( | ||
| f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} Starting {len(inputs)} simulations", | ||
| end="", | ||
| ) | ||
| threads = min(len(inputs), len(self._servers) * self._nsims_per_server) | ||
| with concurrent.futures.ThreadPoolExecutor(threads) as executor: | ||
| futures = [] | ||
| for i, input in enumerate(inputs): | ||
| server_id = i % len(self._servers) | ||
| futures.append( | ||
| executor.submit( | ||
| self._simulate, | ||
| input=input, | ||
| server=self._servers[server_id], | ||
| progress_handler=progress_handler, | ||
| ) | ||
| ) | ||
| for future in concurrent.futures.as_completed(futures): | ||
| summary = future.result() | ||
| if isinstance(summary, SimulationError): | ||
| LOG.error(f"\nError: {summary.message}") | ||
| summaries.append(summary) | ||
| timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") | ||
| LOG.info( | ||
| f"\r{timestamp} Completed {len(summaries)} of {len(inputs)} simulations", | ||
| end="", | ||
| ) | ||
| return summaries | ||
| return task_manager | ||
| def _simulate( | ||
| self, | ||
| simulation_input: ( | ||
| input: ( | ||
| SingleBeadInput | ||
@@ -526,3 +493,10 @@ | PorosityInput | ||
| progress_handler: IProgressHandler | None = None, | ||
| ) -> SimulationTask: | ||
| ) -> ( | ||
| SingleBeadSummary | ||
| | PorositySummary | ||
| | MicrostructureSummary | ||
| | ThermalHistorySummary | ||
| | Microstructure3DSummary | ||
| | SimulationError | ||
| ): | ||
| """Execute a single simulation. | ||
@@ -532,6 +506,9 @@ | ||
| ---------- | ||
| simulation_input: SingleBeadInput, PorosityInput, MicrostructureInput, ThermalHistoryInput, Microstructure3DInput | ||
| input: SingleBeadInput, PorosityInput, MicrostructureInput, ThermalHistoryInput, | ||
| Microstructure3DInput | ||
| Parameters to use for simulation. | ||
| server: ServerConnection | ||
| Server to use for the simulation. | ||
| progress_handler: IProgressHandler, None, default: None | ||
@@ -542,41 +519,45 @@ Handler for progress updates. If ``None``, no progress updates are provided. | ||
| ------- | ||
| SimulationTask | ||
| A task that can be used to monitor the simulation progress. | ||
| """ # noqa: E501 | ||
| if simulation_input.material == AdditiveMaterial(): | ||
| SingleBeadSummary, PorositySummary, MicrostructureSummary, ThermalHistorySummary, | ||
| Microstructure3DSummary, SimulationError | ||
| """ | ||
| if input.material == AdditiveMaterial(): | ||
| raise ValueError("A material is not assigned to the simulation input") | ||
| if ( | ||
| isinstance(simulation_input, (Microstructure3DInput, ThermalHistoryInput)) | ||
| and self.enable_beta_features is False | ||
| ): | ||
| if isinstance(input, Microstructure3DInput) and self.enable_beta_features is False: | ||
| raise BetaFeatureNotEnabledError( | ||
| "This simulation requires beta features to be enabled.\n" | ||
| "Set enable_beta_features=True when creating the Additive client." | ||
| "3D microstructure simulations require beta features to be enabled.\n" | ||
| + "Set enable_beta_features=True when creating the Additive client." | ||
| ) | ||
| try: | ||
| request = create_request(simulation_input, server, progress_handler) | ||
| long_running_op = server.simulation_stub.Simulate(request) | ||
| simulation_task = SimulationTask( | ||
| server, long_running_op, simulation_input, self._user_data_path | ||
| ) | ||
| LOG.debug(f"Simulation task created for {simulation_input.id}") | ||
| request = None | ||
| if isinstance(input, ThermalHistoryInput): | ||
| return self._simulate_thermal_history( | ||
| input, USER_DATA_PATH, server, progress_handler | ||
| ) | ||
| else: | ||
| request = input._to_simulation_request() | ||
| for response in server.simulation_stub.Simulate(request): | ||
| if response.HasField("progress"): | ||
| progress = Progress.from_proto_msg(input.id, response.progress) | ||
| if progress_handler: | ||
| progress_handler.update(progress) | ||
| if progress.state == ProgressState.ERROR: | ||
| raise Exception(progress.message) | ||
| if response.HasField("melt_pool"): | ||
| return SingleBeadSummary(input, response.melt_pool) | ||
| if response.HasField("porosity_result"): | ||
| return PorositySummary(input, response.porosity_result) | ||
| if response.HasField("microstructure_result"): | ||
| return MicrostructureSummary( | ||
| input, response.microstructure_result, self._user_data_path | ||
| ) | ||
| if response.HasField("microstructure_3d_result"): | ||
| return Microstructure3DSummary( | ||
| input, response.microstructure_3d_result, self._user_data_path | ||
| ) | ||
| except Exception as e: | ||
| metadata = OperationMetadata(simulation_id=simulation_input.id, message=str(e)) | ||
| errored_op = Operation(name=simulation_input.id, done=True) | ||
| errored_op.metadata.Pack(metadata) | ||
| simulation_task = SimulationTask( | ||
| server, errored_op, simulation_input, self._user_data_path | ||
| ) | ||
| return SimulationError(input, str(e)) | ||
| if progress_handler: | ||
| time.sleep(0.1) # Allow time for the server to start the simulation | ||
| progress_handler.update(simulation_task.status()) | ||
| return simulation_task | ||
| def materials_list(self) -> list[str]: | ||
@@ -589,6 +570,5 @@ """Get a list of material names used in additive simulations. | ||
| Names of available additive materials. | ||
| """ | ||
| response = self._server.materials_stub.GetMaterialsList(Empty()) | ||
| return response.names | ||
| response = self._servers[0].materials_stub.GetMaterialsList(Empty()) | ||
| return [n for n in response.names] | ||
@@ -600,2 +580,3 @@ def material(self, name: str) -> AdditiveMaterial: | ||
| ---------- | ||
| name: str | ||
@@ -607,7 +588,5 @@ Name of material. | ||
| AdditiveMaterial | ||
| Requested material definition. | ||
| """ | ||
| request = GetMaterialRequest(name=name) | ||
| result = self._server.materials_stub.GetMaterial(request) | ||
| result = self._servers[0].materials_stub.GetMaterial(request) | ||
| return AdditiveMaterial._from_material_message(result) | ||
@@ -617,8 +596,5 @@ | ||
| def load_material( | ||
| parameters_file: str, | ||
| thermal_lookup_file: str, | ||
| characteristic_width_lookup_file: str, | ||
| parameters_file: str, thermal_lookup_file: str, characteristic_width_lookup_file: str | ||
| ) -> AdditiveMaterial: | ||
| """Load a custom material definition for the current session. The resulting | ||
| ``AdditiveMaterial`` object will not be saved to the library. | ||
| """Load a user-provided material definition. | ||
@@ -643,8 +619,2 @@ Parameters | ||
| in the *Additive Manufacturing Beta Features* documentation. | ||
| Returns | ||
| ------- | ||
| AdditiveMaterial | ||
| A material definition for use in additive simulations. | ||
| """ | ||
@@ -657,66 +627,2 @@ material = AdditiveMaterial() | ||
| def add_material( | ||
| self, | ||
| parameters_file: str, | ||
| thermal_lookup_file: str, | ||
| characteristic_width_lookup_file: str, | ||
| ) -> AdditiveMaterial | None: | ||
| """Add a custom material to the library for use in additive simulations. | ||
| Parameters | ||
| ---------- | ||
| parameters_file: str | ||
| Name of the JSON file containing material parameters. For more information, see | ||
| `Create Material Configuration File (.json) | ||
| <https://ansyshelp.ansys.com/account/secured?returnurl=/Views/Secured/corp/v232/en/add_beta/add_print_udm_tool_create_tables.html>`_ | ||
| in the *Additive Manufacturing Beta Features* documentation. | ||
| thermal_lookup_file: str | ||
| Name of the CSV file containing the lookup table for temperature-dependent properties. | ||
| For more information, see `Create Material Lookup File (.csv) | ||
| <https://ansyshelp.ansys.com/account/secured?returnurl=/Views/Secured/corp/v232/en/add_beta/add_print_udm_create_mat_lookup.html>`_ | ||
| in the *Additive Manufacturing Beta Features* documentation. | ||
| characteristic_width_lookup_file: str | ||
| Name of the CSV file containing the lookup table for characteristic melt pool width. For | ||
| more information, see | ||
| `Find Characteristic Width Values and Generate Characteristic Width File (.csv) | ||
| <https://ansyshelp.ansys.com/account/secured?returnurl=/Views/Secured/corp/v232/en/add_beta/add_print_udm_tool_find_cw.html>`_ | ||
| in the *Additive Manufacturing Beta Features* documentation. | ||
| Returns | ||
| ------- | ||
| AdditiveMaterial | ||
| A definition of the material that was added to the library. | ||
| """ # noqa: E501 | ||
| material = self.load_material( | ||
| parameters_file, thermal_lookup_file, characteristic_width_lookup_file | ||
| ) | ||
| names = self.materials_list() | ||
| if material.name.lower() in (name.lower() for name in names): | ||
| raise ValueError(f"Material {material.name} already exists. Unable to add material.") | ||
| request = AddMaterialRequest(id=misc.short_uuid(), material=material._to_material_message()) | ||
| LOG.info(f"Adding material {request.material.name}") | ||
| response = self._server.materials_stub.AddMaterial(request) | ||
| if response.HasField("error"): | ||
| raise RuntimeError(response.error) | ||
| return AdditiveMaterial._from_material_message(response.material) | ||
| def remove_material(self, name: str): | ||
| """Remove a material from the server. | ||
| Parameters | ||
| ---------- | ||
| name: str | ||
| Name of the material to remove. | ||
| """ | ||
| if name.lower() in (material.lower() for material in RESERVED_MATERIAL_NAMES): | ||
| raise ValueError(f"Unable to remove Ansys-supplied material '{name}'.") | ||
| self._server.materials_stub.RemoveMaterial(RemoveMaterialRequest(name=name)) | ||
| def tune_material( | ||
@@ -727,3 +633,3 @@ self, | ||
| progress_handler: IProgressHandler = None, | ||
| ) -> MaterialTuningSummary | None: | ||
| ) -> MaterialTuningSummary: | ||
| """Tune a custom material for use with additive simulations. | ||
@@ -751,40 +657,5 @@ | ||
| ------- | ||
| MaterialTuningSummary, None | ||
| Summary of material tuning or ``None`` if the tuning failed. | ||
| MaterialTuningSummary | ||
| Summary of material tuning. | ||
| """ # noqa: E501 | ||
| task = self.tune_material_async(input, out_dir) | ||
| task.wait(progress_handler=progress_handler) | ||
| return task.summary | ||
| def tune_material_async( | ||
| self, | ||
| input: MaterialTuningInput, | ||
| out_dir: str = USER_DATA_PATH, | ||
| ) -> SimulationTask: | ||
| """Tune a custom material for use with additive simulations asynchronously. | ||
| This method performs the same function as the Material Tuning Tool | ||
| described in | ||
| `Find Simulation Parameters to Match Simulation to Experiments | ||
| <https://ansyshelp.ansys.com/account/secured?returnurl=/Views/Secured/corp/v232/en/add_beta/add_print_udm_tool_match_sim_to_exp.html>`_. | ||
| It is used for one step in the material tuning process. The other steps | ||
| are described in | ||
| `Chapter 2: Material Tuning Tool (Beta) to Create User Defined Materials | ||
| <https://ansyshelp.ansys.com/account/secured?returnurl=/Views/Secured/corp/v232/en/add_beta/add_science_BETA_material_tuning_tool.html>`_. | ||
| Parameters | ||
| ---------- | ||
| input: MaterialTuningInput | ||
| Input parameters for material tuning. | ||
| out_dir: str, default: USER_DATA_PATH | ||
| Folder path for output files. | ||
| Returns | ||
| ------- | ||
| SimulationTask | ||
| An asynchronous simulation task. | ||
| """ | ||
| if input.id == "": | ||
@@ -802,127 +673,107 @@ input.id = misc.short_uuid() | ||
| operation = self._server.materials_stub.TuneMaterial(request) | ||
| for response in self._servers[0].materials_stub.TuneMaterial(request): | ||
| if response.HasField("progress"): | ||
| progress = Progress.from_proto_msg(input.id, response.progress) | ||
| if progress.state == ProgressState.ERROR: | ||
| raise Exception(progress.message) | ||
| else: | ||
| for m in progress.message.splitlines(): | ||
| if ( | ||
| "License successfully" in m | ||
| or "Starting ThermalSolver" in m | ||
| or "threads for solver" in m | ||
| ): | ||
| continue | ||
| LOG.info(m) | ||
| if progress_handler: | ||
| progress_handler.update(progress) | ||
| if response.HasField("result"): | ||
| return MaterialTuningSummary(input, response.result, out_dir) | ||
| return SimulationTask(self._server, operation, input, out_dir) | ||
| def __file_upload_reader( | ||
| self, file_name: str, chunk_size=2 * 1024**2 | ||
| ) -> Iterator[UploadFileRequest]: | ||
| """Read a file and return an iterator of UploadFileRequests.""" | ||
| file_size = os.path.getsize(file_name) | ||
| short_name = os.path.basename(file_name) | ||
| with open(file_name, mode="rb") as f: | ||
| while True: | ||
| chunk = f.read(chunk_size) | ||
| if not chunk: | ||
| break | ||
| yield UploadFileRequest( | ||
| name=short_name, | ||
| total_size=file_size, | ||
| content=chunk, | ||
| content_md5=hashlib.md5(chunk).hexdigest(), | ||
| ) | ||
| def simulate_study( | ||
| def _simulate_thermal_history( | ||
| self, | ||
| study: ParametricStudy, | ||
| simulation_ids: list[str] | None = None, | ||
| types: list[SimulationType] | None = None, | ||
| priority: int | None = None, | ||
| iteration: int = None, | ||
| ): | ||
| """Run the simulations in a parametric study. | ||
| input: ThermalHistoryInput, | ||
| out_dir: str, | ||
| server: ServerConnection, | ||
| progress_handler: IProgressHandler | None = None, | ||
| ) -> ThermalHistorySummary: | ||
| """Run a thermal history simulation. | ||
| Parameters | ||
| ---------- | ||
| study : ParametricStudy | ||
| Parametric study to run. | ||
| simulation_ids : list[str], default: None | ||
| List of simulation IDs to run. If this value is ``None``, | ||
| all simulations with a status of ``Pending`` are run. | ||
| types : list[SimulationType], default: None | ||
| Type of simulations to run. If this value is ``None``, | ||
| all simulation types are run. | ||
| priority : int, default: None | ||
| Priority of simulations to run. If this value is ``None``, | ||
| all priorities are run. | ||
| iteration : int, default: None | ||
| Iteration number of simulations to run. The default is ``None``, | ||
| all iterations are run. | ||
| progress_handler : IProgressHandler, None, default: None | ||
| Handler for progress updates. If ``None``, a :class:`ParametricStudyProcessHandler` will be used. | ||
| Parameters | ||
| ---------- | ||
| input: ThermalHistoryInput | ||
| Simulation input parameters. | ||
| out_dir: str | ||
| Folder path for output files. | ||
| server: ServerConnection | ||
| Server to use for the simulation. | ||
| progress_handler: IPorgressHandler, None, default: None | ||
| Handler for progress updates. If ``None``, no progress updates are provided. | ||
| . | ||
| Returns | ||
| ------- | ||
| :class:`ThermalHistorySummary` | ||
| """ | ||
| SLEEP_INTERVAL = 2 | ||
| progress_handler = ParametricStudyProgressHandler(study) | ||
| num_summaries = 0 | ||
| if input.geometry is None or input.geometry.path == "": | ||
| raise ValueError("The geometry path is not defined in the simulation input") | ||
| try: | ||
| task_mgr = self.simulate_study_async( | ||
| study, simulation_ids, types, priority, iteration, progress_handler | ||
| ) | ||
| # Allow time for the server to start the simulations | ||
| time.sleep(SLEEP_INTERVAL) | ||
| while not task_mgr.done: | ||
| task_mgr.status(progress_handler) | ||
| if len(task_mgr.summaries()) > num_summaries: | ||
| # TODO: Only update the study with new summaries | ||
| study.update(task_mgr.summaries()) | ||
| num_summaries = len(task_mgr.summaries()) | ||
| time.sleep(SLEEP_INTERVAL) | ||
| remote_geometry_path = "" | ||
| for response in server.simulation_stub.UploadFile( | ||
| self.__file_upload_reader(input.geometry.path) | ||
| ): | ||
| remote_geometry_path = response.remote_file_name | ||
| progress = Progress.from_proto_msg(input.id, response.progress) | ||
| if progress_handler: | ||
| progress_handler.update(progress) | ||
| if progress.state == ProgressState.ERROR: | ||
| raise Exception(progress.message) | ||
| except Exception as e: | ||
| LOG.error(f"Error running study: {e}") | ||
| study.reset_simulation_status() | ||
| raise RuntimeError from e | ||
| request = input._to_simulation_request(remote_geometry_path=remote_geometry_path) | ||
| for response in server.simulation_stub.Simulate(request): | ||
| if response.HasField("progress"): | ||
| progress = Progress.from_proto_msg(input.id, response.progress) | ||
| if progress_handler: | ||
| progress_handler.update(progress) | ||
| if progress.state == ProgressState.ERROR and "WARN" not in progress.message: | ||
| raise Exception(progress.message) | ||
| if response.HasField("thermal_history_result"): | ||
| path = os.path.join(out_dir, input.id, "coax_ave_output") | ||
| local_zip = download_file( | ||
| server.simulation_stub, | ||
| response.thermal_history_result.coax_ave_zip_file, | ||
| path, | ||
| ) | ||
| with zipfile.ZipFile(local_zip, "r") as zip: | ||
| zip.extractall(path) | ||
| os.remove(local_zip) | ||
| return ThermalHistorySummary(input, path) | ||
| def simulate_study_async( | ||
| self, | ||
| study: ParametricStudy, | ||
| simulation_ids: list[str] | None = None, | ||
| types: list[SimulationType] | None = None, | ||
| priority: int | None = None, | ||
| iteration: int = None, | ||
| progress_handler: IProgressHandler = None, | ||
| ) -> SimulationTaskManager: | ||
| """Run the simulations in a parametric study asynchronously. | ||
| Parameters | ||
| ---------- | ||
| study : ParametricStudy | ||
| Parametric study to run. | ||
| simulation_ids : list[str], default: None | ||
| List of simulation IDs to run. If this value is ``None``, | ||
| all simulations with a status of ``Pending`` are run. | ||
| types : list[SimulationType], default: None | ||
| Type of simulations to run. If this value is ``None``, | ||
| all simulation types are run. | ||
| priority : int, default: None | ||
| Priority of simulations to run. If this value is ``None``, | ||
| all priorities are run. | ||
| iteration : int, default: None | ||
| Iteration number of simulations to run. The default is ``None``, | ||
| all iterations are run. | ||
| progress_handler : IProgressHandler, None, default: None | ||
| Handler for progress updates. | ||
| """ | ||
| inputs = study.simulation_inputs(self.material, simulation_ids, types, priority, iteration) | ||
| if not inputs: | ||
| # no simulations met the provided criteria, return an empty task manager | ||
| return SimulationTaskManager() | ||
| ids = [i.id for i in inputs] | ||
| study.set_simulation_status(ids, SimulationStatus.PENDING) | ||
| study.clear_errors(ids) | ||
| return self.simulate_async(inputs, progress_handler) | ||
| def _check_for_duplicate_id(self, inputs): | ||
| """Check for duplicate simulation IDs in a list of inputs. | ||
| If an input does not have an ID, one will be assigned. | ||
| Parameters | ||
| ---------- | ||
| inputs : SingleBeadInput, PorosityInput, MicrostructureInput, ThermalHistoryInput, Microstructure3DInput, list | ||
| A simulation input or a list of simulation inputs. | ||
| Raises | ||
| ------ | ||
| ValueError | ||
| If a duplicate ID is found in the list of inputs. | ||
| """ # noqa: E501 | ||
| if not isinstance(inputs, list): | ||
| # An individual input, not a list | ||
| if inputs.id == "": | ||
| inputs.id = misc.short_uuid() | ||
| return | ||
| ids = [] | ||
| for i in inputs: | ||
| if not i.id: | ||
| # give input an id if none provided | ||
| i.id = misc.short_uuid() | ||
| if any(id == i.id for id in ids): | ||
| raise ValueError(f'Duplicate simulation ID "{i.id}" in input list') | ||
| ids.append(i.id) | ||
| for input in inputs: | ||
| if input.id == "": | ||
| input.id = misc.short_uuid() | ||
| if input.id in ids: | ||
| raise ValueError(f'Duplicate simulation ID "{input.id}" in input list') | ||
| ids.append(input.id) |
@@ -23,3 +23,2 @@ # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| """Unit conversion constants and functions.""" | ||
| MM_TO_METER = 0.001 | ||
@@ -41,3 +40,2 @@ METER_TO_MM = 1000 | ||
| Equivalent degrees in kelvin. | ||
| """ | ||
@@ -59,4 +57,3 @@ return celsius + 273.15 | ||
| Equivalent degrees in celsius. | ||
| """ | ||
| return kelvin - 273.15 |
@@ -23,15 +23,11 @@ # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| """Provides a function for downloading files from the server to the client.""" | ||
| import hashlib | ||
| import os | ||
| from ansys.additive.core.progress_handler import ( | ||
| IProgressHandler, | ||
| Progress, | ||
| ProgressState, | ||
| ) | ||
| from ansys.api.additive.v0.additive_simulation_pb2 import DownloadFileRequest | ||
| from ansys.api.additive.v0.additive_simulation_pb2_grpc import SimulationServiceStub | ||
| from ansys.additive.core.progress_handler import IProgressHandler, Progress, ProgressState | ||
| def download_file( | ||
@@ -47,4 +43,2 @@ stub: SimulationServiceStub, | ||
| ---------- | ||
| stub: SimulationServiceStub | ||
| gRPC stub for the simulation service. | ||
| remote_file_name: str | ||
@@ -61,3 +55,2 @@ Path to file on the server. | ||
| Local path of downloaded file. | ||
| """ | ||
@@ -78,3 +71,3 @@ | ||
| if len(response.content) > 0: | ||
| md5 = hashlib.md5(response.content).hexdigest() # noqa: S324 | ||
| md5 = hashlib.md5(response.content).hexdigest() | ||
| if md5 != response.content_md5: | ||
@@ -81,0 +74,0 @@ msg = "Download error, MD5 sums did not match" |
@@ -23,3 +23,3 @@ # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| """Provides functions for downloading sample datasets from the PyAdditive repository.""" | ||
| from http.client import HTTPMessage | ||
| import os | ||
@@ -29,3 +29,2 @@ import shutil | ||
| import zipfile | ||
| from http.client import HTTPMessage | ||
@@ -68,3 +67,2 @@ from ansys.additive.core import EXAMPLES_PATH | ||
| Path to the decompressed contents of the ZIP file. | ||
| """ | ||
@@ -96,3 +94,2 @@ outdir = EXAMPLES_PATH | ||
| Name for the example data file. | ||
| Returns | ||
@@ -102,3 +99,2 @@ ------- | ||
| HttpMessage: HTTP status message, if any. | ||
| """ | ||
@@ -147,3 +143,2 @@ # First check if file has already been downloaded | ||
| Path to the characteristic width lookup file (CSV). | ||
| """ | ||
@@ -220,3 +215,2 @@ | ||
| Path to the characteristic width lookup file. | ||
| """ | ||
@@ -223,0 +217,0 @@ |
@@ -27,1 +27,3 @@ # Copyright (C) 2024 ANSYS, Inc. and/or its affiliates. | ||
| """Raised when a beta feature is not enabled.""" | ||
| pass |
@@ -23,5 +23,4 @@ # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| """Provides a container for part geometry files.""" | ||
| from enum import IntEnum | ||
| import os | ||
| from enum import IntEnum | ||
@@ -66,3 +65,2 @@ from ansys.api.additive.v0.additive_domain_pb2 import BuildFileMachineType | ||
| Path to the build file. | ||
| """ | ||
@@ -91,3 +89,6 @@ | ||
| return False | ||
| return all(getattr(self, k) == getattr(other, k) for k in self.__dict__) | ||
| for k in self.__dict__: | ||
| if getattr(self, k) != getattr(other, k): | ||
| return False | ||
| return True | ||
@@ -129,3 +130,2 @@ @property | ||
| Path to file. | ||
| """ | ||
@@ -145,3 +145,6 @@ if not os.path.exists(path): | ||
| return False | ||
| return all(getattr(self, k) == getattr(other, k) for k in self.__dict__) | ||
| for k in self.__dict__: | ||
| if getattr(self, k) != getattr(other, k): | ||
| return False | ||
| return True | ||
@@ -148,0 +151,0 @@ @property |
@@ -70,8 +70,5 @@ # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| """ | ||
| import logging | ||
| import sys | ||
| from IPython import get_ipython | ||
| # Default logging configuration | ||
@@ -84,24 +81,4 @@ LOG_LEVEL = logging.DEBUG | ||
| FILE_MSG_FORMAT = STDOUT_MSG_FORMAT | ||
| DATE_FORMAT = "%Y-%m-%d %H:%M:%S" | ||
| def is_notebook() -> bool: | ||
| """Check if the code is running in a Jupyter notebook. | ||
| Returns: | ||
| bool: True if running in a Jupyter notebook, False otherwise. | ||
| """ | ||
| try: | ||
| shell = get_ipython().__class__.__name__ | ||
| if shell == "ZMQInteractiveShell": | ||
| return True | ||
| elif shell == "TerminalInteractiveShell": | ||
| return False | ||
| else: | ||
| return False | ||
| except NameError: | ||
| return False | ||
| class PyAdditivePercentStyle(logging.PercentStyle): | ||
@@ -118,3 +95,6 @@ """Provides a common messaging style for the ``PyAdditiveFormatter`` class.""" | ||
| defaults = self._defaults | ||
| values = defaults | record.__dict__ if defaults else record.__dict__ | ||
| if defaults: | ||
| values = defaults | record.__dict__ | ||
| else: | ||
| values = record.__dict__ | ||
@@ -167,7 +147,6 @@ # We can make any changes that we want in the record here. For example, | ||
| Name of the file to write log log messages to. | ||
| """ | ||
| file_handler = None | ||
| stdout_handler = None | ||
| std_out_handler = None | ||
| _level = logging.DEBUG | ||
@@ -181,10 +160,5 @@ _instances = {} | ||
| if is_notebook(): | ||
| level = logging.INFO | ||
| # create default main logger | ||
| self.logger = logging.getLogger("PyAdditive_global") | ||
| self.logger.setLevel(level) | ||
| self.logger.propagate = True | ||
@@ -219,3 +193,2 @@ | ||
| Logging level to filter the message severity allowed in the logger. | ||
| """ | ||
@@ -232,3 +205,2 @@ | ||
| Logging level to filter the message severity allowed in the logger. | ||
| """ | ||
@@ -245,3 +217,2 @@ | ||
| Name of the logger. | ||
| """ | ||
@@ -257,19 +228,3 @@ | ||
| def setLevel(self, level: str | int) -> None: | ||
| """Set the logging level for the logger. | ||
| Parameters | ||
| ---------- | ||
| level : str or int | ||
| Logging level to filter the message severity allowed in the logger. | ||
| If int, it must be one of the levels defined in the :obj:`~logging` module. | ||
| Valid string values are ``"DEBUG"``, ``"INFO"``, ``"WARNING"``, ``"ERROR"``, | ||
| and ``"CRITICAL"``. | ||
| """ | ||
| self.logger.setLevel(level) | ||
| for handler in self.logger.handlers: | ||
| handler.setLevel(level) | ||
| def addfile_handler(logger, filename=FILE_NAME, level=LOG_LEVEL): | ||
@@ -291,3 +246,2 @@ """Add a file handler to the input. | ||
| :class:`Logger` or :class:`logging.Logger` object. | ||
| """ | ||
@@ -323,14 +277,13 @@ file_handler = logging.FileHandler(filename) | ||
| :class:`Logger` or :class:`logging.Logger` object. | ||
| """ | ||
| stdout_handler = logging.StreamHandler() | ||
| stdout_handler.setLevel(level) | ||
| stdout_handler.setFormatter(PyAdditiveFormatter(STDOUT_MSG_FORMAT, DATE_FORMAT)) | ||
| std_out_handler = logging.StreamHandler() | ||
| std_out_handler.setLevel(level) | ||
| std_out_handler.setFormatter(PyAdditiveFormatter(STDOUT_MSG_FORMAT)) | ||
| if isinstance(logger, Logger): | ||
| logger.stdout_handler = stdout_handler | ||
| logger.logger.addHandler(stdout_handler) | ||
| logger.std_out_handler = std_out_handler | ||
| logger.logger.addHandler(std_out_handler) | ||
| elif isinstance(logger, logging.Logger): | ||
| logger.addHandler(stdout_handler) | ||
| logger.addHandler(std_out_handler) | ||
@@ -337,0 +290,0 @@ return logger |
@@ -23,9 +23,9 @@ # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| """Provides a container for machine parameters.""" | ||
| import math | ||
| import ansys.additive.core.conversions as conversions | ||
| from ansys.api.additive.v0.additive_domain_pb2 import MachineSettings as MachineMessage | ||
| import ansys.additive.core.conversions as conversions | ||
| class MachineConstants: | ||
@@ -137,3 +137,6 @@ """Provides constants for additive manufacturing machine settings.""" | ||
| return False | ||
| return all(getattr(self, k) == getattr(__o, k) for k in self.__dict__) | ||
| for k in self.__dict__: | ||
| if getattr(self, k) != getattr(__o, k): | ||
| return False | ||
| return True | ||
@@ -157,6 +160,3 @@ def __validate_range(self, value, min, max, name): | ||
| self.__validate_range( | ||
| value, | ||
| MachineConstants.MIN_LASER_POWER, | ||
| MachineConstants.MAX_LASER_POWER, | ||
| "laser_power", | ||
| value, MachineConstants.MIN_LASER_POWER, MachineConstants.MAX_LASER_POWER, "laser_power" | ||
| ) | ||
@@ -176,6 +176,3 @@ self._laser_power = value | ||
| self.__validate_range( | ||
| value, | ||
| MachineConstants.MIN_SCAN_SPEED, | ||
| MachineConstants.MAX_SCAN_SPEED, | ||
| "scan_speed", | ||
| value, MachineConstants.MIN_SCAN_SPEED, MachineConstants.MAX_SCAN_SPEED, "scan_speed" | ||
| ) | ||
@@ -326,4 +323,3 @@ self._scan_speed = value | ||
| """Create an additive machine from a machine message received from the | ||
| Additive service. | ||
| """ | ||
| Additive service.""" | ||
| if isinstance(msg, MachineMessage): | ||
@@ -346,4 +342,3 @@ return AdditiveMachine( | ||
| """Create a machine message from the additive machine to send to the | ||
| Additive service. | ||
| """ | ||
| Additive service.""" | ||
| return MachineMessage( | ||
@@ -350,0 +345,0 @@ laser_power=self.laser_power, |
@@ -23,6 +23,4 @@ # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| """Provides input and result summary containers for material tuning.""" | ||
| import os | ||
| from ansys.additive.core.simulation_input_base import SimulationInputBase | ||
| from ansys.api.additive.v0.additive_domain_pb2 import ( | ||
@@ -37,3 +35,3 @@ MaterialTuningInput as MaterialTuningInputMessage, | ||
| class MaterialTuningInput(SimulationInputBase): | ||
| class MaterialTuningInput: | ||
| """Provides input parameters for tuning a custom material. | ||
@@ -43,2 +41,4 @@ | ||
| ---------- | ||
| id: str | ||
| ID for this set of tuning simulations. | ||
| experiment_data_file: str | ||
@@ -65,3 +65,2 @@ Name of the CSV file containing the experimental results data. | ||
| This value is ignored otherwise. The default is ``353.15`` K, which is 80 C. | ||
| """ | ||
@@ -72,2 +71,3 @@ | ||
| *, | ||
| id: str, | ||
| experiment_data_file: str, | ||
@@ -82,3 +82,2 @@ material_configuration_file: str, | ||
| """Initialize a MaterialTuningInput object.""" | ||
| super().__init__() | ||
@@ -95,2 +94,3 @@ if not os.path.isfile(experiment_data_file): | ||
| raise FileNotFoundError(f"File not found: {characteristic_width_lookup_file}") | ||
| self.id = id | ||
| self.allowable_error = allowable_error | ||
@@ -125,6 +125,3 @@ self.max_iterations = max_iterations | ||
| for k in self.__dict__: | ||
| if k == "_id": | ||
| repr += "id: " + str(self.id) + "\n" | ||
| else: | ||
| repr += k + ": " + str(getattr(self, k)) + "\n" | ||
| repr += k + ": " + str(getattr(self, k)) + "\n" | ||
| return repr | ||
@@ -135,3 +132,6 @@ | ||
| return False | ||
| return all(getattr(self, k) == getattr(other, k) for k in self.__dict__) | ||
| for k in self.__dict__: | ||
| if getattr(self, k) != getattr(other, k): | ||
| return False | ||
| return True | ||
@@ -152,4 +152,2 @@ | ||
| self._optimized_parameters_file = os.path.join(out_dir, "optimized_parameters.csv") | ||
| self._coefficients_file = None | ||
| self._material_configuration_file = None | ||
| self._characteristic_width_file = None | ||
@@ -161,13 +159,3 @@ self._log_file = None | ||
| if msg.coefficients: | ||
| self._coefficients_file = os.path.join(out_dir, "coefficients.csv") | ||
| with open(self._coefficients_file, "wb") as f: | ||
| f.write(msg.coefficients) | ||
| if msg.material_parameters: | ||
| self._material_configuration_file = os.path.join(out_dir, "material_configuration.json") | ||
| with open(self._material_configuration_file, "wb") as f: | ||
| f.write(msg.material_parameters) | ||
| if msg.characteristic_width_lookup: | ||
| if len(msg.characteristic_width_lookup) > 0: | ||
| self._characteristic_width_file = os.path.join( | ||
@@ -178,6 +166,6 @@ out_dir, "characteristic_width_lookup.csv" | ||
| f.write(msg.characteristic_width_lookup) | ||
| elif input.characteristic_width_lookup_file: | ||
| elif len(input.characteristic_width_lookup_file) > 0: | ||
| self._characteristic_width_file = input.characteristic_width_lookup_file | ||
| if msg.log: | ||
| if len(msg.log) > 0: | ||
| self._log_file = os.path.join(out_dir, "log.txt") | ||
@@ -198,17 +186,3 @@ with open(self._log_file, "wb") as f: | ||
| @property | ||
| def coefficients_file(self) -> str | None: | ||
| """Path to the calculated coefficients file.""" | ||
| return self._coefficients_file | ||
| @property | ||
| def material_configuration_file(self) -> str | None: | ||
| """Path to the updated material properties file. | ||
| Penetration depth and absorptivity coefficients are updated based on the tuning | ||
| results. | ||
| """ | ||
| return self._material_configuration_file | ||
| @property | ||
| def characteristic_width_file(self) -> str | None: | ||
| def characteristic_width_file(self) -> str: | ||
| """Path to the characteristic width file or ``None``.""" | ||
@@ -215,0 +189,0 @@ return self._characteristic_width_file |
@@ -23,3 +23,2 @@ # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| """Provides a container for material parameters.""" | ||
| import collections | ||
@@ -31,5 +30,2 @@ import csv | ||
| from ansys.api.additive.v0.additive_domain_pb2 import ( | ||
| AdditiveMaterial as MaterialMessage, | ||
| ) | ||
| from ansys.api.additive.v0.additive_domain_pb2 import ( | ||
| CharacteristicWidthDataPoint as CharacteristicWidthDataPointMessage, | ||
@@ -40,15 +36,5 @@ ) | ||
| ) | ||
| from ansys.api.additive.v0.additive_domain_pb2 import AdditiveMaterial as MaterialMessage | ||
| RESERVED_MATERIAL_NAMES = [ | ||
| "17-4PH", | ||
| "316L", | ||
| "Al357", | ||
| "AlSi10Mg", | ||
| "CoCr", | ||
| "IN625", | ||
| "IN718", | ||
| "Ti64", | ||
| ] | ||
| class CharacteristicWidthDataPoint: | ||
@@ -64,7 +50,3 @@ """Provides the container for a characteristic width data point. | ||
| def __init__( | ||
| self, | ||
| *, | ||
| laser_power: float = 0, | ||
| scan_speed: float = 0, | ||
| characteristic_width: float = 0, | ||
| self, *, laser_power: float = 0, scan_speed: float = 0, characteristic_width: float = 0 | ||
| ): | ||
@@ -85,3 +67,6 @@ """Initialize a characteristic width data point.""" | ||
| return False | ||
| return all(getattr(self, k) == getattr(other, k) for k in self.__dict__) | ||
| for k in self.__dict__: | ||
| if getattr(self, k) != getattr(other, k): | ||
| return False | ||
| return True | ||
@@ -91,4 +76,3 @@ @property | ||
| """Characteristic melt pool width for a given laser power and scan | ||
| speed (m). | ||
| """ | ||
| speed (m).""" | ||
| return self._characteristic_width | ||
@@ -128,8 +112,5 @@ | ||
| @staticmethod | ||
| def _from_characteristic_width_data_point_message( | ||
| msg: CharacteristicWidthDataPointMessage, | ||
| ): | ||
| def _from_characteristic_width_data_point_message(msg: CharacteristicWidthDataPointMessage): | ||
| """Create a characteristic width data point`` from a characteristic | ||
| data point message received from the Additive service. | ||
| """ | ||
| data point message received from the Additive service.""" | ||
| if not isinstance(msg, CharacteristicWidthDataPointMessage): | ||
@@ -148,4 +129,3 @@ raise ValueError( | ||
| """Create a characteristic width data point message from this | ||
| characteristic width data point to send to the Additive service. | ||
| """ | ||
| characteristic width data point to send to the Additive service.""" | ||
| msg = CharacteristicWidthDataPointMessage() | ||
@@ -196,3 +176,6 @@ for p in self.__dict__: | ||
| return False | ||
| return all(getattr(self, k) == getattr(other, k) for k in self.__dict__) | ||
| for k in self.__dict__: | ||
| if getattr(self, k) != getattr(other, k): | ||
| return False | ||
| return True | ||
@@ -276,5 +259,3 @@ @property | ||
| @staticmethod | ||
| def _from_thermal_properties_data_point_message( | ||
| msg: ThermalPropertiesDataPointMessage, | ||
| ): | ||
| def _from_thermal_properties_data_point_message(msg: ThermalPropertiesDataPointMessage): | ||
| """Create a thermal properties data point from a thermal characteristic | ||
@@ -323,3 +304,2 @@ data point message received from the Additive service. | ||
| anisotropic_strain_coefficient_z: float = 0, | ||
| description: str = "", | ||
| elastic_modulus: float = 0, | ||
@@ -364,3 +344,2 @@ hardening_factor: float = 0, | ||
| self._anisotropic_strain_coefficient_z = anisotropic_strain_coefficient_z | ||
| self._description = description | ||
| self._elastic_modulus = elastic_modulus | ||
@@ -416,3 +395,6 @@ self._hardening_factor = hardening_factor | ||
| return False | ||
| return all(getattr(self, k) == getattr(__o, k) for k in self.__dict__) | ||
| for k in self.__dict__: | ||
| if getattr(self, k) != getattr(__o, k): | ||
| return False | ||
| return True | ||
@@ -510,12 +492,2 @@ @property | ||
| @property | ||
| def description(self) -> str: | ||
| """Description of the material.""" | ||
| return self._description | ||
| @description.setter | ||
| def description(self, value: str): | ||
| """Set description.""" | ||
| self._description = value | ||
| @property | ||
| def elastic_modulus(self) -> float: | ||
@@ -797,4 +769,3 @@ """Elastic modulus (Pa).""" | ||
| """Create an additive material from a material message received from | ||
| the Additive service. | ||
| """ | ||
| the Additive service.""" | ||
| if not isinstance(msg, MaterialMessage): | ||
@@ -818,4 +789,3 @@ raise ValueError("Invalid message object passed to from_material_message()") | ||
| """Create a material message from the additive material to send to the | ||
| Additive service. | ||
| """ | ||
| Additive service.""" | ||
| msg = MaterialMessage() | ||
@@ -838,3 +808,2 @@ for p in self.__dict__: | ||
| self.name = data["name"] | ||
| self.description = data["description"] | ||
| parameters = data["configuration"] | ||
@@ -841,0 +810,0 @@ # Convert camelCase to snake_case |
@@ -23,10 +23,5 @@ # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| """Provides input and result summary containers for microstructure 3D simulations.""" | ||
| import math | ||
| import os | ||
| from ansys.additive.core import misc | ||
| from ansys.additive.core.machine import AdditiveMachine | ||
| from ansys.additive.core.material import AdditiveMaterial | ||
| from ansys.additive.core.microstructure import Microstructure2DResult | ||
| from ansys.api.additive.v0.additive_domain_pb2 import ( | ||
@@ -38,3 +33,8 @@ Microstructure3DInput as Microstructure3DInputMessage, | ||
| from ansys.additive.core import misc | ||
| from ansys.additive.core.machine import AdditiveMachine | ||
| from ansys.additive.core.material import AdditiveMaterial | ||
| from ansys.additive.core.microstructure import _Microstructure2DResult | ||
| class Microstructure3DInput: | ||
@@ -104,4 +104,4 @@ """Provides input parameters for 3D microstructure simulation. | ||
| num_initial_random_nuclei: int = DEFAULT_NUMBER_OF_RANDOM_NUCLEI, | ||
| machine: AdditiveMachine = None, | ||
| material: AdditiveMaterial = None, | ||
| machine: AdditiveMachine = AdditiveMachine(), | ||
| material: AdditiveMaterial = AdditiveMaterial(), | ||
| ): | ||
@@ -121,4 +121,4 @@ """Initialize a ``Microstructure3DInput`` object.""" | ||
| self.num_initial_random_nuclei = num_initial_random_nuclei | ||
| self.machine = machine if machine else AdditiveMachine() | ||
| self.material = material if material else AdditiveMaterial() | ||
| self.machine = machine | ||
| self.material = material | ||
@@ -206,6 +206,3 @@ def __repr__(self): | ||
| self.__validate_range( | ||
| value, | ||
| self.MIN_POSITION_COORDINATE, | ||
| self.MAX_POSITION_COORDINATE, | ||
| "sample_min_x", | ||
| value, self.MIN_POSITION_COORDINATE, self.MAX_POSITION_COORDINATE, "sample_min_x" | ||
| ) | ||
@@ -226,6 +223,3 @@ self._sample_min_x = value | ||
| self.__validate_range( | ||
| value, | ||
| self.MIN_POSITION_COORDINATE, | ||
| self.MAX_POSITION_COORDINATE, | ||
| "sample_min_y", | ||
| value, self.MIN_POSITION_COORDINATE, self.MAX_POSITION_COORDINATE, "sample_min_y" | ||
| ) | ||
@@ -246,6 +240,3 @@ self._sample_min_y = value | ||
| self.__validate_range( | ||
| value, | ||
| self.MIN_POSITION_COORDINATE, | ||
| self.MAX_POSITION_COORDINATE, | ||
| "sample_min_z", | ||
| value, self.MIN_POSITION_COORDINATE, self.MAX_POSITION_COORDINATE, "sample_min_z" | ||
| ) | ||
@@ -378,6 +369,3 @@ self._sample_min_z = value | ||
| def __init__( | ||
| self, | ||
| input: Microstructure3DInput, | ||
| result: Microstructure3DResult, | ||
| user_data_path: str, | ||
| self, input: Microstructure3DInput, result: Microstructure3DResult, user_data_path: str | ||
| ) -> None: | ||
@@ -398,3 +386,3 @@ """Initialize a ``Microstructure3DSummary`` object.""" | ||
| f.write(result.three_d_vtk) | ||
| self._2d_result = Microstructure2DResult(result.two_d_result, outpath) | ||
| self._2d_result = _Microstructure2DResult(result.two_d_result, outpath) | ||
@@ -401,0 +389,0 @@ def __repr__(self): |
@@ -23,14 +23,5 @@ # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| """Provides input and result summary containers for microstructure simulations.""" | ||
| import math | ||
| import os | ||
| import numpy as np | ||
| import pandas as pd | ||
| from google.protobuf.internal.containers import RepeatedCompositeFieldContainer | ||
| from ansys.additive.core import misc | ||
| from ansys.additive.core.machine import AdditiveMachine | ||
| from ansys.additive.core.material import AdditiveMaterial | ||
| from ansys.additive.core.simulation_input_base import SimulationInputBase | ||
| from ansys.api.additive.v0.additive_domain_pb2 import ( | ||
@@ -43,5 +34,12 @@ MicrostructureInput as MicrostructureInputMessage, | ||
| from ansys.api.additive.v0.additive_simulation_pb2 import SimulationRequest | ||
| from google.protobuf.internal.containers import RepeatedCompositeFieldContainer | ||
| import numpy as np | ||
| import pandas as pd | ||
| from ansys.additive.core import misc | ||
| from ansys.additive.core.machine import AdditiveMachine | ||
| from ansys.additive.core.material import AdditiveMaterial | ||
| class MicrostructureInput(SimulationInputBase): | ||
| class MicrostructureInput: | ||
| """Provides input parameters for microstructure simulation. | ||
@@ -109,2 +107,3 @@ | ||
| self, | ||
| id: str = "", | ||
| *, | ||
@@ -124,7 +123,6 @@ sample_min_x: float = DEFAULT_POSITION_COORDINATE, | ||
| random_seed: int = DEFAULT_RANDOM_SEED, | ||
| machine: AdditiveMachine = None, | ||
| material: AdditiveMaterial = None, | ||
| machine: AdditiveMachine = AdditiveMachine(), | ||
| material: AdditiveMaterial = AdditiveMaterial(), | ||
| ): | ||
| """Initialize a ``MicrostructureInput`` object.""" | ||
| super().__init__() | ||
@@ -154,2 +152,3 @@ # we have a circular dependency here, so we validate sensor_dimension | ||
| # use setters for remaining properties | ||
| self.id = id | ||
| self.sample_min_x = sample_min_x | ||
@@ -163,4 +162,4 @@ self.sample_min_y = sample_min_y | ||
| self.melt_pool_depth = melt_pool_depth | ||
| self.machine = machine if machine else AdditiveMachine() | ||
| self.material = material if material else AdditiveMaterial() | ||
| self.machine = machine | ||
| self.material = material | ||
| if random_seed != self.DEFAULT_RANDOM_SEED: | ||
@@ -219,2 +218,11 @@ self.random_seed = random_seed | ||
| @property | ||
| def id(self) -> str: | ||
| """User-provided ID for this simulation.""" | ||
| return self._id | ||
| @id.setter | ||
| def id(self, value: str): | ||
| self._id = value | ||
| @property | ||
| def machine(self): | ||
@@ -249,6 +257,3 @@ """Machine-related parameters.""" | ||
| self.__validate_range( | ||
| value, | ||
| self.MIN_POSITION_COORDINATE, | ||
| self.MAX_POSITION_COORDINATE, | ||
| "sample_min_x", | ||
| value, self.MIN_POSITION_COORDINATE, self.MAX_POSITION_COORDINATE, "sample_min_x" | ||
| ) | ||
@@ -269,6 +274,3 @@ self._sample_min_x = value | ||
| self.__validate_range( | ||
| value, | ||
| self.MIN_POSITION_COORDINATE, | ||
| self.MAX_POSITION_COORDINATE, | ||
| "sample_min_y", | ||
| value, self.MIN_POSITION_COORDINATE, self.MAX_POSITION_COORDINATE, "sample_min_y" | ||
| ) | ||
@@ -289,6 +291,3 @@ self._sample_min_y = value | ||
| self.__validate_range( | ||
| value, | ||
| self.MIN_POSITION_COORDINATE, | ||
| self.MAX_POSITION_COORDINATE, | ||
| "sample_min_z", | ||
| value, self.MIN_POSITION_COORDINATE, self.MAX_POSITION_COORDINATE, "sample_min_z" | ||
| ) | ||
@@ -364,6 +363,3 @@ self._sample_min_z = value | ||
| self.__validate_range( | ||
| value, | ||
| self.MIN_SENSOR_DIMENSION, | ||
| self.MAX_SENSOR_DIMENSION, | ||
| "sensor_dimension", | ||
| value, self.MIN_SENSOR_DIMENSION, self.MAX_SENSOR_DIMENSION, "sensor_dimension" | ||
| ) | ||
@@ -433,6 +429,3 @@ size_errors = "" | ||
| self.__validate_range( | ||
| value, | ||
| self.MIN_THERMAL_GRADIENT, | ||
| self.MAX_THERMAL_GRADIENT, | ||
| "thermal_gradient", | ||
| value, self.MIN_THERMAL_GRADIENT, self.MAX_THERMAL_GRADIENT, "thermal_gradient" | ||
| ) | ||
@@ -531,6 +524,3 @@ self._thermal_gradient = value | ||
| def __init__( | ||
| self, | ||
| input: MicrostructureInput, | ||
| result: MicrostructureResultMessage, | ||
| user_data_path: str, | ||
| self, input: MicrostructureInput, result: MicrostructureResultMessage, user_data_path: str | ||
| ) -> None: | ||
@@ -547,3 +537,3 @@ """Initialize a ``MicrostructureSummary`` object.""" | ||
| outpath = os.path.join(user_data_path, id) | ||
| self._result = Microstructure2DResult(result, outpath) | ||
| self._result = _Microstructure2DResult(result, outpath) | ||
@@ -655,3 +645,2 @@ def __repr__(self): | ||
| Average grain size (µm). | ||
| """ | ||
@@ -665,3 +654,3 @@ | ||
| class Microstructure2DResult: | ||
| class _Microstructure2DResult: | ||
| """Provides the results of a 2D microstructure simulation.""" | ||
@@ -707,17 +696,2 @@ | ||
| @property | ||
| def xy_average_grain_size(self) -> float: | ||
| """Average grain size (µm) for the XY plane.""" | ||
| return self._xy_average_grain_size | ||
| @property | ||
| def xz_average_grain_size(self) -> float: | ||
| """Average grain size (µm) for the XZ plane.""" | ||
| return self._xz_average_grain_size | ||
| @property | ||
| def yz_average_grain_size(self) -> float: | ||
| """Average grain size (µm) for the YZ plane.""" | ||
| return self._yz_average_grain_size | ||
| def __repr__(self): | ||
@@ -724,0 +698,0 @@ repr = "" |
@@ -23,3 +23,2 @@ # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| """Provides miscellaneous utility functions.""" | ||
| import random | ||
@@ -29,15 +28,5 @@ import string | ||
| def short_uuid(nchars: int = 12) -> str: | ||
| """Generate a short UUID. | ||
| Parameters | ||
| ---------- | ||
| nchars : int, default 12 | ||
| Number of characters in the UUID. Only applies if ``nchars`` is greater | ||
| than 6. Using the default, the probability | ||
| of a collision is 1.23e-17 for 1 billion rounds. | ||
| """ | ||
| alphabet = string.ascii_letters + string.digits | ||
| nchars = max(6, nchars) | ||
| return "".join(random.choices(alphabet, k=nchars)) # noqa: S311 | ||
| def short_uuid() -> str: | ||
| """Generate a short UUID.""" | ||
| alphabet = string.ascii_lowercase + string.digits | ||
| return "".join(random.choices(alphabet, k=8)) |
@@ -23,3 +23,2 @@ # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| """Provides data storage and utility methods for a parametric study.""" | ||
| from ansys.additive.core.parametric_study.constants import ( # noqa: F401 | ||
@@ -31,6 +30,4 @@ DEFAULT_ITERATION, | ||
| ) | ||
| from ansys.additive.core.parametric_study.parametric_runner import ParametricRunner # noqa: F401 | ||
| from ansys.additive.core.parametric_study.parametric_study import ParametricStudy # noqa: F401 | ||
| from ansys.additive.core.parametric_study.parametric_study_progress_handler import ( # noqa: F401 | ||
| ParametricStudyProgressHandler, | ||
| ) | ||
| from ansys.additive.core.parametric_study.parametric_utils import ( # noqa: F401 | ||
@@ -37,0 +34,0 @@ build_rate, |
@@ -55,4 +55,2 @@ # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| """Laser scan speed (m/s).""" | ||
| PV_RATIO = "Laser Power/Scan Speed (J/m)" | ||
| """Ratio of laser power to scan speed (J/m).""" | ||
| START_ANGLE = "Start Angle (degrees)" | ||
@@ -132,3 +130,3 @@ """Hatch scan angle for first layer (degrees).""" | ||
| """Default priority assigned to new simulations.""" | ||
| FORMAT_VERSION = 3 | ||
| FORMAT_VERSION = 2 | ||
| """Parametric study file format version.""" |
@@ -24,4 +24,8 @@ # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| from typing import Optional | ||
| def build_rate(scan_speed: float, layer_thickness: float, hatch_spacing: float) -> float: | ||
| def build_rate( | ||
| scan_speed: float, layer_thickness: float, hatch_spacing: Optional[float] = None | ||
| ) -> float: | ||
| """Calculate the build rate. | ||
@@ -39,3 +43,3 @@ | ||
| Powder deposit layer thickness. | ||
| hatch_spacing : float | ||
| hatch_spacing : float, default: None | ||
| Distance between hatch scan lines. | ||
@@ -46,7 +50,9 @@ | ||
| float | ||
| Volumetric build rate is returned. If input units are m/s and m, | ||
| the output units are m^3/s. | ||
| Volumetric build rate is returned if hatch spacing is provided. | ||
| Otherwise, an area build rate is returned. If input units are m/s and m, | ||
| the output units are m^3/s or m^2/s. | ||
| """ | ||
| return round(scan_speed * layer_thickness * hatch_spacing, 16) | ||
| if hatch_spacing is None: | ||
| return scan_speed * layer_thickness | ||
| return scan_speed * layer_thickness * hatch_spacing | ||
@@ -58,3 +64,3 @@ | ||
| layer_thickness: float, | ||
| hatch_spacing: float, | ||
| hatch_spacing: Optional[float] = None, | ||
| ) -> float: | ||
@@ -75,3 +81,3 @@ """Calculate the energy density. | ||
| Powder deposit layer thickness. | ||
| hatch_spacing : float | ||
| hatch_spacing : float, default: None | ||
| Distance between hatch scan lines. | ||
@@ -82,7 +88,7 @@ | ||
| float | ||
| Volumetric energy density is returned. If input units are W, m/s, and m, | ||
| the output units are J/m^3. | ||
| Volumetric energy density is returned if hatch spacing is provided. | ||
| Otherwise an area energy density is returned. If input units are W, m/s, m, or m, | ||
| the output units are J/m^3 or J/m^2. | ||
| """ | ||
| br = build_rate(scan_speed, layer_thickness, hatch_spacing) | ||
| return laser_power / br if br else float("nan") |
@@ -23,16 +23,13 @@ # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| """Provides input and result summary containers for porosity simulations.""" | ||
| import math | ||
| from ansys.additive.core.machine import AdditiveMachine | ||
| from ansys.additive.core.material import AdditiveMaterial | ||
| from ansys.additive.core.simulation_input_base import SimulationInputBase | ||
| from ansys.api.additive.v0.additive_domain_pb2 import ( | ||
| PorosityInput as PorosityInputMessage, | ||
| ) | ||
| from ansys.api.additive.v0.additive_domain_pb2 import PorosityInput as PorosityInputMessage | ||
| from ansys.api.additive.v0.additive_domain_pb2 import PorosityResult | ||
| from ansys.api.additive.v0.additive_simulation_pb2 import SimulationRequest | ||
| from ansys.additive.core.machine import AdditiveMachine | ||
| from ansys.additive.core.material import AdditiveMaterial | ||
| class PorosityInput(SimulationInputBase): | ||
| class PorosityInput: | ||
| """Provides input parameters for porosity simulation.""" | ||
@@ -49,2 +46,3 @@ | ||
| self, | ||
| id: str = "", | ||
| *, | ||
@@ -54,12 +52,12 @@ size_x: float = DEFAULT_SAMPLE_SIZE, | ||
| size_z: float = DEFAULT_SAMPLE_SIZE, | ||
| machine: AdditiveMachine = None, | ||
| material: AdditiveMaterial = None, | ||
| machine: AdditiveMachine = AdditiveMachine(), | ||
| material: AdditiveMaterial = AdditiveMaterial(), | ||
| ): | ||
| """Initialize a ``PorosityInput`` object.""" | ||
| super().__init__() | ||
| self.id = id | ||
| self.size_x = size_x | ||
| self.size_y = size_y | ||
| self.size_z = size_z | ||
| self.machine = machine if machine else AdditiveMachine() | ||
| self.material = material if material else AdditiveMaterial() | ||
| self.machine = machine | ||
| self.material = material | ||
@@ -94,2 +92,11 @@ def __repr__(self): | ||
| @property | ||
| def id(self) -> str: | ||
| """User-provided ID for the simulation.""" | ||
| return self._id | ||
| @id.setter | ||
| def id(self, value): | ||
| self._id = value | ||
| @property | ||
| def machine(self) -> AdditiveMachine: | ||
@@ -96,0 +103,0 @@ """Machine-related parameters.""" |
@@ -28,17 +28,13 @@ # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| from ansys.api.additive.v0.additive_domain_pb2 import Progress as ProgressMsg | ||
| from ansys.api.additive.v0.additive_domain_pb2 import ProgressState as ProgressMsgState | ||
| from pydantic import BaseModel | ||
| from tqdm import tqdm | ||
| from ansys.api.additive.v0.additive_domain_pb2 import Progress as ProgressMsg | ||
| from ansys.api.additive.v0.additive_domain_pb2 import ProgressState as ProgressMsgState | ||
| from ansys.api.additive.v0.additive_operations_pb2 import OperationMetadata | ||
| class ProgressState(IntEnum): | ||
| """Simulation progress status.""" | ||
| NEW = ProgressMsgState.PROGRESS_STATE_NEW | ||
| """Simulation created and not yet queued to run.""" | ||
| WAITING = ProgressMsgState.PROGRESS_STATE_WAITING | ||
| """Simulation is queued and waiting to start.""" | ||
| """Waiting for the simulation to start.""" | ||
| RUNNING = ProgressMsgState.PROGRESS_STATE_EXECUTING | ||
@@ -50,6 +46,2 @@ """Simulation is running.""" | ||
| """Simulation has errored.""" | ||
| CANCELLED = ProgressMsgState.PROGRESS_STATE_CANCELLED | ||
| """Simulation has been cancelled.""" | ||
| WARNING = ProgressMsgState.PROGRESS_STATE_WARNING | ||
| """Simulation completed with warnings.""" | ||
@@ -77,15 +69,4 @@ | ||
| @classmethod | ||
| def from_operation_metadata(cls, metadata: OperationMetadata): | ||
| """Create a ``Progress`` object from an operation metadata (long-running operations) protobuf message.""" # noqa: E501 | ||
| return cls( | ||
| sim_id=metadata.simulation_id, | ||
| state=metadata.state, | ||
| percent_complete=metadata.percent_complete, | ||
| message=metadata.message, | ||
| context=metadata.context, | ||
| ) | ||
| def __str__(self): | ||
| return f"{self.sim_id}: {self.state.name} - {self.percent_complete}% - {self.context} - {self.message}" # noqa: E501 | ||
| return f"{self.id}: {self.state.name} - {self.percent_complete}% - {self.context} - {self.message}" | ||
@@ -104,3 +85,2 @@ | ||
| Progress information. | ||
| """ | ||
@@ -117,3 +97,2 @@ raise NotImplementedError | ||
| Simulation ID. | ||
| """ | ||
@@ -133,3 +112,2 @@ | ||
| Latest progress. | ||
| """ | ||
@@ -136,0 +114,0 @@ # Don't send progress when generating docs |
@@ -24,3 +24,3 @@ # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| from ansys.additive.core.server_connection.constants import DEFAULT_PRODUCT_VERSION # noqa: F401 | ||
| from ansys.additive.core.server_connection.server_connection import ServerConnection # noqa: F401 | ||
| from ansys.additive.core.server_connection.constants import DEFAULT_PRODUCT_VERSION | ||
| from ansys.additive.core.server_connection.server_connection import ServerConnection |
@@ -31,3 +31,3 @@ # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| """Product name for the Additive server in a PyPIM environment.""" | ||
| DEFAULT_PRODUCT_VERSION = "251" | ||
| DEFAULT_PRODUCT_VERSION = "242" | ||
| """Default Ansys product version to use for the Additive server.""" | ||
@@ -34,0 +34,0 @@ ADDITIVE_SERVER_EXE_NAME = "additiveserver" |
@@ -23,9 +23,10 @@ # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| """Provides startup utilities for a local Additive server.""" | ||
| from __future__ import annotations | ||
| from datetime import datetime | ||
| import os | ||
| from pathlib import Path | ||
| import socket | ||
| import subprocess | ||
| import time | ||
| from datetime import datetime | ||
| from pathlib import Path | ||
@@ -72,3 +73,2 @@ from ansys.additive.core import USER_DATA_PATH | ||
| Server process. To stop the server, call the ``kill()`` method on the returned object. | ||
| """ | ||
@@ -116,3 +116,3 @@ server_exe = "" | ||
| with open(os.path.join(cwd, f"additiveserver_{start_time}.log"), "w") as log_file: | ||
| server_process = subprocess.Popen( # noqa: S603 | ||
| server_process = subprocess.Popen( | ||
| f'"{server_exe}" --port {port}', | ||
@@ -154,3 +154,2 @@ shell=os.name != "nt", # use shell on Linux | ||
| This port may be taken by the time you try to use it. | ||
| """ | ||
@@ -157,0 +156,0 @@ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
@@ -24,5 +24,7 @@ # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| from __future__ import annotations | ||
| import ipaddress | ||
| from pathlib import Path | ||
| import socket | ||
| from pathlib import Path | ||
@@ -79,3 +81,4 @@ from ansys.tools.common.cyberchannel import create_channel as create_cyber_channel | ||
| transport_mode : TransportMode | str | ||
| The transport mode to use for the connection. Can be a member of the :class:`TransportMode <.constants.TransportMode>` enum or a string | ||
| The transport mode to use for the connection. Can be a member of the :class:`TransportMode | ||
| <.constants.TransportMode>` enum or a string | ||
| ('insecure', 'mtls', or 'uds'). | ||
@@ -92,10 +95,9 @@ certs_dir : Path | str | None | ||
| Size, in bytes, of the buffer used to receive messages. Default is :obj:`MAX_RCV_MSG_LEN`. | ||
| Raises | ||
| ------ | ||
| ValueError | ||
| If the target string is improperly formed, the transport mode is invalid, or an unsupported transport mode is specified. | ||
| If the target string is improperly formed, the transport mode is invalid, or an unsupported | ||
| transport mode is specified. | ||
| ConnectionError | ||
| If unable to connect to the Unix Domain Socket when using 'uds' transport mode. | ||
| Returns | ||
@@ -107,3 +109,2 @@ ------- | ||
| Path to the Unix Domain Socket file if using 'uds' transport mode, otherwise None. | ||
| """ | ||
@@ -132,17 +133,23 @@ | ||
| ) | ||
| return create_cyber_channel( | ||
| transport_mode="insecure", | ||
| host=host, | ||
| port=port_str, | ||
| grpc_options=[("grpc.max_receive_message_length", max_rcv_msg_len)], | ||
| ), None | ||
| return ( | ||
| create_cyber_channel( | ||
| transport_mode="insecure", | ||
| host=host, | ||
| port=port_str, | ||
| grpc_options=[("grpc.max_receive_message_length", max_rcv_msg_len)], | ||
| ), | ||
| None, | ||
| ) | ||
| case TransportMode.MTLS: | ||
| return create_cyber_channel( | ||
| transport_mode="mtls", | ||
| host=host, | ||
| port=port_str, | ||
| certs_dir=certs_dir, | ||
| grpc_options=[("grpc.max_receive_message_length", max_rcv_msg_len)], | ||
| ), None | ||
| return ( | ||
| create_cyber_channel( | ||
| transport_mode="mtls", | ||
| host=host, | ||
| port=port_str, | ||
| certs_dir=certs_dir, | ||
| grpc_options=[("grpc.max_receive_message_length", max_rcv_msg_len)], | ||
| ), | ||
| None, | ||
| ) | ||
@@ -166,7 +173,8 @@ case TransportMode.UDS: | ||
| raise ConnectionError( | ||
| f"Could not connect to UDS socket in {uds_folder or 'None'} with id {uds_id or 'None'}." | ||
| f"Could not connect to UDS socket in {uds_folder or 'None'} with id " | ||
| f"{uds_id or 'None'}." | ||
| ) | ||
| return uds_channel, Path(uds_channel._channel.target().decode().removeprefix("unix:")) # type: ignore | ||
| return uds_channel, Path(uds_channel._channel.target().decode().removeprefix("unix:")) | ||
| case _: | ||
| raise ValueError(f"Unsupported transport mode: {transport_mode}") |
@@ -23,15 +23,18 @@ # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| """Provides definitions and untilities for connecting to the Additive server.""" | ||
| from __future__ import annotations | ||
| from dataclasses import dataclass | ||
| import logging | ||
| import os | ||
| from pathlib import Path | ||
| import signal | ||
| import time | ||
| from dataclasses import dataclass | ||
| from pathlib import Path | ||
| from ansys.api.additive.v0.about_pb2_grpc import AboutServiceStub | ||
| from ansys.api.additive.v0.additive_materials_pb2_grpc import MaterialsServiceStub | ||
| from ansys.api.additive.v0.additive_simulation_pb2_grpc import SimulationServiceStub | ||
| import ansys.platform.instancemanagement as pypim | ||
| from google.protobuf.empty_pb2 import Empty | ||
| import grpc | ||
| from google.longrunning.operations_pb2_grpc import OperationsStub | ||
| from google.protobuf.empty_pb2 import Empty | ||
| import ansys.platform.instancemanagement as pypim | ||
| from ansys.additive.core.server_connection.constants import ( | ||
@@ -45,6 +48,2 @@ DEFAULT_PRODUCT_VERSION, | ||
| from ansys.additive.core.server_connection.network_utils import create_channel | ||
| from ansys.api.additive.v0.about_pb2_grpc import AboutServiceStub | ||
| from ansys.api.additive.v0.additive_materials_pb2_grpc import MaterialsServiceStub | ||
| from ansys.api.additive.v0.additive_settings_pb2_grpc import SettingsServiceStub | ||
| from ansys.api.additive.v0.additive_simulation_pb2_grpc import SimulationServiceStub | ||
@@ -64,3 +63,2 @@ | ||
| Server metadata. | ||
| """ | ||
@@ -75,12 +73,3 @@ | ||
| def __str__(self) -> str: | ||
| if not self.connected: | ||
| return f"Server {self.channel_str} is not connected." | ||
| str = f"Server {self.channel_str} is connected." | ||
| if self.metadata: | ||
| for key, value in self.metadata.items(): | ||
| str += f"\n {key}: {value}" | ||
| return str | ||
| class ServerConnection: | ||
@@ -99,3 +88,4 @@ """Provides connection to Additive server. | ||
| transport_mode : TransportMode | str | ||
| The transport mode to use for the connection. Can be a member of the :class:`TransportMode <.constants.TransportMode>` enum or a string | ||
| The transport mode to use for the connection. Can be a member of the :class:`TransportMode | ||
| <.constants.TransportMode>` enum or a string | ||
| ('insecure', 'mtls', or 'uds'). | ||
@@ -127,3 +117,2 @@ certs_dir : Path | str | None | ||
| version. | ||
| """ | ||
@@ -146,3 +135,3 @@ | ||
| if channel and addr: | ||
| if channel is not None and addr is not None: | ||
| raise ValueError("Both 'channel' and 'addr' cannot both be specified.") | ||
@@ -170,8 +159,5 @@ | ||
| self._server_process = LocalServer.launch( | ||
| port, | ||
| product_version=product_version, | ||
| linux_install_path=linux_install_path, | ||
| port, product_version=product_version, linux_install_path=linux_install_path | ||
| ) | ||
| target = f"{LOCALHOST}:{port}" | ||
| # Save UDS parameters for disconnecting | ||
| channel_result, self._uds_file = create_channel( | ||
@@ -186,4 +172,2 @@ target, transport_mode, certs_dir, uds_dir, uds_id, allow_remote_host | ||
| self._about_stub = AboutServiceStub(self._channel) | ||
| self._operations_stub = OperationsStub(self._channel) | ||
| self._settings_stub = SettingsServiceStub(self._channel) | ||
@@ -195,11 +179,6 @@ if not self.ready(): | ||
| def __del__(self) -> None: | ||
| """Disconnect from server.""" | ||
| self.disconnect() | ||
| def disconnect(self): | ||
| """Clean up server connection.""" | ||
| def __del__(self): | ||
| """Destructor for cleaning up server connection.""" | ||
| if hasattr(self, "_server_instance") and self._server_instance: | ||
| self._server_instance.delete() | ||
| self._server_instance = None | ||
| if hasattr(self, "_server_process") and self._server_process: | ||
@@ -224,3 +203,2 @@ if os.name == "nt": | ||
| self._server_process.send_signal(signal.SIGINT) | ||
| self._server_process = None | ||
@@ -230,2 +208,3 @@ @property | ||
| """GRPC channel target. | ||
| The form is generally ``"ip:port"``. For example, ``"127.0.0.1:50052"``. | ||
@@ -252,12 +231,2 @@ In the case of UDS channels, the form is the path to the UDS socket file. | ||
| @property | ||
| def operations_stub(self) -> OperationsStub: | ||
| """Operations service stub.""" | ||
| return self._operations_stub | ||
| @property | ||
| def settings_stub(self) -> SettingsServiceStub: | ||
| """Settings service stub.""" | ||
| return self._settings_stub | ||
| @property | ||
| def uds_file(self) -> Path | None: | ||
@@ -296,3 +265,2 @@ """Path to the Unix Domain Socket file if using 'uds' transport mode, otherwise None.""" | ||
| without receiving a response from the server. | ||
| """ | ||
@@ -299,0 +267,0 @@ ready = False |
@@ -23,4 +23,2 @@ # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| """Provides common definitions and classes for simulations.""" | ||
| from enum import Enum | ||
| from typing import Union | ||
@@ -34,3 +32,3 @@ | ||
| class SimulationType(str, Enum): | ||
| class SimulationType: | ||
| """Provides simulation types.""" | ||
@@ -46,23 +44,11 @@ | ||
| class SimulationStatus(str, Enum): | ||
| class SimulationStatus: | ||
| """Provides simulation status values.""" | ||
| # NOTE: Values are listed in order of precedence when deduping. | ||
| # For example, if duplicate COMPLETED and WARNING simulations are | ||
| # found when removing duplicates, the COMPLETED simulation will be | ||
| # kept. | ||
| PENDING = "Pending" | ||
| """Simulation is waiting to run.""" | ||
| COMPLETED = "Completed" | ||
| """Simulation completed successfully.""" | ||
| WARNING = "Warning" | ||
| """Simulation completed with warnings.""" | ||
| """Simulation was run.""" | ||
| ERROR = "Error" | ||
| """Simulation errored before completion.""" | ||
| CANCELLED = "Cancelled" | ||
| """Simulation was cancelled.""" | ||
| RUNNING = "Running" | ||
| """Simulation is running.""" | ||
| PENDING = "Pending" | ||
| """Simulation is queued and waiting to run.""" | ||
| NEW = "New" | ||
| """Simulation is created but not yet queued to run.""" | ||
| """Simulation errored.""" | ||
| SKIP = "Skip" | ||
@@ -69,0 +55,0 @@ """Do not run this simulation, only applies to parametric studies.""" |
@@ -23,9 +23,7 @@ # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| """Provides input and result summary containers for single bead simulations.""" | ||
| import contextlib | ||
| import math | ||
| import os | ||
| import zipfile | ||
| import numpy as np | ||
| from ansys.api.additive.v0.additive_domain_pb2 import MeltPool as MeltPoolMessage | ||
| from ansys.api.additive.v0.additive_domain_pb2 import SingleBeadInput as SingleBeadInputMessage | ||
| from ansys.api.additive.v0.additive_simulation_pb2 import SimulationRequest | ||
| from pandas import DataFrame | ||
@@ -35,11 +33,5 @@ | ||
| from ansys.additive.core.material import AdditiveMaterial | ||
| from ansys.additive.core.simulation_input_base import SimulationInputBase | ||
| from ansys.api.additive.v0.additive_domain_pb2 import MeltPool as MeltPoolMessage | ||
| from ansys.api.additive.v0.additive_domain_pb2 import ( | ||
| SingleBeadInput as SingleBeadInputMessage, | ||
| ) | ||
| from ansys.api.additive.v0.additive_simulation_pb2 import SimulationRequest | ||
| class SingleBeadInput(SimulationInputBase): | ||
| class SingleBeadInput: | ||
| """Provides input parameters for a single bead simulation.""" | ||
@@ -53,27 +45,15 @@ | ||
| """Maximum bead length (m).""" | ||
| DEFAULT_OUTPUT_THERMAL_HISTORY = False | ||
| """Default output thermal history flag.""" | ||
| DEFAULT_THERMAL_HISTORY_INTERVAL = 1 | ||
| """Default thermal history interval.""" | ||
| MIN_THERMAL_HISTORY_INTERVAL = 1 | ||
| """Minimum thermal history interval.""" | ||
| MAX_THERMAL_HISTORY_INTERVAL = 10000 | ||
| """Maximum thermal history interval.""" | ||
| def __init__( | ||
| self, | ||
| *, | ||
| id: str = "", | ||
| bead_length: float = DEFAULT_BEAD_LENGTH, | ||
| machine: AdditiveMachine = None, | ||
| material: AdditiveMaterial = None, | ||
| output_thermal_history: bool = DEFAULT_OUTPUT_THERMAL_HISTORY, | ||
| thermal_history_interval: int = DEFAULT_THERMAL_HISTORY_INTERVAL, | ||
| machine: AdditiveMachine = AdditiveMachine(), | ||
| material: AdditiveMaterial = AdditiveMaterial(), | ||
| ): | ||
| """Initialize a ``SingleBeadInput`` object.""" | ||
| super().__init__() | ||
| self.id = id | ||
| self.bead_length = bead_length | ||
| self.machine = machine if machine else AdditiveMachine() | ||
| self.material = material if material else AdditiveMaterial() | ||
| self.output_thermal_history = output_thermal_history | ||
| self.thermal_history_interval = thermal_history_interval | ||
| self.machine = machine | ||
| self.material = material | ||
@@ -97,4 +77,2 @@ def __repr__(self): | ||
| and self.material == __o.material | ||
| and self.output_thermal_history == __o.output_thermal_history | ||
| and self.thermal_history_interval == __o.thermal_history_interval | ||
| ) | ||
@@ -109,2 +87,11 @@ | ||
| @property | ||
| def id(self) -> str: | ||
| """User-provided ID for the simulation.""" | ||
| return self._id | ||
| @id.setter | ||
| def id(self, value): | ||
| self._id = value | ||
| @property | ||
| def machine(self) -> AdditiveMachine: | ||
@@ -141,30 +128,2 @@ """Machine parameters.""" | ||
| @property | ||
| def output_thermal_history(self) -> bool: | ||
| """Flag indicating whether to output the thermal history of the simulation.""" | ||
| return self._output_thermal_history | ||
| @output_thermal_history.setter | ||
| def output_thermal_history(self, value: bool): | ||
| self._output_thermal_history = value | ||
| @property | ||
| def thermal_history_interval(self) -> int: | ||
| """Interval, in simulation steps, between thermal history results. | ||
| Use ``1`` to create thermal history results for every simulation step, | ||
| ``2`` for every other step, and so on. | ||
| """ | ||
| return self._thermal_history_interval | ||
| @thermal_history_interval.setter | ||
| def thermal_history_interval(self, value: int): | ||
| self.__validate_range( | ||
| value, | ||
| self.MIN_THERMAL_HISTORY_INTERVAL, | ||
| self.MAX_THERMAL_HISTORY_INTERVAL, | ||
| "thermal_history_interval", | ||
| ) | ||
| self._thermal_history_interval = value | ||
| def _to_simulation_request(self) -> SimulationRequest: | ||
@@ -176,4 +135,2 @@ """Convert this object into a simulation request message.""" | ||
| bead_length=self.bead_length, | ||
| output_thermal_history=self.output_thermal_history, | ||
| thermal_history_interval=self.thermal_history_interval, | ||
| ) | ||
@@ -201,13 +158,4 @@ return SimulationRequest(id=self.id, single_bead_input=input) | ||
| def __init__(self, msg: MeltPoolMessage, thermal_history_output: str | None = None): | ||
| """Initialize a ``MeltPool`` object. | ||
| Parameters | ||
| ---------- | ||
| msg: MeltPoolMessage | ||
| The message containing the melt pool data. | ||
| thermal_history_output: str | None | ||
| Path to the thermal history output file. | ||
| """ | ||
| def __init__(self, msg: MeltPoolMessage): | ||
| """Initialize a ``MeltPool`` object.""" | ||
| bead_length = [ts.laser_x for ts in msg.time_steps] | ||
@@ -230,3 +178,2 @@ length = [ts.length for ts in msg.time_steps] | ||
| self._df.index.name = "bead_length" | ||
| self._thermal_history_output = thermal_history_output | ||
@@ -250,34 +197,2 @@ def data_frame(self) -> DataFrame: | ||
| def depth_over_width(self) -> float: | ||
| """Return the median reference depth over reference width.""" | ||
| depth = self.median_reference_depth() | ||
| width = self.median_reference_width() | ||
| return depth / width if width != 0 else np.nan | ||
| def length_over_width(self) -> float: | ||
| """Return the median length over width.""" | ||
| length = self.median_length() | ||
| width = self.median_width() | ||
| return length / width if width != 0 else np.nan | ||
| def median_width(self) -> float: | ||
| """Return the median width.""" | ||
| return self._df[MeltPoolColumnNames.WIDTH].median() | ||
| def median_depth(self) -> float: | ||
| """Return the median depth.""" | ||
| return self._df[MeltPoolColumnNames.DEPTH].median() | ||
| def median_length(self) -> float: | ||
| """Return the median length.""" | ||
| return self._df[MeltPoolColumnNames.LENGTH].median() | ||
| def median_reference_width(self) -> float: | ||
| """Return the median reference width.""" | ||
| return self._df[MeltPoolColumnNames.REFERENCE_WIDTH].median() | ||
| def median_reference_depth(self) -> float: | ||
| """Return the median reference depth.""" | ||
| return self._df[MeltPoolColumnNames.REFERENCE_DEPTH].median() | ||
| def __eq__(self, __o: object) -> bool: | ||
@@ -291,18 +206,8 @@ if not isinstance(__o, MeltPool): | ||
| repr += self._df.to_string() | ||
| repr += ( | ||
| "\n" + "grid_full_thermal_sensor_file_output_path: " + str(self.thermal_history_output) | ||
| ) | ||
| return repr | ||
| @property | ||
| def thermal_history_output(self) -> str | None: | ||
| """Path to the thermal history output file.""" | ||
| return self._thermal_history_output | ||
| class SingleBeadSummary: | ||
| """Provides a summary of a single bead simulation.""" | ||
| THERMAL_HISTORY_OUTPUT_ZIP = "gridfullthermal.zip" | ||
| def __init__( | ||
@@ -312,3 +217,2 @@ self, | ||
| msg: MeltPoolMessage, | ||
| thermal_history_output: str | None = None, | ||
| ): | ||
@@ -321,5 +225,3 @@ """Initialize a ``SingleBeadSummary`` object.""" | ||
| self._input = input | ||
| self._melt_pool = MeltPool(msg, thermal_history_output) | ||
| if thermal_history_output is not None: | ||
| self._extract_thermal_history(thermal_history_output) | ||
| self._melt_pool = MeltPool(msg) | ||
@@ -341,11 +243,1 @@ @property | ||
| return repr | ||
| def _extract_thermal_history(self, thermal_history_output): | ||
| """Extract the thermal history output.""" | ||
| zip_file = os.path.join(thermal_history_output, self.THERMAL_HISTORY_OUTPUT_ZIP) | ||
| if not os.path.isfile(zip_file): | ||
| raise FileNotFoundError("Thermal history files not found: " + zip_file) | ||
| with zipfile.ZipFile(zip_file, "r") as zip_ref: | ||
| zip_ref.extractall(thermal_history_output) | ||
| with contextlib.suppress(OSError): | ||
| os.remove(zip_file) |
@@ -23,19 +23,20 @@ # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| """Provides input and result summary containers for thermal history simulations.""" | ||
| from __future__ import annotations | ||
| from ansys.additive.core.geometry_file import BuildFile, StlFile | ||
| from ansys.additive.core.machine import AdditiveMachine | ||
| from ansys.additive.core.material import AdditiveMaterial | ||
| from ansys.additive.core.simulation_input_base import SimulationInputBase | ||
| from ansys.api.additive.v0.additive_domain_pb2 import BuildFile as BuildFileMessage | ||
| from ansys.api.additive.v0.additive_domain_pb2 import ( | ||
| CoaxialAverageSensorInputs as CoaxialAverageSensorInputsMessage, | ||
| ) | ||
| from ansys.api.additive.v0.additive_domain_pb2 import Range as RangeMessage | ||
| from ansys.api.additive.v0.additive_domain_pb2 import StlFile as StlFileMessage | ||
| from ansys.api.additive.v0.additive_domain_pb2 import ( | ||
| ThermalHistoryInput as ThermalHistoryInputMessage, | ||
| ) | ||
| from ansys.api.additive.v0.additive_domain_pb2 import BuildFile as BuildFileMessage | ||
| from ansys.api.additive.v0.additive_domain_pb2 import Range as RangeMessage | ||
| from ansys.api.additive.v0.additive_domain_pb2 import StlFile as StlFileMessage | ||
| from ansys.api.additive.v0.additive_simulation_pb2 import SimulationRequest | ||
| from ansys.additive.core.geometry_file import BuildFile, StlFile | ||
| from ansys.additive.core.machine import AdditiveMachine | ||
| from ansys.additive.core.material import AdditiveMaterial | ||
| class Range: | ||
@@ -129,8 +130,5 @@ """Defines a range of values.""" | ||
| def _to_coaxial_average_sensor_inputs_message( | ||
| self, | ||
| ) -> CoaxialAverageSensorInputsMessage: | ||
| def _to_coaxial_average_sensor_inputs_message(self) -> CoaxialAverageSensorInputsMessage: | ||
| """Create a coaxial average sensor input message to send to the server | ||
| based upon this object. | ||
| """ | ||
| based upon this object.""" | ||
| msg = CoaxialAverageSensorInputsMessage(sensor_radius=self.radius) | ||
@@ -142,3 +140,3 @@ for z in self.z_heights: | ||
| class ThermalHistoryInput(SimulationInputBase): | ||
| class ThermalHistoryInput: | ||
| """Provides input parameters for microstructure simulation.""" | ||
@@ -148,16 +146,14 @@ | ||
| self, | ||
| *, | ||
| machine: AdditiveMachine = None, | ||
| material: AdditiveMaterial = None, | ||
| id: str = "", | ||
| machine: AdditiveMachine = AdditiveMachine(), | ||
| material: AdditiveMaterial = AdditiveMaterial(), | ||
| geometry: StlFile | BuildFile = None, | ||
| coax_ave_sensor_inputs: CoaxialAverageSensorInputs = None, | ||
| coax_ave_sensor_inputs: CoaxialAverageSensorInputs = CoaxialAverageSensorInputs(), | ||
| ): | ||
| """Initialize a ``ThermalHistoryInput`` object.""" | ||
| super().__init__() | ||
| self._machine = machine if machine else AdditiveMachine() | ||
| self._material = material if material else AdditiveMaterial() | ||
| self._id = id | ||
| self._machine = machine | ||
| self._material = material | ||
| self._geometry = geometry | ||
| self._coax_ave_sensor_inputs = ( | ||
| coax_ave_sensor_inputs if coax_ave_sensor_inputs else CoaxialAverageSensorInputs() | ||
| ) | ||
| self._coax_ave_sensor_inputs = coax_ave_sensor_inputs | ||
@@ -176,5 +172,17 @@ def __repr__(self): | ||
| return False | ||
| return all(getattr(self, k) == getattr(other, k) for k in self.__dict__) | ||
| for k in self.__dict__: | ||
| if getattr(self, k) != getattr(other, k): | ||
| return False | ||
| return True | ||
| @property | ||
| def id(self) -> str: | ||
| """User-provided ID for the simulation.""" | ||
| return self._id | ||
| @id.setter | ||
| def id(self, value): | ||
| self._id = value | ||
| @property | ||
| def machine(self) -> AdditiveMachine: | ||
@@ -181,0 +189,0 @@ """Machine parameters.""" |
| # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| # SPDX-License-Identifier: MIT | ||
| # | ||
| # | ||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| # of this software and associated documentation files (the "Software"), to deal | ||
| # in the Software without restriction, including without limitation the rights | ||
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| # copies of the Software, and to permit persons to whom the Software is | ||
| # furnished to do so, subject to the following conditions: | ||
| # | ||
| # The above copyright notice and this permission notice shall be included in all | ||
| # copies or substantial portions of the Software. | ||
| # | ||
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| # SOFTWARE. | ||
| """Provides a class to update progress when running parametric study simulations.""" | ||
| import threading | ||
| from ansys.additive.core.logger import LOG | ||
| from ansys.additive.core.parametric_study import ParametricStudy | ||
| from ansys.additive.core.progress_handler import ( | ||
| IProgressHandler, | ||
| Progress, | ||
| ProgressState, | ||
| ) | ||
| from ansys.additive.core.simulation import SimulationStatus | ||
| class ParametricStudyProgressHandler(IProgressHandler): | ||
| """Provides methods to update parametric study simulation status. | ||
| Parameters | ||
| ---------- | ||
| study : ParametricStudy | ||
| Parametric study to update. | ||
| """ | ||
| def __init__( | ||
| self, | ||
| study: ParametricStudy, | ||
| ) -> None: | ||
| """Initialize progress handler.""" | ||
| self._study_lock = threading.Lock() | ||
| self._study = study | ||
| # Store the last state of each simulation to avoid | ||
| # unnecessary disk writes when setting the simulation status | ||
| # on the study. | ||
| self._last_progress_states = {} | ||
| def update(self, progress: Progress) -> None: | ||
| """Update the progress of a simulation. | ||
| Parameters | ||
| ---------- | ||
| progress : Progress | ||
| Progress information for the simulation. | ||
| """ | ||
| if ( | ||
| progress.sim_id in self._last_progress_states | ||
| and progress.state == self._last_progress_states[progress.sim_id] | ||
| ): | ||
| return | ||
| LOG.debug(f"Updating progress for {progress.sim_id}") | ||
| if progress.state == ProgressState.WAITING: | ||
| self._update_simulation_status(progress.sim_id, SimulationStatus.PENDING) | ||
| elif progress.state == ProgressState.CANCELLED: | ||
| self._update_simulation_status(progress.sim_id, SimulationStatus.CANCELLED) | ||
| elif progress.state == ProgressState.RUNNING: | ||
| self._update_simulation_status(progress.sim_id, SimulationStatus.RUNNING) | ||
| elif progress.state == ProgressState.WARNING: | ||
| self._update_simulation_status(progress.sim_id, SimulationStatus.WARNING) | ||
| elif progress.state == ProgressState.COMPLETED: | ||
| self._update_simulation_status(progress.sim_id, SimulationStatus.COMPLETED) | ||
| elif progress.state == ProgressState.ERROR: | ||
| self._update_simulation_status( | ||
| progress.sim_id, SimulationStatus.ERROR, progress.message | ||
| ) | ||
| self._last_progress_states[progress.sim_id] = progress.state | ||
| def _update_simulation_status( | ||
| self, sim_id: str, status: SimulationStatus, message: str = None | ||
| ) -> None: | ||
| """Update the status of a simulation. | ||
| Parameters | ||
| ---------- | ||
| sim_id : str | ||
| Simulation ID. | ||
| status : SimulationStatus | ||
| Simulation status. | ||
| message : str, optional | ||
| Status message. | ||
| """ | ||
| with self._study_lock: | ||
| self._study.set_simulation_status(sim_id, status, message) |
| # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| # SPDX-License-Identifier: MIT | ||
| # | ||
| # | ||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| # of this software and associated documentation files (the "Software"), to deal | ||
| # in the Software without restriction, including without limitation the rights | ||
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| # copies of the Software, and to permit persons to whom the Software is | ||
| # furnished to do so, subject to the following conditions: | ||
| # | ||
| # The above copyright notice and this permission notice shall be included in all | ||
| # copies or substantial portions of the Software. | ||
| # | ||
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| # SOFTWARE. | ||
| """Provides a base class for simulation inputs.""" | ||
| from ansys.additive.core import misc | ||
| class SimulationInputBase: | ||
| """Provides a base class for simulation inputs.""" | ||
| def __init__(self) -> None: | ||
| """Initialize the simulation input base class.""" | ||
| self._id: str = misc.short_uuid() | ||
| @property | ||
| def id(self) -> str: | ||
| """Return a unique identifier for this simulation.""" | ||
| return self._id |
| # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| # SPDX-License-Identifier: MIT | ||
| # | ||
| # | ||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| # of this software and associated documentation files (the "Software"), to deal | ||
| # in the Software without restriction, including without limitation the rights | ||
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| # copies of the Software, and to permit persons to whom the Software is | ||
| # furnished to do so, subject to the following conditions: | ||
| # | ||
| # The above copyright notice and this permission notice shall be included in all | ||
| # copies or substantial portions of the Software. | ||
| # | ||
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| # SOFTWARE. | ||
| """Set up methods for grpc simulation requests.""" | ||
| import hashlib | ||
| import os | ||
| from collections.abc import Iterator | ||
| from ansys.additive.core.microstructure import MicrostructureInput | ||
| from ansys.additive.core.microstructure_3d import Microstructure3DInput | ||
| from ansys.additive.core.porosity import PorosityInput | ||
| from ansys.additive.core.progress_handler import ( | ||
| IProgressHandler, | ||
| Progress, | ||
| ProgressState, | ||
| ) | ||
| from ansys.additive.core.server_connection import ServerConnection | ||
| from ansys.additive.core.single_bead import SingleBeadInput | ||
| from ansys.additive.core.thermal_history import ThermalHistoryInput | ||
| from ansys.api.additive.v0.additive_simulation_pb2 import ( | ||
| SimulationRequest, | ||
| UploadFileRequest, | ||
| ) | ||
| def __file_upload_reader(file_name: str, chunk_size=2 * 1024**2) -> Iterator[UploadFileRequest]: | ||
| """Read a file and return an iterator of UploadFileRequests.""" | ||
| file_size = os.path.getsize(file_name) | ||
| short_name = os.path.basename(file_name) | ||
| with open(file_name, mode="rb") as f: | ||
| while True: | ||
| chunk = f.read(chunk_size) | ||
| if not chunk: | ||
| break | ||
| yield UploadFileRequest( | ||
| name=short_name, | ||
| total_size=file_size, | ||
| content=chunk, | ||
| content_md5=hashlib.md5(chunk).hexdigest(), # noqa: S324 | ||
| ) | ||
| def _setup_thermal_history( | ||
| input: ThermalHistoryInput, | ||
| server: ServerConnection, | ||
| progress_handler: IProgressHandler | None = None, | ||
| ) -> SimulationRequest: | ||
| """Initialize a thermal history simulation. | ||
| Parameters | ||
| ---------- | ||
| input: ThermalHistoryInput | ||
| Simulation input parameters. | ||
| server: ServerConnection | ||
| Server to use for the simulation. | ||
| progress_handler: IProgressHandler, None, default: None | ||
| Handler for progress updates. If ``None``, no progress updates are provided. | ||
| Returns | ||
| ------- | ||
| :class:`SimulationRequest` | ||
| """ | ||
| if not input.geometry or not input.geometry.path: | ||
| raise ValueError("The geometry path is not defined in the simulation input") | ||
| remote_geometry_path = "" | ||
| for response in server.simulation_stub.UploadFile(__file_upload_reader(input.geometry.path)): | ||
| remote_geometry_path = response.remote_file_name | ||
| progress = Progress.from_proto_msg(input.id, response.progress) | ||
| if progress_handler: | ||
| progress_handler.update(progress) | ||
| if progress.state == ProgressState.ERROR: | ||
| raise Exception(progress.message) | ||
| return input._to_simulation_request(remote_geometry_path=remote_geometry_path) | ||
| def create_request( | ||
| simulation_input: ( | ||
| SingleBeadInput | ||
| | PorosityInput | ||
| | MicrostructureInput | ||
| | ThermalHistoryInput | ||
| | Microstructure3DInput | ||
| ), | ||
| server: ServerConnection, | ||
| progress_handler: IProgressHandler | None = None, | ||
| ) -> SimulationRequest: | ||
| """Create a simulation request and set up any pre-requisites on a server, such as an STL file for a | ||
| thermal history simulation. | ||
| Parameters | ||
| ---------- | ||
| simulation_input: SingleBeadInput, PorosityInput, MicrostructureInput, ThermalHistoryInput, Microstructure3DInput | ||
| Parameters to use for simulation. | ||
| server: ServerConnection | ||
| Server to use for the simulation. | ||
| progress_handler: IProgressHandler, None, default: None | ||
| Handler for progress updates. If ``None``, no progress updates are provided. | ||
| Returns | ||
| ------- | ||
| A SimulationRequest | ||
| """ # noqa: E501 | ||
| if isinstance(simulation_input, ThermalHistoryInput): | ||
| request = _setup_thermal_history(simulation_input, server, progress_handler) | ||
| else: | ||
| request = simulation_input._to_simulation_request() | ||
| return request |
| # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| # SPDX-License-Identifier: MIT | ||
| # | ||
| # | ||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| # of this software and associated documentation files (the "Software"), to deal | ||
| # in the Software without restriction, including without limitation the rights | ||
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| # copies of the Software, and to permit persons to whom the Software is | ||
| # furnished to do so, subject to the following conditions: | ||
| # | ||
| # The above copyright notice and this permission notice shall be included in all | ||
| # copies or substantial portions of the Software. | ||
| # | ||
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| # SOFTWARE. | ||
| """Manages simulation tasks.""" | ||
| import time | ||
| from ansys.additive.core.logger import LOG | ||
| from ansys.additive.core.progress_handler import IProgressHandler, Progress | ||
| from ansys.additive.core.simulation_task import SimulationTask | ||
| class SimulationTaskManager: | ||
| """Provides a manager for simulation tasks.""" | ||
| def __init__(self): | ||
| """Initialize the simulation task manager.""" | ||
| self._tasks: list[SimulationTask] = [] | ||
| @property | ||
| def tasks(self) -> list[SimulationTask]: | ||
| """Get the list of tasks managed by this manager.""" | ||
| return self._tasks | ||
| @property | ||
| def simulation_ids(self) -> list[str]: | ||
| """Get the list of the simulation ids managed by this manager.""" | ||
| return [x.simulation_id for x in self._tasks] | ||
| @property | ||
| def done(self) -> bool: | ||
| """Check if all tasks are done.""" | ||
| return all(t.done for t in self._tasks) | ||
| def add_task(self, task: SimulationTask): | ||
| """Add a task to this manager. | ||
| Parameters | ||
| ---------- | ||
| task: SimulationTask | ||
| The simulation task holding the long-running operation and corresponding server. | ||
| """ | ||
| self._tasks.append(task) | ||
| def status( | ||
| self, progress_handler: IProgressHandler | None = None | ||
| ) -> list[tuple[str, Progress]]: | ||
| """Get status of each operation stored in this manager. | ||
| Parameters | ||
| ---------- | ||
| progress_handler: IProgressHandler, None, default: None | ||
| Handler for progress updates. If ``None``, no progress updates are provided. | ||
| Returns | ||
| ------- | ||
| List of tuples with each tuple containing the operation name and an instance of Progress | ||
| """ | ||
| status_all = [] | ||
| for t in self._tasks: | ||
| progress = t.status() | ||
| status_all.append((progress.sim_id, progress)) | ||
| if progress_handler: | ||
| progress_handler.update(progress) | ||
| return status_all | ||
| def wait_all(self, progress_handler: IProgressHandler | None = None) -> None: | ||
| """Wait for all simulations to finish. A simple loop that waits for each task will wait for the | ||
| simulation that takes the longest. This works because wait returns immediately if operation is done. | ||
| Parameters | ||
| ---------- | ||
| progress_handler: IProgressHandler, None, default: None | ||
| Handler for progress updates. If ``None``, no progress updates are provided. | ||
| """ | ||
| LOG.debug(f"Waiting for {len(self._tasks)} tasks to complete") | ||
| for t in self._tasks: | ||
| t.wait(progress_handler=progress_handler) | ||
| def cancel_all(self) -> None: | ||
| """Cancel all simulations belonging to this simulation task manager.""" | ||
| LOG.debug("Cancelling all tasks") | ||
| for t in self._tasks: | ||
| t.cancel() | ||
| time.sleep(0.1) | ||
| def summaries(self): | ||
| """Get a list of the summaries of completed simulations only.""" | ||
| return [t.summary for t in self._tasks if t.summary] |
| # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. | ||
| # SPDX-License-Identifier: MIT | ||
| # | ||
| # | ||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| # of this software and associated documentation files (the "Software"), to deal | ||
| # in the Software without restriction, including without limitation the rights | ||
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| # copies of the Software, and to permit persons to whom the Software is | ||
| # furnished to do so, subject to the following conditions: | ||
| # | ||
| # The above copyright notice and this permission notice shall be included in all | ||
| # copies or substantial portions of the Software. | ||
| # | ||
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| # SOFTWARE. | ||
| """Container for a simulation task.""" | ||
| import os | ||
| import zipfile | ||
| from google.longrunning.operations_pb2 import ( | ||
| CancelOperationRequest, | ||
| GetOperationRequest, | ||
| Operation, | ||
| WaitOperationRequest, | ||
| ) | ||
| from google.protobuf.any_pb2 import Any | ||
| from google.protobuf.duration_pb2 import Duration | ||
| from google.rpc.code_pb2 import Code | ||
| from ansys.additive.core.download import download_file | ||
| from ansys.additive.core.logger import LOG | ||
| from ansys.additive.core.material_tuning import ( | ||
| MaterialTuningInput, | ||
| MaterialTuningSummary, | ||
| ) | ||
| from ansys.additive.core.microstructure import ( | ||
| MicrostructureInput, | ||
| MicrostructureSummary, | ||
| ) | ||
| from ansys.additive.core.microstructure_3d import ( | ||
| Microstructure3DInput, | ||
| Microstructure3DSummary, | ||
| ) | ||
| from ansys.additive.core.porosity import PorosityInput, PorositySummary | ||
| from ansys.additive.core.progress_handler import ( | ||
| IProgressHandler, | ||
| Progress, | ||
| ProgressState, | ||
| ) | ||
| from ansys.additive.core.server_connection import ServerConnection | ||
| from ansys.additive.core.simulation import SimulationError | ||
| from ansys.additive.core.single_bead import SingleBeadInput, SingleBeadSummary | ||
| from ansys.additive.core.thermal_history import ( | ||
| ThermalHistoryInput, | ||
| ThermalHistorySummary, | ||
| ) | ||
| from ansys.api.additive.v0.additive_materials_pb2 import TuneMaterialResponse | ||
| from ansys.api.additive.v0.additive_operations_pb2 import OperationMetadata | ||
| from ansys.api.additive.v0.additive_simulation_pb2 import SimulationResponse | ||
| class SimulationTask: | ||
| """Provides a simulation task. | ||
| Parameters | ||
| ---------- | ||
| server_connection: ServerConnection | ||
| The client connection to the Additive server. | ||
| long_running_operation: Operation | ||
| The long-running operation representing the simulation on the server. | ||
| simulation_input: SingleBeadInput | PorosityInput | MicrostructureInput | ThermalHistoryInput | Microstructure3DInput | MaterialTuningInput | ||
| The simulation input. | ||
| user_data_path: str | ||
| The path to the user data directory. | ||
| """ # noqa: E501 | ||
| def __init__( | ||
| self, | ||
| server_connection: ServerConnection, | ||
| long_running_operation: Operation, | ||
| simulation_input: ( | ||
| SingleBeadInput | ||
| | PorosityInput | ||
| | MicrostructureInput | ||
| | ThermalHistoryInput | ||
| | Microstructure3DInput | ||
| | MaterialTuningInput | ||
| ), | ||
| user_data_path: str, | ||
| ): | ||
| """Initialize the simulation task.""" | ||
| self._server = server_connection | ||
| self._user_data_path = user_data_path | ||
| self._long_running_op = long_running_operation | ||
| self._simulation_input = simulation_input | ||
| self._summary = None | ||
| @property | ||
| def simulation_id(self) -> str: | ||
| """Get the simulation id associated with this task.""" | ||
| return self._simulation_input.id | ||
| @property | ||
| def summary( | ||
| self, | ||
| ) -> ( | ||
| SingleBeadSummary | ||
| | PorositySummary | ||
| | MicrostructureSummary | ||
| | Microstructure3DSummary | ||
| | ThermalHistorySummary | ||
| | MaterialTuningSummary | ||
| | SimulationError | ||
| | None | ||
| ): | ||
| """The summary of the completed simulation. | ||
| None if simulation is not completed. | ||
| """ | ||
| return self._summary | ||
| def status(self) -> Progress: | ||
| """Fetch status from the server to update progress and results. | ||
| Parameters | ||
| ---------- | ||
| progress_handler: IProgressHandler, None, default: None | ||
| Handler for progress updates. If ``None``, no progress updates are provided. | ||
| Return | ||
| ------ | ||
| Progress | ||
| The progress of the operation. | ||
| """ | ||
| get_request = GetOperationRequest(name=self._long_running_op.name) | ||
| self._long_running_op = self._server.operations_stub.GetOperation(get_request) | ||
| progress = self._update_operation_status(self._long_running_op) | ||
| return progress | ||
| def wait( | ||
| self, | ||
| *, | ||
| progress_update_interval: int = 5, | ||
| progress_handler: IProgressHandler | None = None, | ||
| ) -> None: | ||
| """Wait for simulation to finish while updating progress. | ||
| Parameters | ||
| ---------- | ||
| progress_update_interval: int, default: 5 | ||
| A timeout value (in seconds) to give to the looped WaitOperation() calls to return an | ||
| updated message for a progress update. | ||
| progress_handler: IProgressHandler, None, default: None | ||
| Handler for progress updates. If ``None``, no progress updates are provided. | ||
| """ | ||
| LOG.debug(f"Waiting for {self._long_running_op.name} to complete") | ||
| try: | ||
| while True: | ||
| timeout = Duration(seconds=progress_update_interval) | ||
| wait_request = WaitOperationRequest( | ||
| name=self._long_running_op.name, timeout=timeout | ||
| ) | ||
| awaited_operation = self._server.operations_stub.WaitOperation(wait_request) | ||
| progress = self._update_operation_status(awaited_operation) | ||
| if progress_handler: | ||
| progress_handler.update(progress) | ||
| if awaited_operation.done: | ||
| break | ||
| except Exception as e: | ||
| LOG.error(f"Error while awaiting operation: {e}") | ||
| # Perform a call to status to ensure all messages are received and summary is updated | ||
| progress = self.status() | ||
| if progress_handler: | ||
| progress_handler.update(progress) | ||
| def cancel(self) -> None: | ||
| """Cancel a running simulation.""" | ||
| LOG.debug(f"Cancelling {self._long_running_op.name}") | ||
| request = CancelOperationRequest(name=self._long_running_op.name) | ||
| self._server.operations_stub.CancelOperation(request) | ||
| @property | ||
| def done(self) -> bool: | ||
| """Check if the task is completed.""" | ||
| return self._long_running_op.done | ||
| @staticmethod | ||
| def _convert_metadata_to_progress( | ||
| metadata_message: Any, | ||
| ) -> Progress: | ||
| """Update the progress handler with a metadata message. | ||
| Parameters | ||
| ---------- | ||
| metadata_message: [google.protobuf.Any] | ||
| The metadata field of the Operation protobuf message prior to unpacking | ||
| to an OperationMetadata class. | ||
| Returns | ||
| ------- | ||
| An instance of a Progress class | ||
| """ | ||
| metadata = OperationMetadata() | ||
| metadata_message.Unpack(metadata) | ||
| return Progress.from_operation_metadata(metadata) | ||
| def _unpack_summary( | ||
| self, | ||
| operation: Operation, | ||
| ) -> Progress: | ||
| """Update the simulation summaries. | ||
| If an operation is completed, either the "error" field or | ||
| the "response" field is available. Update the progress according to which | ||
| field is available. | ||
| Parameters | ||
| ---------- | ||
| operation: [google.longrunning.Operation] | ||
| The long-running operation. | ||
| Return | ||
| ------ | ||
| Progress | ||
| The progress of the operation. | ||
| """ | ||
| progress = self._convert_metadata_to_progress(operation.metadata) | ||
| if progress.state == ProgressState.ERROR: | ||
| self._summary = SimulationError(self._simulation_input, progress.message) | ||
| return progress | ||
| if operation.HasField("response"): | ||
| response = ( | ||
| SimulationResponse() | ||
| if not isinstance(self._simulation_input, MaterialTuningInput) | ||
| else TuneMaterialResponse() | ||
| ) | ||
| operation.response.Unpack(response) | ||
| self._summary = self._create_summary_from_response(response) | ||
| elif operation.HasField("error") and operation.error.code not in [ | ||
| Code.CANCELLED, | ||
| Code.OK, | ||
| ]: | ||
| self._summary = SimulationError(self._simulation_input, operation.error.message) | ||
| return progress | ||
| def _update_operation_status( | ||
| self, | ||
| operation: Operation, | ||
| ) -> Progress: | ||
| """Update progress or summary. | ||
| If operation is done, update summary and progress. | ||
| Otherwise, update progress only. | ||
| Parameters | ||
| ---------- | ||
| operation: [google.longrunning.Operation] | ||
| The long-running operation. | ||
| Returns | ||
| ------- | ||
| Progress | ||
| Progress created from long-running operation metadata. | ||
| """ | ||
| if operation.done: | ||
| # If operation is completed, get the summary and update progress. The server | ||
| # should always mark the operation done on either a successful completion or | ||
| # a simulation error. | ||
| progress = self._unpack_summary(operation) | ||
| else: | ||
| # Otherwise, just update progress | ||
| progress = self._convert_metadata_to_progress(operation.metadata) | ||
| return progress | ||
| def _create_summary_from_response( | ||
| self, response: SimulationResponse | TuneMaterialResponse | ||
| ) -> ( | ||
| SingleBeadSummary | ||
| | PorositySummary | ||
| | MicrostructureSummary | ||
| | ThermalHistorySummary | ||
| | Microstructure3DSummary | ||
| | MaterialTuningSummary | ||
| ): | ||
| if isinstance(response, TuneMaterialResponse): | ||
| return MaterialTuningSummary( | ||
| self._simulation_input, response.result, self._user_data_path | ||
| ) | ||
| if response.HasField("melt_pool"): | ||
| thermal_history_output = None | ||
| if self._check_if_thermal_history_is_present(response): | ||
| thermal_history_output = os.path.join( | ||
| self._user_data_path, self._simulation_input.id, "thermal_history" | ||
| ) | ||
| download_file( | ||
| self._server.simulation_stub, | ||
| response.melt_pool.thermal_history_vtk_zip, | ||
| thermal_history_output, | ||
| ) | ||
| return SingleBeadSummary( | ||
| self._simulation_input, response.melt_pool, thermal_history_output | ||
| ) | ||
| if response.HasField("porosity_result"): | ||
| return PorositySummary(self._simulation_input, response.porosity_result) | ||
| if response.HasField("microstructure_result"): | ||
| return MicrostructureSummary( | ||
| self._simulation_input, | ||
| response.microstructure_result, | ||
| self._user_data_path, | ||
| ) | ||
| if response.HasField("microstructure_3d_result"): | ||
| return Microstructure3DSummary( | ||
| self._simulation_input, | ||
| response.microstructure_3d_result, | ||
| self._user_data_path, | ||
| ) | ||
| if response.HasField("thermal_history_result"): | ||
| path = os.path.join(self._user_data_path, self._simulation_input.id, "coax_ave_output") | ||
| local_zip = download_file( | ||
| self._server.simulation_stub, | ||
| response.thermal_history_result.coax_ave_zip_file, | ||
| path, | ||
| ) | ||
| with zipfile.ZipFile(local_zip, "r") as zip: | ||
| zip.extractall(path) | ||
| os.remove(local_zip) | ||
| return ThermalHistorySummary(self._simulation_input, path) | ||
| def _check_if_thermal_history_is_present(self, response) -> bool: | ||
| """Check if thermal history output is present in the response.""" | ||
| return response.melt_pool.thermal_history_vtk_zip != str() |
Sorry, the diff of this file is too big to display
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
360188
-11.51%34
-10.53%7231
-12.1%