From 5b59ff62a57f752be6d50291e2f68bcc11d87918 Mon Sep 17 00:00:00 2001 From: Paul Walters Date: Fri, 25 Jul 2025 13:35:05 -0400 Subject: [PATCH 1/4] Added import thermal signal API --- src/ansys/sherlock/core/lifecycle.py | 56 ++++++ .../sherlock/core/types/lifecycle_types.py | 110 ++++++++++++ tests/test_lifecycle.py | 161 +++++++++++++++++- 3 files changed, 324 insertions(+), 3 deletions(-) create mode 100644 src/ansys/sherlock/core/types/lifecycle_types.py diff --git a/src/ansys/sherlock/core/lifecycle.py b/src/ansys/sherlock/core/lifecycle.py index 7add08f91..0e351a569 100644 --- a/src/ansys/sherlock/core/lifecycle.py +++ b/src/ansys/sherlock/core/lifecycle.py @@ -2,9 +2,11 @@ """Module containing all life cycle management capabilities.""" try: + import SherlockCommonService_pb2 import SherlockLifeCycleService_pb2 import SherlockLifeCycleService_pb2_grpc except ModuleNotFoundError: + from ansys.api.sherlock.v0 import SherlockCommonService_pb2 from ansys.api.sherlock.v0 import SherlockLifeCycleService_pb2 from ansys.api.sherlock.v0 import SherlockLifeCycleService_pb2_grpc @@ -35,6 +37,7 @@ SherlockNoGrpcConnectionException, ) from ansys.sherlock.core.grpc_stub import GrpcStub +from ansys.sherlock.core.types.lifecycle_types import ImportThermalSignalRequest from ansys.sherlock.core.utils.version_check import require_version @@ -2107,3 +2110,56 @@ def load_shock_profile_pulses( for error in e.str_itr(): LOG.error(error) raise e + + @require_version(261) + def import_thermal_signal( + self, request: ImportThermalSignalRequest + ) -> SherlockCommonService_pb2.ReturnCode: + """Import a thermal signal to a life cycle phase. + + Available Since: 2026R1 + + Parameters + ---------- + request: ImportThermalSignalRequest + Request object containing the information needed to import a thermal signal. + + Returns + ------- + SherlockCommonService_pb2.ReturnCode + Status code of the response. 0 for success. + + Examples + -------- + >>> from ansys.sherlock.core.types.lifecycle_types import ImportThermalSignalRequest + >>> from ansys.sherlock.core.types.lifecycle_types import ThermalSignalFileProperties + >>> from ansys.sherlock.core.launcher import launch_sherlock + >>> sherlock = launch_sherlock() + >>> response = sherlock.lifecycle.import_thermal_signal( + >>> ImportThermalSignalRequest( + >>> file_name="/path/to/thermal_signal_file.csv", + >>> project="TestProject", + >>> thermal_signal_file_properties=ThermalSignalFileProperties( + >>> header_row_count=0, + >>> numeric_format="English", + >>> column_delimiter=",", + >>> time_column="Time", + >>> time_units="sec", + >>> temperature_column="Temperature", + >>> temperature_units="C" + >>> ), + >>> phase_name=phaseName, + >>> time_removal= False, + >>> load_range_percentage=0.25, + >>> number_of_bins=0, + >>> filtering_limit=0.0, + >>> generated_cycles_label="Second Generated Cycles from Python", + >>> ) + >>> ) + """ + import_thermal_signal_request = request._convert_to_grpc() + + if not self._is_connection_up(): + raise SherlockNoGrpcConnectionException() + + return self.stub.importThermalSignal(import_thermal_signal_request) diff --git a/src/ansys/sherlock/core/types/lifecycle_types.py b/src/ansys/sherlock/core/types/lifecycle_types.py new file mode 100644 index 000000000..fd65f4f1d --- /dev/null +++ b/src/ansys/sherlock/core/types/lifecycle_types.py @@ -0,0 +1,110 @@ +# Copyright (C) 2021 - 2025 ANSYS, Inc. and/or its affiliates. + +"""Module containing types for the Lifecycle Service.""" + +from pydantic import BaseModel, ValidationInfo, field_validator + +from ansys.sherlock.core.types.common_types import basic_str_validator + +try: + import SherlockLifeCycleService_pb2 +except ModuleNotFoundError: + from ansys.api.sherlock.v0 import SherlockLifeCycleService_pb2 + + +class ThermalSignalFileProperties(BaseModel): + """Properties of a thermal signal file.""" + + header_row_count: int + """Number of rows before the column header in the file.""" + numeric_format: str + """Numeric format for the values.""" + column_delimiter: str + """Delimiter used to separate columns in the file.""" + time_column: str + """Name of the column containing time values.""" + time_units: str + """Units of the time values.""" + temperature_column: str + """Name of the column containing temperature values.""" + temperature_units: str + """Units of the temperature values.""" + + def _convert_to_grpc( + self, + ) -> SherlockLifeCycleService_pb2.ImportThermalSignalRequest.ThermalSignalFileProperties: + """Convert to gRPC ThermalSignalFileProperties.""" + return SherlockLifeCycleService_pb2.ImportThermalSignalRequest.ThermalSignalFileProperties( + headerRowCount=self.header_row_count, + numericFormat=self.numeric_format, + columnDelimiter=self.column_delimiter, + timeColumn=self.time_column, + timeUnits=self.time_units, + temperatureColumn=self.temperature_column, + temperatureUnits=self.temperature_units, + ) + + @field_validator("header_row_count") + @classmethod + def non_negative_int_validation(cls, value: int, info: ValidationInfo): + """Validate integer fields listed contain non-negative values.""" + if value < 0: + raise ValueError(f"{info.field_name} must be greater than or equal to 0.") + return value + + @field_validator("time_column", "time_units", "temperature_column", "temperature_units") + @classmethod + def str_validation(cls, value: str, info: ValidationInfo): + """Validate string fields listed.""" + return basic_str_validator(value, info.field_name) + + +class ImportThermalSignalRequest(BaseModel): + """Request to import a thermal signal file.""" + + file_name: str + """The full path to the CSV thermal signal file to be imported.""" + project: str + """Sherlock project name in which the thermal signal is imported.""" + phase_name: str + """Name of the phase in which the thermal signal is imported.""" + thermal_signal_file_properties: ThermalSignalFileProperties + """Properties of the thermal signal file.""" + time_removal: bool + """Option to indicate that time results with shorter half-cycle durations are removed.""" + load_range_percentage: float + """Defines the fraction of the range near peaks and valleys considered as a dwell region""" + number_of_bins: int + """Number of bins for binning cycles, 0 for no binning""" + filtering_limit: float + """Minimum cycle range to include in results, 0 for not filtering""" + generated_cycles_label: str + """Label used to define the name of all generated thermal events.""" + + @field_validator("file_name", "project", "phase_name", "generated_cycles_label") + @classmethod + def str_validation(cls, value: str, info: ValidationInfo): + """Validate string fields listed.""" + return basic_str_validator(value, info.field_name) + + @field_validator("number_of_bins") + @classmethod + def non_negative_int_validation(cls, value: int, info: ValidationInfo): + """Validate integer fields listed contain non-negative values.""" + if value < 0: + raise ValueError(f"{info.field_name} must be greater than or equal to 0.") + return value + + def _convert_to_grpc(self) -> SherlockLifeCycleService_pb2.ImportThermalSignalRequest: + """Convert to gRPC ImportThermalSignalRequest.""" + return SherlockLifeCycleService_pb2.ImportThermalSignalRequest( + thermalSignalFile=self.file_name, + project=self.project, + phaseName=self.phase_name, + fileProperties=self.thermal_signal_file_properties._convert_to_grpc(), + timeRemoval=self.time_removal, + loadRangePercentage=self.load_range_percentage, + numberOfBins=self.number_of_bins, + filteringLimit=self.filtering_limit, + generatedCyclesLabel=self.generated_cycles_label, + ) diff --git a/tests/test_lifecycle.py b/tests/test_lifecycle.py index de714dbf3..fd8493edf 100644 --- a/tests/test_lifecycle.py +++ b/tests/test_lifecycle.py @@ -1,9 +1,9 @@ # Copyright (C) 2021 - 2025 ANSYS, Inc. and/or its affiliates. -from typing import cast import uuid import grpc +import pydantic import pytest from ansys.sherlock.core.errors import ( @@ -23,6 +23,10 @@ SherlockLoadThermalProfileError, ) from ansys.sherlock.core.lifecycle import Lifecycle +from ansys.sherlock.core.types.lifecycle_types import ( + ImportThermalSignalRequest, + ThermalSignalFileProperties, +) from ansys.sherlock.core.utils.version_check import SKIP_VERSION_CHECK @@ -46,6 +50,7 @@ def test_all(): helper_test_load_harmonic_profile(lifecycle) helper_test_load_shock_profile_dataset(lifecycle) helper_test_load_shock_profile_pulses(lifecycle) + helper_test_import_thermal_signal(lifecycle) def helper_test_create_life_phase(lifecycle: Lifecycle): @@ -2047,8 +2052,158 @@ def helper_test_load_shock_profile_pulses(lifecycle: Lifecycle): pytest.fail("No exception raised when using an invalid parameter") except Exception as e: assert type(e) == SherlockLoadShockProfilePulsesError - load_error = cast(SherlockLoadShockProfilePulsesError, e) - assert len(load_error.error_array) == 1 + + +def helper_test_import_thermal_signal(lifecycle: Lifecycle): + try: + lifecycle.import_thermal_signal( + ImportThermalSignalRequest( + file_name="", + project="Tutorial Project", + thermal_signal_file_properties=ThermalSignalFileProperties( + header_row_count=0, + numeric_format="English", + column_delimiter=",", + time_column="Time", + time_units="sec", + temperature_column="Temperature", + temperature_units="C", + ), + phase_name="Environmental", + time_removal=False, + load_range_percentage=0.25, + number_of_bins=0, + filtering_limit=0.0, + generated_cycles_label="Generated Cycles from pySherlock", + ) + ) + pytest.fail("No exception raised when using a missing file_name parameter") + except Exception as e: + assert isinstance(e, pydantic.ValidationError) + assert ( + str(e.errors()[0]["msg"]) + == "Value error, file_name is invalid because it is None or empty." + ) + + try: + lifecycle.import_thermal_signal( + ImportThermalSignalRequest( + file_name="C:/Temp/ThermalSignalMissing.csv", + project="", + thermal_signal_file_properties=ThermalSignalFileProperties( + header_row_count=0, + numeric_format="English", + column_delimiter=",", + time_column="Time", + time_units="sec", + temperature_column="Temperature", + temperature_units="C", + ), + phase_name="Environmental", + time_removal=False, + load_range_percentage=0.25, + number_of_bins=0, + filtering_limit=0.0, + generated_cycles_label="Generated Cycles from pySherlock", + ) + ) + pytest.fail("No exception raised when using a missing project parameter") + except Exception as e: + assert isinstance(e, pydantic.ValidationError) + assert ( + str(e.errors()[0]["msg"]) + == "Value error, project is invalid because it is None or empty." + ) + + try: + lifecycle.import_thermal_signal( + ImportThermalSignalRequest( + file_name="C:/Temp/ThermalSignalMissing.csv", + project="Tutorial Project", + thermal_signal_file_properties=ThermalSignalFileProperties( + header_row_count=0, + numeric_format="English", + column_delimiter=",", + time_column="Time", + time_units="sec", + temperature_column="Temperature", + temperature_units="C", + ), + phase_name="", + time_removal=False, + load_range_percentage=0.25, + number_of_bins=0, + filtering_limit=0.0, + generated_cycles_label="Generated Cycles from pySherlock", + ) + ) + pytest.fail("No exception raised when using a missing phase_name parameter") + except Exception as e: + assert isinstance(e, pydantic.ValidationError) + assert ( + str(e.errors()[0]["msg"]) + == "Value error, phase_name is invalid because it is None or empty." + ) + + try: + lifecycle.import_thermal_signal( + ImportThermalSignalRequest( + file_name="C:/Temp/ThermalSignalMissing.csv", + project="Tutorial Project", + thermal_signal_file_properties=ThermalSignalFileProperties( + header_row_count=0, + numeric_format="English", + column_delimiter=",", + time_column="Time", + time_units="sec", + temperature_column="Temperature", + temperature_units="C", + ), + phase_name="Environmental", + time_removal=False, + load_range_percentage=0.25, + number_of_bins=-1, + filtering_limit=0.0, + generated_cycles_label="Generated Cycles from pySherlock", + ) + ) + pytest.fail("No exception raised when using a missing generated_cycles_label parameter") + except Exception as e: + assert isinstance(e, pydantic.ValidationError) + assert ( + str(e.errors()[0]["msg"]) + == "Value error, number_of_bins must be greater than or equal to 0." + ) + + try: + lifecycle.import_thermal_signal( + ImportThermalSignalRequest( + file_name="C:/Temp/ThermalSignalMissing.csv", + project="Tutorial Project", + thermal_signal_file_properties=ThermalSignalFileProperties( + header_row_count=0, + numeric_format="English", + column_delimiter=",", + time_column="Time", + time_units="sec", + temperature_column="Temperature", + temperature_units="C", + ), + phase_name="Environmental", + time_removal=False, + load_range_percentage=0.25, + number_of_bins=0, + filtering_limit=0.0, + generated_cycles_label="", + ) + ) + pytest.fail("No exception raised when using a missing generated_cycles_label parameter") + except Exception as e: + assert isinstance(e, pydantic.ValidationError) + assert ( + str(e.errors()[0]["msg"]) + == "Value error, generated_cycles_label is invalid because it is None or empty." + ) if __name__ == "__main__": From f83bc2c86a3e49cd473301e8f4c2dfaa6193138b Mon Sep 17 00:00:00 2001 From: Paul Walters Date: Fri, 25 Jul 2025 13:55:46 -0400 Subject: [PATCH 2/4] Added import thermal signal API --- doc/source/api/index.rst | 2 ++ doc/source/api/lifecycle_types.rst | 13 +++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 doc/source/api/lifecycle_types.rst diff --git a/doc/source/api/index.rst b/doc/source/api/index.rst index 4a543c9bc..1903505af 100644 --- a/doc/source/api/index.rst +++ b/doc/source/api/index.rst @@ -18,6 +18,7 @@ Use the search feature or click links to view API documentation. layer layer_types lifecycle + lifecycle_types model parts parts_types @@ -35,6 +36,7 @@ Use the search feature or click links to view API documentation. ansys.sherlock.core.layer ansys.sherlock.core.types.layer_types ansys.sherlock.core.lifecycle + ansys.sherlock.core.types.lifecycle_types ansys.sherlock.core.model ansys.sherlock.core.parts ansys.sherlock.core.types.parts_types diff --git a/doc/source/api/lifecycle_types.rst b/doc/source/api/lifecycle_types.rst new file mode 100644 index 000000000..f8861ebc4 --- /dev/null +++ b/doc/source/api/lifecycle_types.rst @@ -0,0 +1,13 @@ +.. _ref_common_types: + +LifeCycle Types +=============== +.. automodule:: ansys.sherlock.core.types.lifecycle_types +.. currentmodule:: ansys.sherlock.core.types.lifecycle_types + +Constants +--------- +.. autoclass:: ThermalSignalFileProperties + :members: +.. autoclass:: ImportThermalSignalRequest + :members: From 73be3bc5a93744a0b7a7e1a7dea9b7c779ca7feb Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Fri, 25 Jul 2025 17:57:06 +0000 Subject: [PATCH 3/4] chore: adding changelog file 612.miscellaneous.md [dependabot-skip] --- doc/changelog.d/612.miscellaneous.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/612.miscellaneous.md diff --git a/doc/changelog.d/612.miscellaneous.md b/doc/changelog.d/612.miscellaneous.md new file mode 100644 index 000000000..f025ec98f --- /dev/null +++ b/doc/changelog.d/612.miscellaneous.md @@ -0,0 +1 @@ +Feat: Added import thermal signal API \ No newline at end of file From 0658fc9fbd45f5339be197645c2fc7ca2caee8dc Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Wed, 30 Jul 2025 15:53:50 +0000 Subject: [PATCH 4/4] chore: adding changelog file 612.added.md [dependabot-skip] --- doc/changelog.d/{612.miscellaneous.md => 612.added.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename doc/changelog.d/{612.miscellaneous.md => 612.added.md} (100%) diff --git a/doc/changelog.d/612.miscellaneous.md b/doc/changelog.d/612.added.md similarity index 100% rename from doc/changelog.d/612.miscellaneous.md rename to doc/changelog.d/612.added.md