Skip to content

Commit

Permalink
Merge branch 'main' into dependabot/github_actions/ansys/actions-7
Browse files Browse the repository at this point in the history
  • Loading branch information
yias authored Sep 12, 2024
2 parents 0764fcf + 069a602 commit e72f515
Show file tree
Hide file tree
Showing 13 changed files with 183 additions and 21 deletions.
16 changes: 16 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
Changelog
---------

0.2.3 (2024-08-21)
******************

New:

- Added :py:class:`PostProcessInput<ansys.simai.core.data.model_configuration.PostProcessInput>` class to define post processing input fields.
- Added support for NaN and Inf for Global Coefficients and Post Processings.

Fixes:

- Removed compute argument from :py:meth:`TrainingData.upload_folder()<ansys.simai.core.data.training_data.TrainingData.upload_folder>`
- Fixed Model Configuration to raise a ProcessingError when volume field is missing from a sample specifying volume output.
- Removed wakepy error mode success (deprecated) during optimization.
- Renamed TrainingData method compute() to :py:meth:`TrainingData.extract_data()<ansys.simai.core.data.training_data.TrainingData.extract_data>`.
- Updated documentation of :py:meth:`GeometryDirectory.upload()<ansys.simai.core.data.geometries.GeometryDirectory.upload>`: the ``workspace_id`` argument was moved to ``workspace`` but never updated.

0.2.2 (2024-07-17)
******************

Expand Down
5 changes: 5 additions & 0 deletions doc/source/api_reference/model_configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,8 @@ ModelConfiguration
.. autoclass:: ModelConfiguration()
:members:

PostProcessInput
-----------------

.. autoclass:: PostProcessInput()
:members:
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ build-backend = "flit_core.buildapi"

[project]
name = "ansys-simai-core"
version = "0.2.2"
version = "0.2.3"
description = "A python wrapper for Ansys SimAI"
authors = [
{name = "ANSYS, Inc.", email = "[email protected]"},
Expand Down
9 changes: 5 additions & 4 deletions src/ansys/simai/core/data/geometries.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ def upload( # noqa: D417
For more information, see the :class:`~ansys.simai.core.data.types.NamedFile` class.
metadata: Optional metadata to add to the geometry's simple key-value store.
Lists and nested objects are not supported.
workspace_id: ID or :class:`model <.workspaces.Workspace>` of the workspace to
workspace: ID or :class:`model <.workspaces.Workspace>` of the workspace to
upload the geometry to. This parameter is only necessary if no workspace
is set for the client.
monitor_callback: Optional callback for monitoring the progress of the download.
Expand All @@ -501,15 +501,16 @@ def upload( # noqa: D417
Returns:
Created :py:class:`Geometry` object.
"""
workspace_id = get_id_from_identifiable(workspace, default=self._client._current_workspace)
workspace = self._client.workspaces.get(workspace_id)
workspace = get_object_from_identifiable(
workspace, self._client.workspaces, default=self._client._current_workspace
)
with unpack_named_file(file) as (readable_file, name, extension):
if extension not in workspace.model_manifest.geometry["accepted_file_formats"]:
raise InvalidArguments(
f"Got a file with {extension} extension but expected one of : {workspace.model_manifest.geometry['accepted_file_formats']}"
)
(geometry_fields, upload_id) = self._client._api.create_geometry(
workspace_id, name, extension, metadata
workspace.id, name, extension, metadata
)
geometry = self._model_from(geometry_fields, is_upload_complete=False)
parts = self._client._api.upload_parts(
Expand Down
3 changes: 2 additions & 1 deletion src/ansys/simai/core/data/global_coefficients_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
ComputableDataModel,
Directory,
)
from ansys.simai.core.utils.numerical import cast_values_to_float

if TYPE_CHECKING:
import ansys.simai.core.client
Expand Down Expand Up @@ -179,7 +180,7 @@ def _handle_job_sse_event(self, data):
self._set_is_pending()
elif state == "successful":
logger.debug(f"{self._classname} id {self.id} set status successful")
self._result = {k: float(v) for k, v in data.get("result", {}).get("value", {}).items()}
self._result = cast_values_to_float(data.get("result", {}).get("value"))
self._set_is_over()
elif state in ERROR_STATES:
error_message = f"Computation of global coefficient {target.get('formula')} failed with {data.get('reason', 'UNKNOWN ERROR')}"
Expand Down
35 changes: 28 additions & 7 deletions src/ansys/simai/core/data/model_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,17 @@ class ModelOutput:
volume: list[str] = None


@dataclass
class PostProcessInput:
"""Designates the variables to use as post-processing input.
Args:
surface: the post-processing input surface variables.
"""

surface: list[str] = None


@dataclass
class ModelConfiguration:
"""Configures the build of a model.
Expand All @@ -209,6 +220,7 @@ class ModelConfiguration:
output: the outputs of the model.
global_coefficients: the Global Coefficients of the model.
domain_of_analysis: The Domain of Analysis of the model configuration.
pp_input: The post-processing input (e.g. a surface variable).
Example:
Define a new configuration and launch a build.
Expand All @@ -222,6 +234,7 @@ class ModelConfiguration:
ModelConfiguration,
ModelInput,
ModelOutput,
PostProcessInput,
)
simai = asc.from_config()
Expand All @@ -237,6 +250,9 @@ class ModelConfiguration:
surface=["Pressure", "WallShearStress_0"], volume=["Velocity_0", "Pressure"]
)
# Define the surface post-processing input
pp_input = PostProcessInput(surface=["Temperature_1"])
# Define the model coefficients
global_coefficients = [("max(Pressure)", "maxpress")]
Expand All @@ -256,6 +272,7 @@ class ModelConfiguration:
output=model_output,
global_coefficients=global_coefficients,
domain_of_analysis=doa,
pp_input=pp_input,
)
# Launch a mode build with the new configuration
Expand All @@ -273,6 +290,7 @@ class ModelConfiguration:
input: ModelInput = field(default_factory=lambda: ModelInput())
output: ModelOutput = field(default_factory=lambda: ModelOutput())
domain_of_analysis: DomainOfAnalysis = field(default_factory=lambda: DomainOfAnalysis())
pp_input: PostProcessInput = field(default_factory=lambda: PostProcessInput())

def __set_gc(self, gcs: list[GlobalCoefficientDefinition]):
verified_gcs = []
Expand Down Expand Up @@ -307,23 +325,23 @@ def __init__(
input: Optional[ModelInput] = None,
output: Optional[ModelOutput] = None,
domain_of_analysis: Optional[DomainOfAnalysis] = None,
surface_pp_input: Optional[list] = None,
pp_input: Optional[PostProcessInput] = None,
):
"""Sets the properties of a build configuration."""
if surface_pp_input is None:
surface_pp_input = []
self.project = project
self.input = ModelInput()
if input is not None:
self.input = input
self.output = ModelOutput()
if output is not None:
self.output = output
self.pp_input = PostProcessInput()
if pp_input is not None:
self.pp_input = pp_input
if boundary_conditions is not None and self.input.boundary_conditions is None:
self.input.boundary_conditions = list(boundary_conditions.keys())
self.build_preset = build_preset
self.continuous = continuous
self.surface_pp_input = surface_pp_input
if fields is not None:
if fields.get("surface_input"):
self.input.surface = [fd.get("name") for fd in fields["surface_input"]]
Expand All @@ -334,7 +352,8 @@ def __init__(
if fields.get("volume"):
self.output.volume = [fd.get("name") for fd in fields["volume"]]

self.surface_pp_input = fields.get("surface_pp_input", [])
if fields.get("surface_pp_input"):
self.pp_input.surface = [fd.get("name") for fd in fields["surface_pp_input"]]

self.domain_of_analysis = domain_of_analysis
if simulation_volume is not None:
Expand Down Expand Up @@ -365,7 +384,7 @@ def _set_doa_axis(self, fld: DomainAxisDefinition, param: str) -> dict[str, Any]
return {"length": fld.length, "type": fld.position, "value": fld.value}

def _to_payload(self):
"""Constracts the payload for a build request."""
"""Constructs the payload for a build request."""

bcs = {}
if self.input.boundary_conditions is not None:
Expand Down Expand Up @@ -405,11 +424,13 @@ def _to_payload(self):
if self.global_coefficients is not None:
gcs = [asdict(gc) for gc in self.global_coefficients]

surface_pp_input_fld = self.pp_input.surface or []

flds = {
"surface": surface_fld,
"surface_input": surface_input_fld,
"volume": volume_fld,
"surface_pp_input": self.surface_pp_input if self.surface_pp_input else [],
"surface_pp_input": surface_pp_input_fld,
}

simulation_volume = {
Expand Down
5 changes: 3 additions & 2 deletions src/ansys/simai/core/data/post_processings.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
InvalidServerStateError,
)
from ansys.simai.core.utils.numerical import (
cast_values_to_float,
convert_axis_and_coordinate_to_plane_eq_coeffs,
)

Expand Down Expand Up @@ -186,7 +187,7 @@ def data(self) -> Dict[str, List]:
self.wait()

results = self._get_results()
return {k: float(v) for k, v in results["data"]["values"].items()}
return cast_values_to_float(results["data"]["values"])


class SurfaceEvol(ExportablePostProcessing):
Expand Down Expand Up @@ -595,7 +596,7 @@ def _get_or_run(
If ``False``, this method only gets an existing postprocessing.
This is a non-blocking method. It runs (if not already run orrunning) the postprocessing
of given type with the given parameters. If ``run=False``, if a preprocessing already
of given type with the given parameters. If ``run=False``, if a postprocessing already
exits, it gets it.
"""
# FIXME frozenset(params.items()) works as long as there are no
Expand Down
9 changes: 5 additions & 4 deletions src/ansys/simai/core/data/training_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,13 @@ def extracted_metadata(self) -> Optional[Dict]:
"""Metadata extracted from the training data."""
return self.fields["extracted_metadata"]

def compute(self) -> None:
"""Compute or recompute the training data.
def extract_data(self) -> None:
"""Extract or reextract the data from a training data.
Training data should be computed once all its parts have been fully uploaded.
Data should be extracted from a training data once all its parts have been fully uploaded.
This is done automatically when using :meth:`TrainingDataDirectory.upload_folder` to create training data.
Training data can only be recomputed if computation previously failed or if new data has been added.
Data can only be reextracted from a training data if the extraction previously failed or if new files have been added.
"""
self._client._api.compute_training_data(self.id)

Expand Down
4 changes: 3 additions & 1 deletion src/ansys/simai/core/data/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@
"""


APIResponse = Union[Response, Dict[str, Any], List[Dict[str, Any]]]
JSON = Union[Dict[str, Any], List[Dict[str, Any]]]
APIResponse = Union[Response, JSON]


MonitorCallback = Callable[[int], None]
"""Callback used to monitor the download or upload of a file.
Expand Down
17 changes: 17 additions & 0 deletions src/ansys/simai/core/utils/numerical.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,17 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import logging
import math
from numbers import Number
from typing import Any, Optional, Tuple

from ansys.simai.core.errors import SimAIError

DEFAULT_COMPARISON_EPSILON = 10**-6

logger = logging.getLogger(__name__)


def is_number(value: Any):
return isinstance(value, Number) and not math.isnan(value)
Expand Down Expand Up @@ -113,3 +118,15 @@ def validate_tolerance_parameter(tolerance: Optional[Number]):
raise TypeError(f"The tolerance argument must be a number >= 0 (passed {tolerance})")
if tolerance < 0:
raise ValueError(f"The tolerance argument must be a number >= 0 (passed {tolerance})")


# Recursively go through dict to cast values to float, so that we support NaN and inf
# Only use if all the values are expected to be numbers
def cast_values_to_float(data: Any):
if isinstance(data, dict):
return {k: cast_values_to_float(v) for k, v in data.items()}
else:
try:
return float(data)
except ValueError as err:
raise SimAIError(f"received invalid float value: {data!r}") from err
18 changes: 17 additions & 1 deletion src/ansys/simai/core/utils/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@

import logging
from json.decoder import JSONDecodeError
from typing import Literal, overload

import requests

from ansys.simai.core.data.types import APIResponse
from ansys.simai.core.data.types import JSON, APIResponse
from ansys.simai.core.errors import ApiClientError, NotFoundError

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -76,6 +77,21 @@ def handle_http_errors(response: requests.Response) -> None:
raise ApiClientError(f"{response.status_code}: {response.reason}", response) from e


@overload
def handle_response(response: requests.Response, return_json: Literal[True]) -> JSON:
...


@overload
def handle_response(response: requests.Response, return_json: Literal[False]) -> requests.Response:
...


@overload
def handle_response(response: requests.Response, return_json: bool) -> APIResponse:
...


def handle_response(response: requests.Response, return_json: bool = True) -> APIResponse:
"""Handle HTTP errors and return the relevant data from the response.
Expand Down
Loading

0 comments on commit e72f515

Please sign in to comment.