From 5b59ff62a57f752be6d50291e2f68bcc11d87918 Mon Sep 17 00:00:00 2001 From: Paul Walters Date: Fri, 25 Jul 2025 13:35:05 -0400 Subject: [PATCH 1/6] 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/6] 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 11e6085fd6fdc4005cac47c2462f9769c682853d Mon Sep 17 00:00:00 2001 From: Paul Walters Date: Fri, 1 Aug 2025 09:42:51 -0400 Subject: [PATCH 3/6] Added time filtering to importThermalSignal API. --- src/ansys/sherlock/core/lifecycle.py | 4 +- .../sherlock/core/types/lifecycle_types.py | 20 ++++--- tests/test_lifecycle.py | 52 +++++++++++++++++-- 3 files changed, 64 insertions(+), 12 deletions(-) diff --git a/src/ansys/sherlock/core/lifecycle.py b/src/ansys/sherlock/core/lifecycle.py index 0e351a569..290891ea2 100644 --- a/src/ansys/sherlock/core/lifecycle.py +++ b/src/ansys/sherlock/core/lifecycle.py @@ -2152,7 +2152,9 @@ def import_thermal_signal( >>> time_removal= False, >>> load_range_percentage=0.25, >>> number_of_bins=0, - >>> filtering_limit=0.0, + >>> temperature_range_filtering_limit=0.0, + >>> time_filtering_limit=72.0, + >>> time_filtering_limit_units="hr", >>> generated_cycles_label="Second Generated Cycles from Python", >>> ) >>> ) diff --git a/src/ansys/sherlock/core/types/lifecycle_types.py b/src/ansys/sherlock/core/types/lifecycle_types.py index fd65f4f1d..66c9d569d 100644 --- a/src/ansys/sherlock/core/types/lifecycle_types.py +++ b/src/ansys/sherlock/core/types/lifecycle_types.py @@ -73,15 +73,21 @@ class ImportThermalSignalRequest(BaseModel): 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""" + """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""" + """Number of bins for binning cycles, 0 for no binning.""" + temperature_range_filtering_limit: float + """Minimum cycle range to include in results, 0 for not filtering.""" + time_filtering_limit: float + """Maximum cycle time to include in results, default is 72 hours.""" + time_filtering_limit_units: str + """Units of the time filtering limit.""" 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") + @field_validator( + "file_name", "project", "phase_name", "time_filtering_limit_units", "generated_cycles_label" + ) @classmethod def str_validation(cls, value: str, info: ValidationInfo): """Validate string fields listed.""" @@ -105,6 +111,8 @@ def _convert_to_grpc(self) -> SherlockLifeCycleService_pb2.ImportThermalSignalRe timeRemoval=self.time_removal, loadRangePercentage=self.load_range_percentage, numberOfBins=self.number_of_bins, - filteringLimit=self.filtering_limit, + temperatureRangeFilteringLimit=self.temperature_range_filtering_limit, + timeFilteringLimit=self.time_filtering_limit, + timeFilteringLimitUnits=self.time_filtering_limit_units, generatedCyclesLabel=self.generated_cycles_label, ) diff --git a/tests/test_lifecycle.py b/tests/test_lifecycle.py index fd8493edf..83d74802a 100644 --- a/tests/test_lifecycle.py +++ b/tests/test_lifecycle.py @@ -2073,7 +2073,9 @@ def helper_test_import_thermal_signal(lifecycle: Lifecycle): time_removal=False, load_range_percentage=0.25, number_of_bins=0, - filtering_limit=0.0, + temperature_range_filtering_limit=0.0, + time_filtering_limit=72.0, + time_filtering_limit_units="hr", generated_cycles_label="Generated Cycles from pySherlock", ) ) @@ -2103,7 +2105,9 @@ def helper_test_import_thermal_signal(lifecycle: Lifecycle): time_removal=False, load_range_percentage=0.25, number_of_bins=0, - filtering_limit=0.0, + temperature_range_filtering_limit=0.0, + time_filtering_limit=72.0, + time_filtering_limit_units="hr", generated_cycles_label="Generated Cycles from pySherlock", ) ) @@ -2133,7 +2137,9 @@ def helper_test_import_thermal_signal(lifecycle: Lifecycle): time_removal=False, load_range_percentage=0.25, number_of_bins=0, - filtering_limit=0.0, + temperature_range_filtering_limit=0.0, + time_filtering_limit=72.0, + time_filtering_limit_units="hr", generated_cycles_label="Generated Cycles from pySherlock", ) ) @@ -2163,7 +2169,9 @@ def helper_test_import_thermal_signal(lifecycle: Lifecycle): time_removal=False, load_range_percentage=0.25, number_of_bins=-1, - filtering_limit=0.0, + temperature_range_filtering_limit=0.0, + time_filtering_limit=72.0, + time_filtering_limit_units="hr", generated_cycles_label="Generated Cycles from pySherlock", ) ) @@ -2193,7 +2201,41 @@ def helper_test_import_thermal_signal(lifecycle: Lifecycle): time_removal=False, load_range_percentage=0.25, number_of_bins=0, - filtering_limit=0.0, + temperature_range_filtering_limit=0.0, + time_filtering_limit=72.0, + time_filtering_limit_units="", + generated_cycles_label="", + ) + ) + pytest.fail("No exception raised when using a missing time_filtering_limit parameter") + except Exception as e: + assert isinstance(e, pydantic.ValidationError) + assert ( + str(e.errors()[0]["msg"]) + == "Value error, time_filtering_limit_units 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=0, + temperature_range_filtering_limit=0.0, + time_filtering_limit=72.0, + time_filtering_limit_units="hr", generated_cycles_label="", ) ) From c8bc90108302cb056f5c5709087e157f7c27263b Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Fri, 1 Aug 2025 14:19:22 +0000 Subject: [PATCH 4/6] chore: adding changelog file 619.miscellaneous.md [dependabot-skip] --- doc/changelog.d/619.miscellaneous.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/619.miscellaneous.md diff --git a/doc/changelog.d/619.miscellaneous.md b/doc/changelog.d/619.miscellaneous.md new file mode 100644 index 000000000..4c4f33b37 --- /dev/null +++ b/doc/changelog.d/619.miscellaneous.md @@ -0,0 +1 @@ +Feat: Added time filtering to importThermalSignal API. \ No newline at end of file From 7dd66fb5bd70e6e202bf0eac813c3452c97bd1bf Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Tue, 12 Aug 2025 19:47:50 +0000 Subject: [PATCH 5/6] chore: adding changelog file 619.miscellaneous.md [dependabot-skip] --- doc/changelog.d/619.miscellaneous.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changelog.d/619.miscellaneous.md b/doc/changelog.d/619.miscellaneous.md index 4c4f33b37..3b84c146b 100644 --- a/doc/changelog.d/619.miscellaneous.md +++ b/doc/changelog.d/619.miscellaneous.md @@ -1 +1 @@ -Feat: Added time filtering to importThermalSignal API. \ No newline at end of file +Feat: Added time filtering to importThermalSignal API. From fa4543c3f4ca4982341375af593550a549bb9568 Mon Sep 17 00:00:00 2001 From: Paul Walters Date: Tue, 12 Aug 2025 15:50:24 -0400 Subject: [PATCH 6/6] Added time filtering to importThermalSignal API. --- tests/test_lifecycle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_lifecycle.py b/tests/test_lifecycle.py index 83d74802a..058fe784f 100644 --- a/tests/test_lifecycle.py +++ b/tests/test_lifecycle.py @@ -2204,10 +2204,10 @@ def helper_test_import_thermal_signal(lifecycle: Lifecycle): temperature_range_filtering_limit=0.0, time_filtering_limit=72.0, time_filtering_limit_units="", - generated_cycles_label="", + generated_cycles_label="Generated Cycles from pySherlock", ) ) - pytest.fail("No exception raised when using a missing time_filtering_limit parameter") + pytest.fail("No exception raised when using a missing time_filtering_limit_units parameter") except Exception as e: assert isinstance(e, pydantic.ValidationError) assert (