From 0f11fee565175a0bcc34c11ca9a2b2c410448891 Mon Sep 17 00:00:00 2001 From: Maxime Cailler Date: Wed, 2 Jul 2025 09:32:05 +0200 Subject: [PATCH 01/17] Pushed draft branch for thermic source --- MyTestBench.py | 20 +++ pyproject.toml | 14 +- src/ansys/speos/core/project.py | 309 ++++++++++++++------------------ src/ansys/speos/core/source.py | 105 +++++++++++ 4 files changed, 271 insertions(+), 177 deletions(-) create mode 100644 MyTestBench.py diff --git a/MyTestBench.py b/MyTestBench.py new file mode 100644 index 000000000..9ba91d50b --- /dev/null +++ b/MyTestBench.py @@ -0,0 +1,20 @@ +import ansys.speos.core.source as source +from ansys.speos.core.launcher import launch_local_speos_rpc_server +import ansys.speos.core as core + +import os + +speos = launch_local_speos_rpc_server(version='252') +speos_file = os.path.join(os.getcwd(), "Speos Bench", "Inverse.1.1.speos", "Inverse.1.1.speos") +project = core.Project(speos=speos, path=speos_file) +mysource = project.find(name=".*", name_regex=True, feature_type=source.SourceThermic) +mysource[0].set_emissive_faces_temp(value=100) +mysource[0].commit() +mysimu = project.find(name=".*", name_regex=True, feature_type=core.simulation.SimulationInverse) +mysimu = mysimu[0] +new_source = project.create_source(name="test", feature_type=source.SourceThermic) +new_source.commit() +print(project) + +# mysimu.compute_CPU() +# print(mysimu.result_list) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 2597dc3d9..cff13ec97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ dependencies=[ "protobuf>=3.20,<7", "grpcio>=1.50.0,<1.71", "grpcio-health-checking>=1.45.0,<1.68", - "ansys-api-speos==0.15.2", + "ansys-api-speos==0.15.0", "ansys-tools-path>=0.3.1", "numpy>=1.20.3,<3", "comtypes>=1.4,<1.5", @@ -43,11 +43,11 @@ graphics = [ ] tests = [ "psutil==7.0.0", - "pytest==8.4.0", + "pytest==8.3.5", "pyvista>=0.40.0,<0.46", "ansys-tools-visualization-interface>=0.8.3", "ansys-platform-instancemanagement>=1.0.3", - "pytest-cov==6.2.1", + "pytest-cov==6.1.1", ] jupyter = [ "matplotlib", @@ -55,10 +55,10 @@ jupyter = [ "ipywidgets", "pyvista[jupyter]>=0.43,<0.46", "ansys-tools-visualization-interface>=0.8.3", - "notebook==7.4.3", + "notebook==7.4.2", ] doc = [ - "ansys-sphinx-theme==1.5.2", + "ansys-sphinx-theme==1.4.4", "numpydoc==1.8.0", "Sphinx==8.1.3", "sphinx-copybutton==0.5.2", @@ -68,8 +68,8 @@ doc = [ "sphinxcontrib-mermaid==1.0.0", "myst-parser==4.0.1", "nbsphinx==0.9.7", - "jupytext==1.17.2", - "jupyterlab==4.4.3", + "jupytext==1.17.1", + "jupyterlab==4.4.2", "jupyter-server==2.16.0", "nbconvert==7.16.6", "pyvista[jupyter]>=0.43,<0.46", diff --git a/src/ansys/speos/core/project.py b/src/ansys/speos/core/project.py index 567d25ec7..0c3b3083a 100644 --- a/src/ansys/speos/core/project.py +++ b/src/ansys/speos/core/project.py @@ -34,7 +34,6 @@ import ansys.speos.core.body as body import ansys.speos.core.face as face from ansys.speos.core.generic.general_methods import graphics_required -from ansys.speos.core.generic.visualization_methods import local2absolute from ansys.speos.core.kernel.body import BodyLink from ansys.speos.core.kernel.face import FaceLink from ansys.speos.core.kernel.part import ProtoPart @@ -43,7 +42,6 @@ import ansys.speos.core.part as part import ansys.speos.core.proto_message_utils as proto_message_utils from ansys.speos.core.sensor import ( - Sensor3DIrradiance, SensorCamera, SensorIrradiance, SensorRadiance, @@ -54,10 +52,10 @@ SimulationInverse, ) from ansys.speos.core.source import ( - SourceAmbientNaturalLight, SourceLuminaire, SourceRayFile, SourceSurface, + SourceThermic, ) from ansys.speos.core.speos import Speos @@ -95,13 +93,12 @@ class Project: Link object for the scene in database. """ - def __init__(self, speos: Speos, path: Optional[Union[str, Path]] = ""): + def __init__(self, speos: Speos, path: str = ""): self.client = speos.client """Speos instance client.""" self.scene_link = speos.client.scenes().create() """Link object for the scene in database.""" self._features = [] - path = str(path) if len(path): self.scene_link.load_file(path) self._fill_features() @@ -158,7 +155,7 @@ def create_source( description: str = "", feature_type: type = SourceSurface, metadata: Optional[Mapping[str, str]] = None, - ) -> Union[SourceSurface, SourceRayFile, SourceLuminaire, SourceAmbientNaturalLight]: + ) -> Union[SourceSurface, SourceRayFile, SourceLuminaire, SourceThermic]: """Create a new Source feature. Parameters @@ -173,8 +170,7 @@ def create_source( By default, ``ansys.speos.core.source.SourceSurface``. Allowed types: Union[ansys.speos.core.source.SourceSurface, ansys.speos.core.source.SourceRayFile, \ - ansys.speos.core.source.SourceLuminaire, \ - ansys.speos.core.source.SourceAmbientNaturalLight]. + ansys.speos.core.source.SourceLuminaire]. metadata : Optional[Mapping[str, str]] Metadata of the feature. By default, ``{}``. @@ -182,7 +178,7 @@ def create_source( Returns ------- Union[ansys.speos.core.source.SourceSurface,ansys.speos.core.source.SourceRayFile,\ - ansys.speos.core.source.SourceLuminaire, ansys.speos.core.source.SourceAmbientNaturalLight] + ansys.speos.core.source.SourceLuminaire] Source class instance. """ if metadata is None: @@ -195,40 +191,39 @@ def create_source( ) raise ValueError(msg) feature = None - match feature_type.__name__: - case "SourceSurface": - feature = SourceSurface( - project=self, - name=name, - description=description, - metadata=metadata, - ) - case "SourceRayFile": - feature = SourceRayFile( - project=self, - name=name, - description=description, - metadata=metadata, - ) - case "SourceLuminaire": - feature = SourceLuminaire( - project=self, - name=name, - description=description, - metadata=metadata, - ) - case "SourceAmbientNaturalLight": - feature = SourceAmbientNaturalLight( - project=self, - name=name, - description=description, - metadata=metadata, - ) - case _: - msg = "Requested feature {} does not exist in supported list {}".format( - feature_type, [SourceSurface, SourceLuminaire, SourceRayFile] - ) - raise TypeError(msg) + if feature_type == SourceSurface: + feature = SourceSurface( + project=self, + name=name, + description=description, + metadata=metadata, + ) + elif feature_type == SourceRayFile: + feature = SourceRayFile( + project=self, + name=name, + description=description, + metadata=metadata, + ) + elif feature_type == SourceLuminaire: + feature = SourceLuminaire( + project=self, + name=name, + description=description, + metadata=metadata, + ) + elif feature_type == SourceThermic: + feature = SourceThermic( + project=self, + name=name, + description=description, + metadata=metadata, + ) + else: + msg = "Requested feature {} does not exist in supported list {}".format( + feature_type, [SourceSurface, SourceLuminaire, SourceRayFile] + ) + raise TypeError(msg) self._features.append(feature) return feature @@ -275,38 +270,37 @@ def create_simulation( ) raise ValueError(msg) feature = None - match feature_type.__name__: - case "SimulationDirect": - feature = SimulationDirect( - project=self, - name=name, - description=description, - metadata=metadata, - ) - case "SimulationInverse": - feature = SimulationInverse( - project=self, - name=name, - description=description, - metadata=metadata, - ) - case "SimulationInteractive": - feature = SimulationInteractive( - project=self, - name=name, - description=description, - metadata=metadata, - ) - case _: - msg = "Requested feature {} does not exist in supported list {}".format( - feature_type, - [ - SimulationDirect, - SimulationInverse, - SimulationInteractive, - ], - ) - raise TypeError(msg) + if feature_type == SimulationDirect: + feature = SimulationDirect( + project=self, + name=name, + description=description, + metadata=metadata, + ) + elif feature_type == SimulationInverse: + feature = SimulationInverse( + project=self, + name=name, + description=description, + metadata=metadata, + ) + elif feature_type == SimulationInteractive: + feature = SimulationInteractive( + project=self, + name=name, + description=description, + metadata=metadata, + ) + else: + msg = "Requested feature {} does not exist in supported list {}".format( + feature_type, + [ + SimulationDirect, + SimulationInverse, + SimulationInteractive, + ], + ) + raise TypeError(msg) self._features.append(feature) return feature @@ -316,7 +310,7 @@ def create_sensor( description: str = "", feature_type: type = SensorIrradiance, metadata: Optional[Mapping[str, str]] = None, - ) -> Union[SensorCamera, SensorRadiance, SensorIrradiance, Sensor3DIrradiance]: + ) -> Union[SensorCamera, SensorRadiance, SensorIrradiance]: """Create a new Sensor feature. Parameters @@ -331,8 +325,7 @@ def create_sensor( By default, ``ansys.speos.core.sensor.SensorIrradiance``. Allowed types: Union[ansys.speos.core.sensor.SensorCamera,\ ansys.speos.core.sensor.SensorRadiance, \ - ansys.speos.core.sensor.SensorIrradiance, \ - ansys.speos.core.sensor.Sensor3DIrradiance]. + ansys.speos.core.sensor.SensorIrradiance]. metadata : Optional[Mapping[str, str]] Metadata of the feature. By default, ``{}``. @@ -340,8 +333,7 @@ def create_sensor( Returns ------- Union[ansys.speos.core.sensor.SensorCamera,\ - ansys.speos.core.sensor.SensorRadiance, ansys.speos.core.sensor.SensorIrradiance, \ - ansys.speos.core.sensor.Sensor3DIrradiance] + ansys.speos.core.sensor.SensorRadiance, ansys.speos.core.sensor.SensorIrradiance] Sensor class instance. """ if metadata is None: @@ -354,41 +346,32 @@ def create_sensor( ) raise ValueError(msg) feature = None - match feature_type.__name__: - case "SensorIrradiance": - feature = SensorIrradiance( - project=self, - name=name, - description=description, - metadata=metadata, - ) - case "SensorRadiance": - feature = SensorRadiance( - project=self, - name=name, - description=description, - metadata=metadata, - ) - case "SensorCamera": - feature = SensorCamera( - project=self, - name=name, - description=description, - metadata=metadata, - ) - case "Sensor3DIrradiance": - feature = Sensor3DIrradiance( - project=self, - name=name, - description=description, - metadata=metadata, - ) - case _: - msg = "Requested feature {} does not exist in supported list {}".format( - feature_type, - [SensorIrradiance, SensorRadiance, SensorCamera, Sensor3DIrradiance], - ) - raise TypeError(msg) + if feature_type == SensorIrradiance: + feature = SensorIrradiance( + project=self, + name=name, + description=description, + metadata=metadata, + ) + elif feature_type == SensorRadiance: + feature = SensorRadiance( + project=self, + name=name, + description=description, + metadata=metadata, + ) + elif feature_type == SensorCamera: + feature = SensorCamera( + project=self, + name=name, + description=description, + metadata=metadata, + ) + else: + msg = "Requested feature {} does not exist in supported list {}".format( + feature_type, [SensorIrradiance, SensorRadiance, SensorCamera] + ) + raise TypeError(msg) self._features.append(feature) return feature @@ -438,6 +421,7 @@ def find( SourceSurface, SourceLuminaire, SourceRayFile, + SourceThermic, SensorIrradiance, SensorRadiance, SensorCamera, @@ -772,8 +756,6 @@ def _fill_features(self): op_feature._fill(mat_inst=mat_inst) for src_inst in scene_data.sources: - if src_inst.name in [_._name for _ in self._features]: - continue src_feat = None if src_inst.HasField("rayfile_properties"): src_feat = SourceRayFile( @@ -796,20 +778,17 @@ def _fill_features(self): source_instance=src_inst, default_values=False, ) - elif src_inst.HasField("ambient_properties"): - if src_inst.ambient_properties.HasField("natural_light_properties"): - src_feat = SourceAmbientNaturalLight( - project=self, - name=src_inst.name, - source_instance=src_inst, - default_values=False, - ) + elif src_inst.HasField("thermic_properties"): + src_feat = SourceThermic( + project=self, + name=src_inst.name, + source_instance=src_inst, + default_values=False, + ) if src_feat is not None: self._features.append(src_feat) for ssr_inst in scene_data.sensors: - if ssr_inst.name in [_._name for _ in self._features]: - continue ssr_feat = None if ssr_inst.HasField("irradiance_properties"): ssr_feat = SensorIrradiance( @@ -832,18 +811,9 @@ def _fill_features(self): sensor_instance=ssr_inst, default_values=False, ) - elif ssr_inst.HasField("irradiance_3d_properties"): - ssr_feat = Sensor3DIrradiance( - project=self, - name=ssr_inst.name, - sensor_instance=ssr_inst, - default_values=False, - ) self._features.append(ssr_feat) for sim_inst in scene_data.simulations: - if sim_inst.name in [_._name for _ in self._features]: - continue sim_feat = None simulation_template_link = self.client[sim_inst.simulation_guid].get() if simulation_template_link.HasField("direct_mc_simulation_template"): @@ -892,6 +862,26 @@ def __extract_part_mesh_info( """ import pyvista as pv + def local2absolute(local_vertice: np.ndarray, coordinates) -> np.ndarray: + """Convert local coordinate to global coordinate. + + Parameters + ---------- + local_vertice: np.ndarray + numpy array includes x, y, z info. + + Returns + ------- + np.ndarray + numpy array includes x, y, z info + + """ + global_origin = np.array(coordinates[:3]) + global_x = np.array(coordinates[3:6]) * local_vertice[0] + global_y = np.array(coordinates[6:9]) * local_vertice[1] + global_z = np.array(coordinates[9:]) * local_vertice[2] + return global_origin + global_x + global_y + global_z + part_coordinate = [ 0.0, 0.0, @@ -937,7 +927,6 @@ def _create_speos_feature_preview( SensorCamera, SensorRadiance, SensorIrradiance, - Sensor3DIrradiance, SourceLuminaire, SourceRayFile, SourceLuminaire, @@ -951,7 +940,7 @@ def _create_speos_feature_preview( plotter: Plotter ansys.tools.visualization_interface.Plotter speos_feature: Union[SensorCamera, SensorRadiance, SensorIrradiance, - Sensor3DIrradiance, SourceLuminaire, SourceRayFile, SourceLuminaire] + SourceLuminaire, SourceRayFile, SourceLuminaire] speos feature whose visual data will be added. scene_seize: float seize of max scene bounds @@ -967,7 +956,6 @@ def _create_speos_feature_preview( SensorIrradiance, SensorRadiance, SensorCamera, - Sensor3DIrradiance, SourceLuminaire, SourceRayFile, SourceSurface, @@ -1036,18 +1024,6 @@ def _create_preview(self, viz_args=None) -> Plotter: from ansys.tools.visualization_interface import Plotter - def find_all_subparts(target_part): - subparts = [] - current_subparts_found = target_part.find( - name=".*", name_regex=True, feature_type=part.Part.SubPart - ) - if not current_subparts_found: - return subparts - for subpart in current_subparts_found: - subparts.append(subpart) - subparts.extend(find_all_subparts(subpart)) - return subparts - if viz_args is None: viz_args = {} viz_args["show_edges"] = True @@ -1057,27 +1033,20 @@ def find_all_subparts(target_part): if self.scene_link.get().part_guid != "": _preview_mesh = pv.PolyData() # Retrieve root part - root_part = self.find(name="", feature_type=part.Part)[0] - subparts = find_all_subparts(root_part) # all subpart - subparts = [ - subpart - for subpart in subparts - if any( - isinstance(_geo_feature, body.Body) for _geo_feature in subpart._geom_features - ) - ] # filter subpart which contains ansys.speos.core.body.Body - for subpart in subparts: - subpart_axis = subpart._part_instance.axis_system - subpart_guid = subpart._part_instance.part_guid - part_mesh_data = self.__extract_part_mesh_info( - part_data=self.client[subpart_guid].get(), - part_coordinate_info=subpart_axis, - ) - if part_mesh_data is not None: - _preview_mesh = _preview_mesh.append_polydata(part_mesh_data) + root_part_data = self.client[self.scene_link.get().part_guid].get() + + # Loop on all sub parts to retrieve their mesh + if len(root_part_data.parts) != 0: + for part_idx, part_item in enumerate(root_part_data.parts): + part_item_data = self.client[part_item.part_guid].get() + poly_data = self.__extract_part_mesh_info( + part_data=part_item_data, + part_coordinate_info=part_item.axis_system, + ) + if poly_data is not None: + _preview_mesh = _preview_mesh.append_polydata(poly_data) # Add also the mesh of bodies directly contained in root part - root_part_data = self.client[self.scene_link.get().part_guid].get() poly_data = self.__extract_part_mesh_info(part_data=root_part_data) if poly_data is not None: _preview_mesh = _preview_mesh.append_polydata(poly_data) diff --git a/src/ansys/speos/core/source.py b/src/ansys/speos/core/source.py index 583765fc6..279bad86b 100644 --- a/src/ansys/speos/core/source.py +++ b/src/ansys/speos/core/source.py @@ -1255,6 +1255,111 @@ def delete(self) -> SourceSurface: return self +class SourceThermic(BaseSource): + """ThermicSource. + + By default, a flux from intensity file is chosen, with an incandescent spectrum. + + Parameters + ---------- + project : ansys.speos.core.project.Project + Project that will own the feature. + name : str + Name of the feature. + description : str + Description of the feature. + By default, ``""``. + metadata : Optional[Mapping[str, str]] + Metadata of the feature. + By default, ``{}``. + default_values : bool + Uses default values when True. + """ + + def __init__( + self, + project: project.Project, + name: str, + description: str = "", + metadata: Optional[Mapping[str, str]] = None, + source_instance: Optional[ProtoScene.SourceInstance] = None, + default_values: bool = True, + ) -> None: + if metadata is None: + metadata = {} + + super().__init__( + project=project, + name=name, + description=description, + metadata=metadata, + source_instance=source_instance, + ) + self._speos_client = self._project.client + self._name = name + + self._intensity = Intensity( + speos_client=self._speos_client, + name=name + ".Intensity", + key="", + ) + + if default_values: + # Default values + self.set_emissive_faces(geometries=[]) + self.set_emissive_faces_temp(value=2000) + + + def set_emissive_faces(self, geometries: List[tuple[GeoRef, bool]]) -> SourceThermic: + """Set existence constant. + + Parameters + ---------- + geometries : List[tuple[ansys.speos.core.geo_ref.GeoRef, bool]] + List of (face, reverseNormal). + + Returns + ------- + ansys.speos.core.source.SourceSurface + Surface source. + """ + + + self._source_instance.thermic_properties.emissive_faces_properties.ClearField( + "geo_paths" + ) + if geometries != []: + my_list = [ + ProtoScene.GeoPath(geo_path=gr.to_native_link(), reverse_normal=reverse_normal) + for (gr, reverse_normal) in geometries + ] + self._source_instance.thermic_properties.emissive_faces_properties.geo_paths.extend( + my_list + ) + self._source_template.thermic.emissives_faces.SetInParent() + return self + + def set_emissive_faces_temp(self, value: float = 2000) -> SourceThermic: + if not self._source_template.thermic.HasField("temperature_field"): + self._source_template.thermic.emissives_faces.temperature = value + return self + + # def commit(self) -> SourceThermic: + # """Save feature: send the local data to the speos server database. + # + # Returns + # ------- + # ansys.speos.core.source.SourceSurface + # Source feature. + # """ + # + # # spectrum & source + # super().commit() + # return self + + + + class BaseSourceAmbient(BaseSource): """ Super Class for ambient sources. From ddf82a6a5db1300c20b6a9af0da760dd3fd85663 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 08:51:57 +0000 Subject: [PATCH 02/17] ci: auto fixes from pre-commit.com hooks. for more information, see https://pre-commit.ci --- MyTestBench.py | 12 ++++++------ src/ansys/speos/core/source.py | 23 ++++++++--------------- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/MyTestBench.py b/MyTestBench.py index 9ba91d50b..e863b803d 100644 --- a/MyTestBench.py +++ b/MyTestBench.py @@ -1,10 +1,10 @@ -import ansys.speos.core.source as source -from ansys.speos.core.launcher import launch_local_speos_rpc_server -import ansys.speos.core as core - import os -speos = launch_local_speos_rpc_server(version='252') +import ansys.speos.core as core +from ansys.speos.core.launcher import launch_local_speos_rpc_server +import ansys.speos.core.source as source + +speos = launch_local_speos_rpc_server(version="252") speos_file = os.path.join(os.getcwd(), "Speos Bench", "Inverse.1.1.speos", "Inverse.1.1.speos") project = core.Project(speos=speos, path=speos_file) mysource = project.find(name=".*", name_regex=True, feature_type=source.SourceThermic) @@ -17,4 +17,4 @@ print(project) # mysimu.compute_CPU() -# print(mysimu.result_list) \ No newline at end of file +# print(mysimu.result_list) diff --git a/src/ansys/speos/core/source.py b/src/ansys/speos/core/source.py index 279bad86b..f9f05b5ea 100644 --- a/src/ansys/speos/core/source.py +++ b/src/ansys/speos/core/source.py @@ -1277,13 +1277,13 @@ class SourceThermic(BaseSource): """ def __init__( - self, - project: project.Project, - name: str, - description: str = "", - metadata: Optional[Mapping[str, str]] = None, - source_instance: Optional[ProtoScene.SourceInstance] = None, - default_values: bool = True, + self, + project: project.Project, + name: str, + description: str = "", + metadata: Optional[Mapping[str, str]] = None, + source_instance: Optional[ProtoScene.SourceInstance] = None, + default_values: bool = True, ) -> None: if metadata is None: metadata = {} @@ -1309,7 +1309,6 @@ def __init__( self.set_emissive_faces(geometries=[]) self.set_emissive_faces_temp(value=2000) - def set_emissive_faces(self, geometries: List[tuple[GeoRef, bool]]) -> SourceThermic: """Set existence constant. @@ -1323,11 +1322,7 @@ def set_emissive_faces(self, geometries: List[tuple[GeoRef, bool]]) -> SourceThe ansys.speos.core.source.SourceSurface Surface source. """ - - - self._source_instance.thermic_properties.emissive_faces_properties.ClearField( - "geo_paths" - ) + self._source_instance.thermic_properties.emissive_faces_properties.ClearField("geo_paths") if geometries != []: my_list = [ ProtoScene.GeoPath(geo_path=gr.to_native_link(), reverse_normal=reverse_normal) @@ -1358,8 +1353,6 @@ def set_emissive_faces_temp(self, value: float = 2000) -> SourceThermic: # return self - - class BaseSourceAmbient(BaseSource): """ Super Class for ambient sources. From 5f00348ba7777d88117af9ad43eb2e931971fcc9 Mon Sep 17 00:00:00 2001 From: Maxime Cailler Date: Wed, 2 Jul 2025 11:55:56 +0200 Subject: [PATCH 03/17] Re-adding full project.py script --- src/ansys/speos/core/project.py | 307 ++++++++++++++++++-------------- 1 file changed, 177 insertions(+), 130 deletions(-) diff --git a/src/ansys/speos/core/project.py b/src/ansys/speos/core/project.py index 0c3b3083a..a7d95592c 100644 --- a/src/ansys/speos/core/project.py +++ b/src/ansys/speos/core/project.py @@ -34,6 +34,7 @@ import ansys.speos.core.body as body import ansys.speos.core.face as face from ansys.speos.core.generic.general_methods import graphics_required +from ansys.speos.core.generic.visualization_methods import local2absolute from ansys.speos.core.kernel.body import BodyLink from ansys.speos.core.kernel.face import FaceLink from ansys.speos.core.kernel.part import ProtoPart @@ -42,6 +43,7 @@ import ansys.speos.core.part as part import ansys.speos.core.proto_message_utils as proto_message_utils from ansys.speos.core.sensor import ( + Sensor3DIrradiance, SensorCamera, SensorIrradiance, SensorRadiance, @@ -52,6 +54,7 @@ SimulationInverse, ) from ansys.speos.core.source import ( + SourceAmbientNaturalLight, SourceLuminaire, SourceRayFile, SourceSurface, @@ -93,12 +96,13 @@ class Project: Link object for the scene in database. """ - def __init__(self, speos: Speos, path: str = ""): + def __init__(self, speos: Speos, path: Optional[Union[str, Path]] = ""): self.client = speos.client """Speos instance client.""" self.scene_link = speos.client.scenes().create() """Link object for the scene in database.""" self._features = [] + path = str(path) if len(path): self.scene_link.load_file(path) self._fill_features() @@ -155,7 +159,7 @@ def create_source( description: str = "", feature_type: type = SourceSurface, metadata: Optional[Mapping[str, str]] = None, - ) -> Union[SourceSurface, SourceRayFile, SourceLuminaire, SourceThermic]: + ) -> Union[SourceSurface, SourceRayFile, SourceLuminaire, SourceAmbientNaturalLight, SourceThermic]: """Create a new Source feature. Parameters @@ -170,7 +174,8 @@ def create_source( By default, ``ansys.speos.core.source.SourceSurface``. Allowed types: Union[ansys.speos.core.source.SourceSurface, ansys.speos.core.source.SourceRayFile, \ - ansys.speos.core.source.SourceLuminaire]. + ansys.speos.core.source.SourceLuminaire, \ + ansys.speos.core.source.SourceAmbientNaturalLight]. metadata : Optional[Mapping[str, str]] Metadata of the feature. By default, ``{}``. @@ -178,7 +183,7 @@ def create_source( Returns ------- Union[ansys.speos.core.source.SourceSurface,ansys.speos.core.source.SourceRayFile,\ - ansys.speos.core.source.SourceLuminaire] + ansys.speos.core.source.SourceLuminaire, ansys.speos.core.source.SourceAmbientNaturalLight] Source class instance. """ if metadata is None: @@ -191,39 +196,47 @@ def create_source( ) raise ValueError(msg) feature = None - if feature_type == SourceSurface: - feature = SourceSurface( - project=self, - name=name, - description=description, - metadata=metadata, - ) - elif feature_type == SourceRayFile: - feature = SourceRayFile( - project=self, - name=name, - description=description, - metadata=metadata, - ) - elif feature_type == SourceLuminaire: - feature = SourceLuminaire( - project=self, - name=name, - description=description, - metadata=metadata, - ) - elif feature_type == SourceThermic: - feature = SourceThermic( - project=self, - name=name, - description=description, - metadata=metadata, - ) - else: - msg = "Requested feature {} does not exist in supported list {}".format( - feature_type, [SourceSurface, SourceLuminaire, SourceRayFile] - ) - raise TypeError(msg) + match feature_type.__name__: + case "SourceSurface": + feature = SourceSurface( + project=self, + name=name, + description=description, + metadata=metadata, + ) + case "SourceRayFile": + feature = SourceRayFile( + project=self, + name=name, + description=description, + metadata=metadata, + ) + case "SourceLuminaire": + feature = SourceLuminaire( + project=self, + name=name, + description=description, + metadata=metadata, + ) + case "SourceAmbientNaturalLight": + feature = SourceAmbientNaturalLight( + project=self, + name=name, + description=description, + metadata=metadata, + ) + case "SourceThermic": + feature = SourceThermic( + project=self, + name=name, + description=description, + metadata=metadata, + ) + case _: + msg = "Requested feature {} does not exist in supported list {}".format( + feature_type, [SourceSurface, SourceLuminaire, SourceRayFile] + ) + raise TypeError(msg) self._features.append(feature) return feature @@ -270,37 +283,38 @@ def create_simulation( ) raise ValueError(msg) feature = None - if feature_type == SimulationDirect: - feature = SimulationDirect( - project=self, - name=name, - description=description, - metadata=metadata, - ) - elif feature_type == SimulationInverse: - feature = SimulationInverse( - project=self, - name=name, - description=description, - metadata=metadata, - ) - elif feature_type == SimulationInteractive: - feature = SimulationInteractive( - project=self, - name=name, - description=description, - metadata=metadata, - ) - else: - msg = "Requested feature {} does not exist in supported list {}".format( - feature_type, - [ - SimulationDirect, - SimulationInverse, - SimulationInteractive, - ], - ) - raise TypeError(msg) + match feature_type.__name__: + case "SimulationDirect": + feature = SimulationDirect( + project=self, + name=name, + description=description, + metadata=metadata, + ) + case "SimulationInverse": + feature = SimulationInverse( + project=self, + name=name, + description=description, + metadata=metadata, + ) + case "SimulationInteractive": + feature = SimulationInteractive( + project=self, + name=name, + description=description, + metadata=metadata, + ) + case _: + msg = "Requested feature {} does not exist in supported list {}".format( + feature_type, + [ + SimulationDirect, + SimulationInverse, + SimulationInteractive, + ], + ) + raise TypeError(msg) self._features.append(feature) return feature @@ -310,7 +324,7 @@ def create_sensor( description: str = "", feature_type: type = SensorIrradiance, metadata: Optional[Mapping[str, str]] = None, - ) -> Union[SensorCamera, SensorRadiance, SensorIrradiance]: + ) -> Union[SensorCamera, SensorRadiance, SensorIrradiance, Sensor3DIrradiance]: """Create a new Sensor feature. Parameters @@ -325,7 +339,8 @@ def create_sensor( By default, ``ansys.speos.core.sensor.SensorIrradiance``. Allowed types: Union[ansys.speos.core.sensor.SensorCamera,\ ansys.speos.core.sensor.SensorRadiance, \ - ansys.speos.core.sensor.SensorIrradiance]. + ansys.speos.core.sensor.SensorIrradiance, \ + ansys.speos.core.sensor.Sensor3DIrradiance]. metadata : Optional[Mapping[str, str]] Metadata of the feature. By default, ``{}``. @@ -333,7 +348,8 @@ def create_sensor( Returns ------- Union[ansys.speos.core.sensor.SensorCamera,\ - ansys.speos.core.sensor.SensorRadiance, ansys.speos.core.sensor.SensorIrradiance] + ansys.speos.core.sensor.SensorRadiance, ansys.speos.core.sensor.SensorIrradiance, \ + ansys.speos.core.sensor.Sensor3DIrradiance] Sensor class instance. """ if metadata is None: @@ -346,32 +362,41 @@ def create_sensor( ) raise ValueError(msg) feature = None - if feature_type == SensorIrradiance: - feature = SensorIrradiance( - project=self, - name=name, - description=description, - metadata=metadata, - ) - elif feature_type == SensorRadiance: - feature = SensorRadiance( - project=self, - name=name, - description=description, - metadata=metadata, - ) - elif feature_type == SensorCamera: - feature = SensorCamera( - project=self, - name=name, - description=description, - metadata=metadata, - ) - else: - msg = "Requested feature {} does not exist in supported list {}".format( - feature_type, [SensorIrradiance, SensorRadiance, SensorCamera] - ) - raise TypeError(msg) + match feature_type.__name__: + case "SensorIrradiance": + feature = SensorIrradiance( + project=self, + name=name, + description=description, + metadata=metadata, + ) + case "SensorRadiance": + feature = SensorRadiance( + project=self, + name=name, + description=description, + metadata=metadata, + ) + case "SensorCamera": + feature = SensorCamera( + project=self, + name=name, + description=description, + metadata=metadata, + ) + case "Sensor3DIrradiance": + feature = Sensor3DIrradiance( + project=self, + name=name, + description=description, + metadata=metadata, + ) + case _: + msg = "Requested feature {} does not exist in supported list {}".format( + feature_type, + [SensorIrradiance, SensorRadiance, SensorCamera, Sensor3DIrradiance], + ) + raise TypeError(msg) self._features.append(feature) return feature @@ -756,6 +781,8 @@ def _fill_features(self): op_feature._fill(mat_inst=mat_inst) for src_inst in scene_data.sources: + if src_inst.name in [_._name for _ in self._features]: + continue src_feat = None if src_inst.HasField("rayfile_properties"): src_feat = SourceRayFile( @@ -778,6 +805,14 @@ def _fill_features(self): source_instance=src_inst, default_values=False, ) + elif src_inst.HasField("ambient_properties"): + if src_inst.ambient_properties.HasField("natural_light_properties"): + src_feat = SourceAmbientNaturalLight( + project=self, + name=src_inst.name, + source_instance=src_inst, + default_values=False, + ) elif src_inst.HasField("thermic_properties"): src_feat = SourceThermic( project=self, @@ -789,6 +824,8 @@ def _fill_features(self): self._features.append(src_feat) for ssr_inst in scene_data.sensors: + if ssr_inst.name in [_._name for _ in self._features]: + continue ssr_feat = None if ssr_inst.HasField("irradiance_properties"): ssr_feat = SensorIrradiance( @@ -811,9 +848,18 @@ def _fill_features(self): sensor_instance=ssr_inst, default_values=False, ) + elif ssr_inst.HasField("irradiance_3d_properties"): + ssr_feat = Sensor3DIrradiance( + project=self, + name=ssr_inst.name, + sensor_instance=ssr_inst, + default_values=False, + ) self._features.append(ssr_feat) for sim_inst in scene_data.simulations: + if sim_inst.name in [_._name for _ in self._features]: + continue sim_feat = None simulation_template_link = self.client[sim_inst.simulation_guid].get() if simulation_template_link.HasField("direct_mc_simulation_template"): @@ -862,26 +908,6 @@ def __extract_part_mesh_info( """ import pyvista as pv - def local2absolute(local_vertice: np.ndarray, coordinates) -> np.ndarray: - """Convert local coordinate to global coordinate. - - Parameters - ---------- - local_vertice: np.ndarray - numpy array includes x, y, z info. - - Returns - ------- - np.ndarray - numpy array includes x, y, z info - - """ - global_origin = np.array(coordinates[:3]) - global_x = np.array(coordinates[3:6]) * local_vertice[0] - global_y = np.array(coordinates[6:9]) * local_vertice[1] - global_z = np.array(coordinates[9:]) * local_vertice[2] - return global_origin + global_x + global_y + global_z - part_coordinate = [ 0.0, 0.0, @@ -927,6 +953,7 @@ def _create_speos_feature_preview( SensorCamera, SensorRadiance, SensorIrradiance, + Sensor3DIrradiance, SourceLuminaire, SourceRayFile, SourceLuminaire, @@ -940,7 +967,7 @@ def _create_speos_feature_preview( plotter: Plotter ansys.tools.visualization_interface.Plotter speos_feature: Union[SensorCamera, SensorRadiance, SensorIrradiance, - SourceLuminaire, SourceRayFile, SourceLuminaire] + Sensor3DIrradiance, SourceLuminaire, SourceRayFile, SourceLuminaire] speos feature whose visual data will be added. scene_seize: float seize of max scene bounds @@ -956,6 +983,7 @@ def _create_speos_feature_preview( SensorIrradiance, SensorRadiance, SensorCamera, + Sensor3DIrradiance, SourceLuminaire, SourceRayFile, SourceSurface, @@ -1024,6 +1052,18 @@ def _create_preview(self, viz_args=None) -> Plotter: from ansys.tools.visualization_interface import Plotter + def find_all_subparts(target_part): + subparts = [] + current_subparts_found = target_part.find( + name=".*", name_regex=True, feature_type=part.Part.SubPart + ) + if not current_subparts_found: + return subparts + for subpart in current_subparts_found: + subparts.append(subpart) + subparts.extend(find_all_subparts(subpart)) + return subparts + if viz_args is None: viz_args = {} viz_args["show_edges"] = True @@ -1033,20 +1073,27 @@ def _create_preview(self, viz_args=None) -> Plotter: if self.scene_link.get().part_guid != "": _preview_mesh = pv.PolyData() # Retrieve root part - root_part_data = self.client[self.scene_link.get().part_guid].get() - - # Loop on all sub parts to retrieve their mesh - if len(root_part_data.parts) != 0: - for part_idx, part_item in enumerate(root_part_data.parts): - part_item_data = self.client[part_item.part_guid].get() - poly_data = self.__extract_part_mesh_info( - part_data=part_item_data, - part_coordinate_info=part_item.axis_system, - ) - if poly_data is not None: - _preview_mesh = _preview_mesh.append_polydata(poly_data) + root_part = self.find(name="", feature_type=part.Part)[0] + subparts = find_all_subparts(root_part) # all subpart + subparts = [ + subpart + for subpart in subparts + if any( + isinstance(_geo_feature, body.Body) for _geo_feature in subpart._geom_features + ) + ] # filter subpart which contains ansys.speos.core.body.Body + for subpart in subparts: + subpart_axis = subpart._part_instance.axis_system + subpart_guid = subpart._part_instance.part_guid + part_mesh_data = self.__extract_part_mesh_info( + part_data=self.client[subpart_guid].get(), + part_coordinate_info=subpart_axis, + ) + if part_mesh_data is not None: + _preview_mesh = _preview_mesh.append_polydata(part_mesh_data) # Add also the mesh of bodies directly contained in root part + root_part_data = self.client[self.scene_link.get().part_guid].get() poly_data = self.__extract_part_mesh_info(part_data=root_part_data) if poly_data is not None: _preview_mesh = _preview_mesh.append_polydata(poly_data) From 5466a49eac80501992ce16699598df75bc085eb8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 09:56:22 +0000 Subject: [PATCH 04/17] ci: auto fixes from pre-commit.com hooks. for more information, see https://pre-commit.ci --- src/ansys/speos/core/project.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ansys/speos/core/project.py b/src/ansys/speos/core/project.py index a7d95592c..4b5274992 100644 --- a/src/ansys/speos/core/project.py +++ b/src/ansys/speos/core/project.py @@ -159,7 +159,9 @@ def create_source( description: str = "", feature_type: type = SourceSurface, metadata: Optional[Mapping[str, str]] = None, - ) -> Union[SourceSurface, SourceRayFile, SourceLuminaire, SourceAmbientNaturalLight, SourceThermic]: + ) -> Union[ + SourceSurface, SourceRayFile, SourceLuminaire, SourceAmbientNaturalLight, SourceThermic + ]: """Create a new Source feature. Parameters From 6c6d0d7056c7ab7ecc2c87308b2c237ed76e588b Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Wed, 2 Jul 2025 10:00:30 +0000 Subject: [PATCH 05/17] chore: adding changelog file 651.added.md [dependabot-skip] --- doc/changelog.d/651.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/651.added.md diff --git a/doc/changelog.d/651.added.md b/doc/changelog.d/651.added.md new file mode 100644 index 000000000..06f3fe406 --- /dev/null +++ b/doc/changelog.d/651.added.md @@ -0,0 +1 @@ +Add thermic source \ No newline at end of file From 3a9263d397033238aa9405e23d607aa7b7acfd7c Mon Sep 17 00:00:00 2001 From: plu Date: Wed, 2 Jul 2025 11:02:57 +0100 Subject: [PATCH 06/17] run pre-commit to add missing docstring --- src/ansys/speos/core/source.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/ansys/speos/core/source.py b/src/ansys/speos/core/source.py index f9f05b5ea..fa7432cbd 100644 --- a/src/ansys/speos/core/source.py +++ b/src/ansys/speos/core/source.py @@ -1335,6 +1335,19 @@ def set_emissive_faces(self, geometries: List[tuple[GeoRef, bool]]) -> SourceThe return self def set_emissive_faces_temp(self, value: float = 2000) -> SourceThermic: + """Set existence constant temperature. + + Parameters + ---------- + value: float + temperature to be set on the emissive faces. + + Returns + ------- + ansys.speos.core.source.SourceThermic + Thermic source + + """ if not self._source_template.thermic.HasField("temperature_field"): self._source_template.thermic.emissives_faces.temperature = value return self From dcbb88af6acfe6079bce36937882f067d2398a69 Mon Sep 17 00:00:00 2001 From: plu Date: Wed, 2 Jul 2025 11:08:23 +0100 Subject: [PATCH 07/17] refactor the test code into example as temp location. use pathlib rather than os.path --- MyTestBench.py => examples/core/source_thermic_example_tmp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename MyTestBench.py => examples/core/source_thermic_example_tmp.py (86%) diff --git a/MyTestBench.py b/examples/core/source_thermic_example_tmp.py similarity index 86% rename from MyTestBench.py rename to examples/core/source_thermic_example_tmp.py index e863b803d..5ca61b2b0 100644 --- a/MyTestBench.py +++ b/examples/core/source_thermic_example_tmp.py @@ -1,11 +1,11 @@ -import os +from pathlib import Path import ansys.speos.core as core from ansys.speos.core.launcher import launch_local_speos_rpc_server import ansys.speos.core.source as source speos = launch_local_speos_rpc_server(version="252") -speos_file = os.path.join(os.getcwd(), "Speos Bench", "Inverse.1.1.speos", "Inverse.1.1.speos") +speos_file = Path.cwd() / "Speos Bench" / "Inverse.1.1.speos" / "Inverse.1.1.speos" project = core.Project(speos=speos, path=speos_file) mysource = project.find(name=".*", name_regex=True, feature_type=source.SourceThermic) mysource[0].set_emissive_faces_temp(value=100) From 7de324eb1d20224bf7879302497b87b8abcef8e0 Mon Sep 17 00:00:00 2001 From: plu Date: Thu, 3 Jul 2025 10:10:41 +0100 Subject: [PATCH 08/17] add key to the Intensity class to be filled --- src/ansys/speos/core/source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ansys/speos/core/source.py b/src/ansys/speos/core/source.py index fa7432cbd..9ad96a287 100644 --- a/src/ansys/speos/core/source.py +++ b/src/ansys/speos/core/source.py @@ -1301,7 +1301,7 @@ def __init__( self._intensity = Intensity( speos_client=self._speos_client, name=name + ".Intensity", - key="", + key=self._source_template.thermic.intensity_guid, ) if default_values: From ee721b9fd84b01e9b451d36cd32b0f697934bb81 Mon Sep 17 00:00:00 2001 From: plu Date: Thu, 3 Jul 2025 10:28:03 +0100 Subject: [PATCH 09/17] remove no need to check if it is temperature field --- src/ansys/speos/core/source.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ansys/speos/core/source.py b/src/ansys/speos/core/source.py index 9ad96a287..b687043cf 100644 --- a/src/ansys/speos/core/source.py +++ b/src/ansys/speos/core/source.py @@ -1348,8 +1348,7 @@ def set_emissive_faces_temp(self, value: float = 2000) -> SourceThermic: Thermic source """ - if not self._source_template.thermic.HasField("temperature_field"): - self._source_template.thermic.emissives_faces.temperature = value + self._source_template.thermic.emissives_faces.temperature = value return self # def commit(self) -> SourceThermic: From 76274b520c97f4b546666e165983fd2f4248da2a Mon Sep 17 00:00:00 2001 From: Elodie Chamblas Date: Thu, 3 Jul 2025 12:41:55 +0200 Subject: [PATCH 10/17] Fix issue with intensity template --- src/ansys/speos/core/source.py | 40 ++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/src/ansys/speos/core/source.py b/src/ansys/speos/core/source.py index b687043cf..fdd588ae0 100644 --- a/src/ansys/speos/core/source.py +++ b/src/ansys/speos/core/source.py @@ -1351,18 +1351,34 @@ def set_emissive_faces_temp(self, value: float = 2000) -> SourceThermic: self._source_template.thermic.emissives_faces.temperature = value return self - # def commit(self) -> SourceThermic: - # """Save feature: send the local data to the speos server database. - # - # Returns - # ------- - # ansys.speos.core.source.SourceSurface - # Source feature. - # """ - # - # # spectrum & source - # super().commit() - # return self + def commit(self) -> SourceThermic: + """Save feature: send the local data to the speos server database. + + Returns + ------- + ansys.speos.core.source.SourceThermic + Source feature. + """ + # intensity + self._intensity.commit() + self._source_template.thermic.intensity_guid = self._intensity.intensity_template_link.key + + # source base + super().commit() + return self + + def reset(self) -> SourceThermic: + """Reset feature: override local data by the one from the speos server database. + + Returns + ------- + ansys.speos.core.source.SourceThermic + Source feature. + """ + self._intensity.reset() + # source base + super().reset() + return self class BaseSourceAmbient(BaseSource): From e5f81321a322955e5c978eaea1023348f28ae7c0 Mon Sep 17 00:00:00 2001 From: plu Date: Thu, 3 Jul 2025 12:15:07 +0100 Subject: [PATCH 11/17] property and setter method --- src/ansys/speos/core/source.py | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/ansys/speos/core/source.py b/src/ansys/speos/core/source.py index fdd588ae0..6eb393635 100644 --- a/src/ansys/speos/core/source.py +++ b/src/ansys/speos/core/source.py @@ -1307,10 +1307,10 @@ def __init__( if default_values: # Default values self.set_emissive_faces(geometries=[]) - self.set_emissive_faces_temp(value=2000) + self.emissive_faces_temperature = 2000 def set_emissive_faces(self, geometries: List[tuple[GeoRef, bool]]) -> SourceThermic: - """Set existence constant. + """Set emssive faces for thermic source. Parameters ---------- @@ -1319,8 +1319,8 @@ def set_emissive_faces(self, geometries: List[tuple[GeoRef, bool]]) -> SourceThe Returns ------- - ansys.speos.core.source.SourceSurface - Surface source. + ansys.speos.core.source.ThermicSource + Thermic source. """ self._source_instance.thermic_properties.emissive_faces_properties.ClearField("geo_paths") if geometries != []: @@ -1334,22 +1334,36 @@ def set_emissive_faces(self, geometries: List[tuple[GeoRef, bool]]) -> SourceThe self._source_template.thermic.emissives_faces.SetInParent() return self - def set_emissive_faces_temp(self, value: float = 2000) -> SourceThermic: - """Set existence constant temperature. + @property + def emissive_faces_temperature(self) -> float: + """Get temperature settings for emissive faces. + + Returns + ------- + float + temperature settings for emissive faces. + + """ + if self._source_template.thermic.HasField("emissives_faces"): + return self._source_template.thermic.emissives_faces.temperature + else: + raise AttributeError("This feature is not defined as emissive faces.") + + @emissive_faces_temperature.setter + def emissive_faces_temperature(self, value: float) -> None: + """Set temperature settings for emissive faces. Parameters ---------- value: float - temperature to be set on the emissive faces. + temperature settings for emissive faces. Returns ------- - ansys.speos.core.source.SourceThermic - Thermic source + None """ self._source_template.thermic.emissives_faces.temperature = value - return self def commit(self) -> SourceThermic: """Save feature: send the local data to the speos server database. From 81f0f61cb3078c56ee3f04039f6aabe65682b56f Mon Sep 17 00:00:00 2001 From: plu Date: Thu, 3 Jul 2025 12:15:24 +0100 Subject: [PATCH 12/17] add unittest --- tests/core/test_source.py | 51 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/core/test_source.py b/tests/core/test_source.py index 3a679b110..69b061161 100644 --- a/tests/core/test_source.py +++ b/tests/core/test_source.py @@ -31,6 +31,7 @@ SourceLuminaire, SourceRayFile, SourceSurface, + SourceThermic, ) from tests.conftest import test_path @@ -525,6 +526,56 @@ def test_create_natural_light_source(speos: Speos): source2.delete() +def test_create_thermic_source(speos: Speos): + """Test creation of thermic source.""" + p = Project(speos=speos) + + source1 = SourceThermic( + p, + name="Thermmic Source", + ) + # Geometry + root_part = p.create_root_part().commit() + body_b1 = root_part.create_body(name="TheBodyB1").commit() + face_b1_f1 = ( + body_b1.create_face(name="TheFaceF1") + .set_vertices([0, 0, 0, 1, 0, 0, 0, 1, 0]) + .set_facets([0, 1, 2]) + .set_normals([0, 0, 1, 0, 0, 1, 0, 0, 1]) + .commit() + ) + # Optical Property + p.create_optical_property("OpaqueMirror50").set_volume_opaque().set_surface_mirror( + reflectance=50 + ).set_geometries(geometries=[body_b1.geo_path]).commit() + + source1.set_emissive_faces(geometries=[(face_b1_f1.geo_path, False)]) + source1.commit() + + assert source1._source_template.thermic.HasField("emissives_faces") + assert source1._source_template.thermic.emissives_faces.temperature == 2000 + assert source1._source_instance.HasField("thermic_properties") + assert source1._source_instance.thermic_properties.HasField("emissive_faces_properties") + assert ( + source1._source_instance.thermic_properties.emissive_faces_properties.geo_paths[0].geo_path + == "TheBodyB1/TheFaceF1" + ) + assert ( + source1._source_instance.thermic_properties.emissive_faces_properties.geo_paths[ + 0 + ].reverse_normal + is False + ) + + intensity = speos.client[source1.source_template_link.get().thermic.intensity_guid] + assert intensity.get().HasField("cos") + assert intensity.get().cos.N == 1 + + source1.emissive_faces_temperature = 3500 + assert source1._source_template.thermic.emissives_faces.temperature == 3500 + assert source1.emissive_faces_temperature == 3500 + + def test_keep_same_internal_feature(speos: Speos): """Test regarding source internal features (like spectrum, intensity). From 5ebe14b116b48f7e7c08a0626996e95db3de2394 Mon Sep 17 00:00:00 2001 From: plu Date: Thu, 3 Jul 2025 12:22:52 +0100 Subject: [PATCH 13/17] add example --- examples/core/source.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/examples/core/source.py b/examples/core/source.py index e30e77aca..965abd1b3 100644 --- a/examples/core/source.py +++ b/examples/core/source.py @@ -17,6 +17,7 @@ SourceLuminaire, SourceRayFile, SourceSurface, + SourceThermic, ) # - @@ -254,6 +255,22 @@ def create_face(body): source5.delete() # - +# ### Thermic source + +# + +source6 = p.create_source(name="Thermic.1", feature_type=SourceThermic) +source6.set_emissive_faces(geometries=[(GeoRef.from_native_link("TheBodyB/TheFaceF"), False)]) +print(source6) + +source6.commit() +print(source6) +# - + +# + +source6.delete() +# - + + # When creating sources, this creates some intermediate objects (spectrums, intensity templates). # # Deleting a source does not delete in cascade those objects From ff25390077d63df9e63452506907aff0f920b6b5 Mon Sep 17 00:00:00 2001 From: plu Date: Thu, 3 Jul 2025 12:26:35 +0100 Subject: [PATCH 14/17] improve the unittest coverage --- tests/core/test_source.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/core/test_source.py b/tests/core/test_source.py index 69b061161..c7a2deb4a 100644 --- a/tests/core/test_source.py +++ b/tests/core/test_source.py @@ -530,10 +530,7 @@ def test_create_thermic_source(speos: Speos): """Test creation of thermic source.""" p = Project(speos=speos) - source1 = SourceThermic( - p, - name="Thermmic Source", - ) + source1 = p.create_source(name="Thermic Source", feature_type=SourceThermic) # Geometry root_part = p.create_root_part().commit() body_b1 = root_part.create_body(name="TheBodyB1").commit() From ff5d3453c66463e07de9dc9f72fff411fe8b3cb8 Mon Sep 17 00:00:00 2001 From: Maxime Cailler Date: Fri, 5 Sep 2025 15:57:14 +0200 Subject: [PATCH 15/17] Finalized thermic source basic development (incl unit test) --- examples/core/source_thermic_example_tmp.py | 20 ---- src/ansys/speos/core/project.py | 2 +- src/ansys/speos/core/source.py | 104 +++++++++++++++++++- tests/core/test_source.py | 16 +++ 4 files changed, 116 insertions(+), 26 deletions(-) delete mode 100644 examples/core/source_thermic_example_tmp.py diff --git a/examples/core/source_thermic_example_tmp.py b/examples/core/source_thermic_example_tmp.py deleted file mode 100644 index 5ca61b2b0..000000000 --- a/examples/core/source_thermic_example_tmp.py +++ /dev/null @@ -1,20 +0,0 @@ -from pathlib import Path - -import ansys.speos.core as core -from ansys.speos.core.launcher import launch_local_speos_rpc_server -import ansys.speos.core.source as source - -speos = launch_local_speos_rpc_server(version="252") -speos_file = Path.cwd() / "Speos Bench" / "Inverse.1.1.speos" / "Inverse.1.1.speos" -project = core.Project(speos=speos, path=speos_file) -mysource = project.find(name=".*", name_regex=True, feature_type=source.SourceThermic) -mysource[0].set_emissive_faces_temp(value=100) -mysource[0].commit() -mysimu = project.find(name=".*", name_regex=True, feature_type=core.simulation.SimulationInverse) -mysimu = mysimu[0] -new_source = project.create_source(name="test", feature_type=source.SourceThermic) -new_source.commit() -print(project) - -# mysimu.compute_CPU() -# print(mysimu.result_list) diff --git a/src/ansys/speos/core/project.py b/src/ansys/speos/core/project.py index c9e1efa33..80deebc2c 100644 --- a/src/ansys/speos/core/project.py +++ b/src/ansys/speos/core/project.py @@ -236,7 +236,7 @@ def create_source( ) case _: msg = "Requested feature {} does not exist in supported list {}".format( - feature_type, [SourceSurface, SourceLuminaire, SourceRayFile] + feature_type, [SourceSurface, SourceLuminaire, SourceRayFile, SourceThermic] ) raise TypeError(msg) self._features.append(feature) diff --git a/src/ansys/speos/core/source.py b/src/ansys/speos/core/source.py index 6eb393635..94d3a3b62 100644 --- a/src/ansys/speos/core/source.py +++ b/src/ansys/speos/core/source.py @@ -45,6 +45,7 @@ from ansys.speos.core.kernel.client import SpeosClient from ansys.speos.core.kernel.scene import ProtoScene from ansys.speos.core.kernel.source_template import ProtoSourceTemplate +from ansys.speos.core.opt_prop import OptProp from ansys.speos.core.spectrum import Spectrum @@ -1304,12 +1305,17 @@ def __init__( key=self._source_template.thermic.intensity_guid, ) + self._sop = OptProp( + project=self._project, + name=self._name, + ) + if default_values: - # Default values self.set_emissive_faces(geometries=[]) self.emissive_faces_temperature = 2000 + self._sop.set_surface_mirror(0) - def set_emissive_faces(self, geometries: List[tuple[GeoRef, bool]]) -> SourceThermic: + def set_emissive_faces(self, geometries: List[tuple[Union[GeoRef, face.Face, body.Body], bool]]) -> SourceThermic: """Set emssive faces for thermic source. Parameters @@ -1319,7 +1325,7 @@ def set_emissive_faces(self, geometries: List[tuple[GeoRef, bool]]) -> SourceThe Returns ------- - ansys.speos.core.source.ThermicSource + ansys.speos.core.source.SourceThermic Thermic source. """ self._source_instance.thermic_properties.emissive_faces_properties.ClearField("geo_paths") @@ -1365,18 +1371,104 @@ def emissive_faces_temperature(self, value: float) -> None: """ self._source_template.thermic.emissives_faces.temperature = value + def set_temperature_field(self) -> SourceThermic: + """Set thermic source in temperature field mode. + + Parameters + ---------- + None + + Returns + ------- + ansys.speos.core.source.SourceThermic + Thermic source. + """ + if not self._source_template.thermic.HasField("temperature_field"): + self._source_template.thermic.temperature_field.SetInParent() + if self._source_instance.thermic_properties.temperature_field_properties.axis_plane is None: + axis_plane = [0, 0, 0, 1, 0, 0, 0, 1, 0] + self._source_instance.thermic_properties.temperature_field_properties.axis_plane[:] = axis_plane + return self + + @property + def temperature_field_uri(self) -> str: + """Get temperature field file uri. + + Returns + ------- + string + temperature field file uri. + + """ + if self._source_template.thermic.HasField("temperature_field"): + return self._source_template.thermic.temperature_field.temperature_field_uri + else: + raise AttributeError("This feature is not defined with temperature field.") + + def set_temperature_field_uri(self, file: str) -> None: + """Set temperature field file path. + + Parameters + ---------- + file: str + temperature field file path. + + Returns + ------- + None + + """ + self._source_template.thermic.temperature_field.temperature_field_uri = file + + @property + def sop(self) -> OptProp: + """Get SOP for thermic source in temperature field mode. + + Returns + ------- + ansys.speos.core.opt_prop.OptProp + Surface Optical property. + + """ + if self._source_template.thermic.HasField("temperature_field"): + return self._sop + else: + raise AttributeError("This feature is not defined with temperature field.") + + @sop.setter + def sop(self, new_sop: OptProp) -> None: + """Set SOP for thermic source in temperature field mode. + + Parameters + ---------- + ansys.speos.core.opt_prop.OptProp + Surface Optical property. + + Returns + ------- + None + + """ + self._sop = new_sop + self._source_template.thermic.temperature_field.sop_guid = self._sop.sop_template_link.key + def commit(self) -> SourceThermic: """Save feature: send the local data to the speos server database. Returns ------- ansys.speos.core.source.SourceThermic - Source feature. + Thermic source feature. """ # intensity self._intensity.commit() self._source_template.thermic.intensity_guid = self._intensity.intensity_template_link.key + # sop + if self._source_template.thermic.HasField("temperature_field"): + self._sop.commit() + self._source_template.thermic.temperature_field.sop_guid = self._sop.sop_template_link.key + # source base super().commit() return self @@ -1387,7 +1479,7 @@ def reset(self) -> SourceThermic: Returns ------- ansys.speos.core.source.SourceThermic - Source feature. + Thermic source feature. """ self._intensity.reset() # source base @@ -2085,3 +2177,5 @@ def set_sun_manual(self) -> BaseSourceAmbient.Manual: # Happens in case of feature reset (to be sure to always modify correct data) self._type._sun = natural_light_properties.sun_axis_system.manual_sun return self._type + + diff --git a/tests/core/test_source.py b/tests/core/test_source.py index c7a2deb4a..acb0f901d 100644 --- a/tests/core/test_source.py +++ b/tests/core/test_source.py @@ -629,9 +629,25 @@ def test_keep_same_internal_feature(speos: Speos): source3.commit() assert source3.source_template_link.get().rayfile.spectrum_guid == spectrum_guid + # THERMIC SOURCE + source4 = SourceThermic(project=p, name="Thermic.1") + source4.emissive_faces_temperature = 2000 + source4.commit() + + # Modify field type + source4.set_temperature_field() + uri = str(Path(test_path) / "dummy.OPTTemperatureField") + source4.set_temperature_field_uri(uri) + sop_guid = source4._source_template.thermic.temperature_field.sop_guid + assert source4._source_template.thermic.temperature_field.temperature_field_uri == uri + assert source4.temperature_field_uri == uri + assert source4._source_template.thermic.temperature_field.sop_guid == sop_guid + + source1.delete() source2.delete() source3.delete() + source4.delete() def test_commit_source(speos: Speos): From 2855c91a3749fd2185b8063e468683b3b55656e3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 13:57:35 +0000 Subject: [PATCH 16/17] ci: auto fixes from pre-commit.com hooks. for more information, see https://pre-commit.ci --- src/ansys/speos/core/source.py | 14 +++++++++----- tests/core/test_source.py | 1 - 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/ansys/speos/core/source.py b/src/ansys/speos/core/source.py index 94d3a3b62..f1e34d8e0 100644 --- a/src/ansys/speos/core/source.py +++ b/src/ansys/speos/core/source.py @@ -1315,7 +1315,9 @@ def __init__( self.emissive_faces_temperature = 2000 self._sop.set_surface_mirror(0) - def set_emissive_faces(self, geometries: List[tuple[Union[GeoRef, face.Face, body.Body], bool]]) -> SourceThermic: + def set_emissive_faces( + self, geometries: List[tuple[Union[GeoRef, face.Face, body.Body], bool]] + ) -> SourceThermic: """Set emssive faces for thermic source. Parameters @@ -1387,7 +1389,9 @@ def set_temperature_field(self) -> SourceThermic: self._source_template.thermic.temperature_field.SetInParent() if self._source_instance.thermic_properties.temperature_field_properties.axis_plane is None: axis_plane = [0, 0, 0, 1, 0, 0, 0, 1, 0] - self._source_instance.thermic_properties.temperature_field_properties.axis_plane[:] = axis_plane + self._source_instance.thermic_properties.temperature_field_properties.axis_plane[:] = ( + axis_plane + ) return self @property @@ -1467,7 +1471,9 @@ def commit(self) -> SourceThermic: # sop if self._source_template.thermic.HasField("temperature_field"): self._sop.commit() - self._source_template.thermic.temperature_field.sop_guid = self._sop.sop_template_link.key + self._source_template.thermic.temperature_field.sop_guid = ( + self._sop.sop_template_link.key + ) # source base super().commit() @@ -2177,5 +2183,3 @@ def set_sun_manual(self) -> BaseSourceAmbient.Manual: # Happens in case of feature reset (to be sure to always modify correct data) self._type._sun = natural_light_properties.sun_axis_system.manual_sun return self._type - - diff --git a/tests/core/test_source.py b/tests/core/test_source.py index acb0f901d..0fcf4fd26 100644 --- a/tests/core/test_source.py +++ b/tests/core/test_source.py @@ -643,7 +643,6 @@ def test_keep_same_internal_feature(speos: Speos): assert source4.temperature_field_uri == uri assert source4._source_template.thermic.temperature_field.sop_guid == sop_guid - source1.delete() source2.delete() source3.delete() From ae44b328228a57e3e040d28eb4c33187b8013c50 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Fri, 5 Sep 2025 13:58:31 +0000 Subject: [PATCH 17/17] chore: adding changelog file 651.added.md [dependabot-skip] --- doc/changelog.d/651.added.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changelog.d/651.added.md b/doc/changelog.d/651.added.md index 06f3fe406..1bfb6fb50 100644 --- a/doc/changelog.d/651.added.md +++ b/doc/changelog.d/651.added.md @@ -1 +1 @@ -Add thermic source \ No newline at end of file +Add thermic source