From fe961f549bb63e3e1ec76be74eccd1e79b0f57f9 Mon Sep 17 00:00:00 2001 From: plu Date: Sat, 25 Oct 2025 13:59:14 +0100 Subject: [PATCH 01/16] Add support VBB --- src/ansys/speos/core/simulation.py | 1039 ++++++++++++++++++++++++++++ 1 file changed, 1039 insertions(+) diff --git a/src/ansys/speos/core/simulation.py b/src/ansys/speos/core/simulation.py index 1d22399e9..1d17ea888 100644 --- a/src/ansys/speos/core/simulation.py +++ b/src/ansys/speos/core/simulation.py @@ -72,6 +72,164 @@ class BaseSimulation: This is a Super class, **Do not instantiate this class yourself** """ + class _SourceSampling: + """Source sampling mode. + + Parameters + ---------- + source_sampling : Union[ + simulation_template_pb2.RoughnessOnly, + simulation_template_pb2.Iridescence, + simulation_template_pb2.Isotropic, + simulation_template_pb2.Anisotropic] to complete. + stable_ctr : bool + Variable to indicate if usage is inside class scope + + Notes + ----- + **Do not instantiate this class yourself**, use set_weight method available in simulation + classes. + + """ + + class _Adaptive: + """Adaptive sampling mode. + + Parameters + ---------- + uniform : simulation_template_pb2.SourceSamplingAdaptive + uniform settings to complete. + stable_ctr : bool + Variable to indicate if usage is inside class scope + + """ + + def __init__( + self, adaptive: simulation_template_pb2.SourceSamplingAdaptive, stable_ctr: bool + ) -> None: + if not stable_ctr: + msg = "_Adaptive class instantiated outside the class scope" + raise RuntimeError(msg) + self._adaptive = adaptive + # Default setting + self.adaptive_uri = "" + + @property + def adaptive_uri(self) -> str: + """Get adaptive uri. + + Returns + ------- + str: + Adaptive sampling file uri. + + """ + return self._adaptive.file_uri + + @adaptive_uri.setter + def adaptive_uri(self, uri: Union[Path | str]) -> None: + """Set adaptive uri. + + Parameters + ---------- + uri: Union[Path | str] + Adaptive sampling file uri. + + Returns + ------- + None + + """ + self._adaptive.file_uri = str(uri) + + class _Uniform: + """Uniform sampling mode. + + Parameters + ---------- + uniform : simulation_template_pb2.SourceSamplingUniformIsotropic to complete. + stable_ctr : bool + Variable to indicate if usage is inside class scope + + """ + + def __init__( + self, + uniform: simulation_template_pb2.SourceSamplingUniformIsotropic, + stable_ctr: bool, + ) -> None: + if not stable_ctr: + msg = "_Uniform class instantiated outside the class scope" + raise RuntimeError(msg) + self._uniform = uniform + # Default setting + self.theta_sampling = 2 + + @property + def theta_sampling(self) -> int: + """Get theta sampling. + + Returns + ------- + int + theta sampling. + """ + return self._uniform.theta_sampling + + @theta_sampling.setter + def theta_sampling(self, theta_sampling: int) -> None: + """Set theta sampling. + + Parameters + ---------- + theta_sampling: int + theta sampling. + + Returns + ------- + None + + """ + self._uniform.theta_sampling = theta_sampling + + def __init__( + self, + source_sampling: Union[ + simulation_template_pb2.RoughnessOnly, + simulation_template_pb2.Iridescence, + simulation_template_pb2.Isotropic, + simulation_template_pb2.Anisotropic, + ], + stable_ctr: bool = False, + ) -> None: + if not stable_ctr: + msg = "_SourceSampling class instantiated outside of the class scope" + raise RuntimeError(msg) + self._source_sampling = source_sampling + + def set_uniform(self) -> BaseSimulation._SourceSampling._Uniform: + """Set uniform type of source sampling. + + Returns + ------- + BaseSimulation._SourceSampling._Uniform + + """ + return self._Uniform( + self._source_sampling.uniform_isotropic, + stable_ctr=True, + ) + + def set_adaptive(self) -> BaseSimulation._SourceSampling._Adaptive: + """Set adaptive type of source sampling. + + Returns + ------- + BaseSimulation._SourceSampling._Adaptive + + """ + return self._Adaptive(self._source_sampling.adaptive, stable_ctr=True) + class Weight: """The Weight represents the ray energy. @@ -1575,3 +1733,884 @@ def set_impact_report(self, value: bool = False) -> SimulationInteractive: """ self._job.interactive_simulation_properties.impact_report = value return self + + +class SimulationVirtualBSDF(BaseSimulation): + """Type of simulation : Virtual BSDF Bench. + + By default, + geometry distance tolerance is set to 0.01, + maximum number of impacts is set to 100, + a colorimetric standard is set to CIE 1931, + and weight's minimum energy percentage is set to 0.005. + + Parameters + ---------- + project : ansys.speos.core.project.Project + Project in which simulation shall be created. + name : str + Name of the simulation. + description : str + Description of the Simulation. + By default, ``""``. + metadata : Optional[Mapping[str, str]] + Metadata of the feature. + By default, ``{}``. + simulation_instance : ansys.api.speos.scene.v2.scene_pb2.Scene.SimulationInstance, optional + Simulation instance to provide if the feature does not have to be created from scratch + By default, ``None``, means that the feature is created from scratch by default. + default_values : bool + Uses default values when True. + """ + + class RoughnessOnly(BaseSimulation._SourceSampling): + """Roughness only mode of BSDF bench measurement. + + By default, + 2 degrees uniform type sampling is set + + Parameters + ---------- + mode_template : simulation_template_pb2.RoughnessOnly + roughness settings to complete. + stable_ctr : bool + Variable to indicate if usage is inside class scope + """ + + def __init__( + self, + mode_template: simulation_template_pb2.RoughnessOnly, + stable_ctr: bool = False, + ) -> None: + if not stable_ctr: + msg = "RoughnessOnly class instantiated outside of class scope" + raise RuntimeError(msg) + super().__init__(source_sampling=mode_template, stable_ctr=True) + + class AllCharacteristics: + """BSDF depends on all properties mode of BSDF bench measurement. + + By default, + is_bsdf180 is true + reflection_and_transmission is true + Color does not depend on viewing direction is set + Source sampling is set to be isotropic + + Parameters + ---------- + mode_template : simulation_template_pb2.AllCharacteristics + all properties dependent BSDF settings to complete. + stable_ctr : bool + Variable to indicate if usage is inside class scope + """ + + class Iridescence(BaseSimulation._SourceSampling): + """Color depends on viewing direction of BSDF measurement settings. + + By default, + 2 degrees uniform type sampling is set + + Parameters + ---------- + mode_template : simulation_template_pb2.Iridescence + Iridescence settings to complete. + stable_ctr : bool + Variable to indicate if usage is inside class scope + """ + + def __init__( + self, + iridescence_mode: simulation_template_pb2.Iridescence, + stable_ctr: bool = False, + ) -> None: + if not stable_ctr: + msg = "AllCharacteristics class instantiated outside of class scope" + raise RuntimeError(msg) + super().__init__(source_sampling=iridescence_mode, stable_ctr=True) + + class NonIridescence: + """Color does not depend on viewing direction of BSDF measurement settings. + + By default, + 2 degrees set_isotropic uniform type source sampling is set + + Parameters + ---------- + non_iridescence_mode : simulation_template_pb2.NoIridescence + NonIridescence settings to complete. + stable_ctr : bool + Variable to indicate if usage is inside class scope + """ + + class Isotropic(BaseSimulation._SourceSampling): + """Uniform Isotropic source sampling. + + By default, + 2 degrees uniform type source sampling is set + + Parameters + ---------- + non_iridescence_isotropic : simulation_template_pb2.Isotropic + Isotropic settings to complete. + stable_ctr : bool + Variable to indicate if usage is inside class scope + """ + + def __init__( + self, + non_iridescence_isotropic: simulation_template_pb2.Isotropic, + stable_ctr: bool = False, + ): + if not stable_ctr: + msg = "Isotropic class instantiated outside of class scope" + raise RuntimeError(msg) + super().__init__(source_sampling=non_iridescence_isotropic, stable_ctr=True) + + class Anisotropic(BaseSimulation._SourceSampling): + """Anisotropic source sampling. + + Parameters + ---------- + non_iridescence_anisotropic : simulation_template_pb2.Anisotropic + Anisotropic settings to complete. + stable_ctr : bool + Variable to indicate if usage is inside class scope + """ + + def __init__( + self, + non_iridescence_anisotropic: simulation_template_pb2.Anisotropic, + stable_ctr: bool = False, + ): + if not stable_ctr: + msg = "Anisotropic class instantiated outside of class scope" + raise RuntimeError(msg) + super().__init__(source_sampling=non_iridescence_anisotropic, stable_ctr=True) + + class _Uniform: + """Anisotorpic Uniform sampling mode. + + Parameters + ---------- + uniform : simulation_template_pb2.SourceSamplingUniformAnisotropic to complete. + stable_ctr : bool + Variable to indicate if usage is inside class scope + + """ + + def __init__( + self, + uniform: simulation_template_pb2.SourceSamplingUniformAnisotropic, + stable_ctr: bool = False, + ) -> None: + if not stable_ctr: + msg = "_Uniform class instantiated outside of class scope" + raise RuntimeError(msg) + self._uniform = uniform + + @property + def theta_sampling(self) -> int: + """Get theta_sampling. + + Returns + ------- + int + theta sampling. + + """ + return self._uniform.theta_sampling + + @theta_sampling.setter + def theta_sampling(self, theta_sampling: int) -> None: + """Set theta_sampling. + + Parameters + ---------- + theta_sampling: int + theta sampling. + + Returns + ------- + None + + """ + self._uniform.theta_sampling = theta_sampling + + @property + def phi_sampling(self) -> int: + """Get phi_sampling. + + Returns + ------- + int + phi sampling. + + """ + return self._uniform.phi_sampling + + @phi_sampling.setter + def phi_sampling(self, phi_sampling: int) -> None: + """Set phi_sampling. + + Parameters + ---------- + phi_sampling: int + phi sampling. + + Returns + ------- + None + + """ + self._uniform.phi_sampling = phi_sampling + + def set_symmetric_unspecified( + self, + ) -> ( + SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Anisotropic._Uniform + ): + """Set symmetric type as unspecified. + + Returns + ------- + SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Anisotropic._Uniform + + """ + self._uniform.symmetry_type = 0 + return self + + def set_semmetric_none( + self, + ) -> ( + SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Anisotropic._Uniform + ): + """Set symmetric type as non-specified. + + Returns + ------- + SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Anisotropic._Uniform + + """ + self._uniform.symmetry_type = 1 + return self + + def set_symmetric_1_plane_symmetric( + self, + ) -> ( + SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Anisotropic._Uniform + ): + """Set symmetric type as plane symmetric. + + Returns + ------- + SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Anisotropic._Uniform + + """ + self._uniform.symmetry_type = 2 + return self + + def set_symmetric_2_plane_symmetric( + self, + ) -> ( + SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Anisotropic._Uniform + ): + """Set symmetric type as 2 planes symmetric. + + Returns + ------- + SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Anisotropic._Uniform + + """ + self._uniform.symmetry_type = 3 + return self + + def set_uniform( + self, + ) -> SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Anisotropic._Uniform: + """Set anisotropic uniform type. + + Returns + ------- + SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Anisotropic._Uniform + + """ + return self._Uniform(self._source_sampling.uniform_anisotropic, stable_ctr=True) + + def __init__( + self, + non_iridescence_mode, + stable_ctr: bool = False, + ): + if not stable_ctr: + msg = "NonIridescence class instantiated outside of class scope" + raise RuntimeError(msg) + self._non_iridescence = non_iridescence_mode + self.set_isotropic() + + def set_isotropic( + self, + ) -> SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Isotropic: + """Set isotropic type of uniform source. + + Returns + ------- + SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Isotropic + Isotropic source settings + + """ + return self.Isotropic( + self._non_iridescence.isotropic, + stable_ctr=True, + ) + + def set_anisotropic( + self, + ) -> SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Anisotropic: + """Set anisotropic type of uniform source. + + Returns + ------- + SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Anisotropic + Anisotropic source settings + + """ + return self.Anisotropic( + self._non_iridescence.anisotropic, + stable_ctr=True, + ) + + def __init__( + self, + mode_template: simulation_template_pb2.VirtualBSDFBench, + stable_ctr: bool = False, + ) -> None: + if not stable_ctr: + msg = "AllCharacteristics class instantiated outside of class scope" + raise RuntimeError(msg) + self._mode = mode_template + # Default values + self.is_bsdf180 = True + self.reflection_and_transmission = True + self.set_non_iridescence() + + @property + def is_bsdf180(self) -> bool: + """Get settings if bsdf is bsdf180. + + Returns + ------- + bool + True if bsdf180 is to be generated, False otherwise. + + """ + return self._mode.is_bsdf180 + + @is_bsdf180.setter + def is_bsdf180(self, value: bool) -> None: + """Set settings if bsdf180. + + Parameters + ---------- + value: bool + True if bsdf180 is to be generated, False otherwise. + + Returns + ------- + None + + """ + self._mode.is_bsdf180 = value + + @property + def reflection_and_transmission(self) -> bool: + """Get settings if reflection and transmission is to be generated. + + Returns + ------- + bool + True if reflection and transmission is to be generated, False otherwise. + + """ + return self._mode.sensor_reflection_and_transmission + + @reflection_and_transmission.setter + def reflection_and_transmission(self, value: bool) -> None: + """Set settings if reflection and transmission is to be generated. + + Parameters + ---------- + value: bool + True if reflection and transmission is to be generated, False otherwise. + + Returns + ------- + None + + """ + self._mode.sensor_reflection_and_transmission = value + + def set_non_iridescence(self) -> SimulationVirtualBSDF.AllCharacteristics.NonIridescence: + """Set bsdf color does not depend on viewing direction. + + Returns + ------- + SimulationVirtualBSDF.AllCharacteristics.NonIridescence + NonIridescence settings to be complete + """ + return self.NonIridescence( + non_iridescence_mode=self._mode.no_iridescence, + stable_ctr=True, + ) + + def set_iridescence(self) -> SimulationVirtualBSDF.AllCharacteristics.Iridescence: + """Set bsdf color depends on viewing direction. + + Returns + ------- + SimulationVirtualBSDF.AllCharacteristics.Iridescence + Iridescence settings to be complete + """ + return self.Iridescence( + iridescence_mode=self._mode.iridescence, + stable_ctr=True, + ) + + class WavelengthsRange: + """Range of wavelengths. + + By default, a range from 400nm to 700nm is chosen, with a sampling of 13. + + Parameters + ---------- + wavelengths_range : ansys.api.speos.sensor.v1.common_pb2.WavelengthsRange + Wavelengths range protobuf object to modify. + default_values : bool + Uses default values when True. + stable_ctr : bool + Variable to indicate if usage is inside class scope + + Notes + ----- + **Do not instantiate this class yourself**, use set_wavelengths_range method available in + sensor classes. + """ + + def __init__( + self, + wavelengths_range, + default_values: bool = True, + stable_ctr: bool = False, + ) -> None: + if not stable_ctr: + msg = "WavelengthsRange class instantiated outside of class scope" + raise RuntimeError(msg) + self._wavelengths_range = wavelengths_range + + if default_values: + # Default values + self.set_start().set_end().set_sampling() + + @property + def start(self) -> float: + """Return start wavelength. + + Returns + ------- + float + Start wavelength. + + """ + return self._wavelengths_range.w_start + + @start.setter + def start(self, value: float) -> None: + """Set start wavelength. + + Parameters + ---------- + value: float + Start wavelength. + + Returns + ------- + None + """ + self._wavelengths_range.w_start = value + + @property + def end(self) -> float: + """ + Return end wavelength. + + Returns + ------- + float + End wavelength. + + """ + return self._wavelengths_range.w_end + + @end.setter + def end(self, value: float) -> None: + """ + Set end wavelength. + + Parameters + ---------- + value: float + End wavelength. + + Returns + ------- + None + + """ + self._wavelengths_range.w_end = value + + @property + def sampling(self) -> int: + """ + Return sampling. + + Returns + ------- + int + Wavelength sampling. + + """ + return self._wavelengths_range.w_sampling + + @sampling.setter + def sampling(self, value: int) -> None: + """ + Set sampling. + + Parameters + ---------- + value: int + wavelength sampling. + + Returns + ------- + None + + """ + self._wavelengths_range.w_sampling = value + + class SensorUniform: + """BSDF bench sensor settings.""" + + def __init__( + self, + sensor_uniform_mode, + stable_ctr: bool = False, + ): + if not stable_ctr: + msg = "SensorUniform class instantiated outside of class scope" + raise RuntimeError(msg) + self._sensor_uniform_mode = sensor_uniform_mode + + @property + def theta_sampling(self) -> int: + """Get theta sampling. + + Returns + ------- + int + theta sampling. + + """ + return self._sensor_uniform_mode.theta_sampling + + @theta_sampling.setter + def theta_sampling(self, value: int) -> None: + """ + Set theta sampling. + + Parameters + ---------- + value: int + theta sampling. + + Returns + ------- + None + + """ + self._sensor_uniform_mode.theta_sampling = value + + @property + def phi_sampling(self) -> int: + """ + Get phi sampling. + + Returns + ------- + int + phi sampling. + + """ + return self._sensor_uniform_mode.phi_sampling + + @phi_sampling.setter + def phi_sampling(self, value: int) -> None: + """ + Set phi sampling. + + Parameters + ---------- + value: int + phi sampling. + + Returns + ------- + None + + """ + self._sensor_uniform_mode.phi_sampling = value + + def __init__( + self, + project: project.Project, + name: str, + description: str = "", + metadata: Optional[Mapping[str, str]] = None, + simulation_instance: Optional[ProtoScene.SimulationInstance] = None, + default_values: bool = True, + ) -> None: + if metadata is None: + metadata = {} + + super().__init__( + project=project, + name=name, + description=description, + metadata=metadata, + simulation_instance=simulation_instance, + ) + + self._wavelengths_range = self.set_wavelengths_range() + self._sensor_sampling_mode = self.set_sensor_sampling_uniform() + + if default_values: + self.geom_distance_tolerance = 0.01 + self.max_impact = 100 + self.set_weight() + self.set_colorimetric_standard_CIE_1931() + + @property + def geom_distance_tolerance(self) -> float: + """Return the geometry distance tolerance. + + Returns + ------- + float + Maximum distance in mm to consider two faces as tangent. + """ + return ( + self._simulation_template.virtual_bsdf_bench_simulation_template.geom_distance_tolerance + ) + + @geom_distance_tolerance.setter + def geom_distance_tolerance(self, value: float) -> None: + """Set the geometry distance tolerance. + + Parameters + ---------- + value : float + Maximum distance in mm to consider two faces as tangent. + By default, ``0.01`` + + Returns + ------- + None + """ + self._simulation_template.virtual_bsdf_bench_simulation_template.geom_distance_tolerance = ( + value + ) + + @property + def max_impact(self) -> int: + """Return the maximum number of impacts. + + Returns + ------- + int + The maximum number of impacts. + """ + return self._simulation_template.virtual_bsdf_bench_simulation_template.max_impact + + @max_impact.setter + def max_impact(self, value: int) -> None: + """Define a value to determine the maximum number of ray impacts during propagation. + + When a ray has interacted N times with the geometry, the propagation of the ray stops. + + Parameters + ---------- + value : int + The maximum number of impacts. + By default, ``100``. + + Returns + ------- + None + """ + self._simulation_template.virtual_bsdf_bench_simulation_template.max_impact = value + + @property + def integration_angle(self) -> float: + """Return the sensor integration angle. + + Returns + ------- + float + The sensor integration angle. + + """ + tmp_sensor = self._simulation_template.virtual_bsdf_bench_simulation_template.sensor + return tmp_sensor.integration_angle + + @integration_angle.setter + def integration_angle(self, angle: float) -> None: + """Set the sensor integration angle. + + Parameters + ---------- + angle: float + The sensor integration angle. + + Returns + ------- + None + + """ + tmp_sensor = self._simulation_template.virtual_bsdf_bench_simulation_template.sensor + tmp_sensor.integration_angle = angle + + def set_weight(self) -> BaseSimulation.Weight: + """Activate weight. Highly recommended to fill. + + Returns + ------- + ansys.speos.core.simulation.BaseSimulation.Weight + Simulation.Weight + """ + return BaseSimulation.Weight( + self._simulation_template.virtual_bsdf_bench_simulation_template.weight, + stable_ctr=True, + ) + + def set_weight_none(self) -> SimulationVirtualBSDF: + """Deactivate weight. + + Returns + ------- + ansys.speos.core.simulation.SimulationVirtualBSDF + Inverse simulation + """ + self._simulation_template.virtual_bsdf_bench_simulation_template.ClearField("weight") + return self + + def set_colorimetric_standard_CIE_1931(self) -> SimulationVirtualBSDF: + """Set the colorimetric standard to CIE 1931. + + 2 degrees CIE Standard Colorimetric Observer Data. + + Returns + ------- + ansys.speos.core.simulation.SimulationVirtualBSDF + Inverse simulation + """ + self._simulation_template.virtual_bsdf_bench_simulation_template.colorimetric_standard = ( + simulation_template_pb2.CIE_1931 + ) + return self + + def set_colorimetric_standard_CIE_1964(self) -> SimulationVirtualBSDF: + """Set the colorimetric standard to CIE 1964. + + 10 degrees CIE Standard Colorimetric Observer Data. + + Returns + ------- + ansys.speos.core.simulation.SimulationVirtualBSDF + Inverse simulation + """ + self._simulation_template.virtual_bsdf_bench_simulation_template.colorimetric_standard = ( + simulation_template_pb2.CIE_1964 + ) + return self + + def set_mode_roughness_only(self) -> SimulationVirtualBSDF.RoughnessOnly: + """Set BSDF depends on surface roughness only. + + Returns + ------- + SimulationVirtualBSDF.RoughnessOnly + roughness only settings + """ + return SimulationVirtualBSDF.RoughnessOnly( + self._simulation_template.virtual_bsdf_bench_simulation_template.roughness_only, + stable_ctr=True, + ) + + def set_mode_all_characteristics(self) -> SimulationVirtualBSDF.AllCharacteristics: + """Set BSDF depends on all properties. + + Returns + ------- + SimulationVirtualBSDF.AllCharacteristics + all properties settings + """ + return SimulationVirtualBSDF.AllCharacteristics( + self._simulation_template.virtual_bsdf_bench_simulation_template.all_characteristics, + stable_ctr=True, + ) + + def set_wavelengths_range(self) -> SimulationVirtualBSDF.WavelengthsRange: + """Set the range of wavelengths. + + Returns + ------- + ansys.speos.core.sensor.BaseSensor.WavelengthsRange + Wavelengths range. + """ + if self._wavelengths_range._wavelengths_range is not ( + self._simulation_template.virtual_bsdf_bench_simulation_template.wavelengths_range + ): + # Happens in case of feature reset (to be sure to always modify correct data) + self._wavelengths_range._wavelengths_range = ( + self._simulation_template.virtual_bsdf_bench_simulation_template.wavelengths_range + ) + return self._wavelengths_range + + def set_sensor_sampling_uniform(self) -> SimulationVirtualBSDF.SensorUniform: + """Set sensor sampling uniform. + + Returns + ------- + ansys.speos.core.sensor.BaseSensor.SensorUniform + uniform type of sensor settings + + """ + if ( + self._sensor_sampling_mode._sensor_uniform_mode + is not self._simulation_template.virtual_bsdf_bench_simulation_template.sensor + ): + # Happens in case of feature reset (to be sure to always modify correct data) + self._sensor_sampling_mode._sensor_uniform_mode = ( + self._simulation_template.virtual_bsdf_bench_simulation_template.sensor + ) + return self._sensor_sampling_mode + + def set_sensor_sampling_automatic(self) -> SimulationVirtualBSDF: + """Set sensor sampling automatic. + + Returns + ------- + SimulationVirtualBSDF + + """ + self._simulation_template.virtual_bsdf_bench_simulation_template.sensor.automatic.SetInParent() + return self From e17e8d576d606678dc51813da20d59cbea56d78a Mon Sep 17 00:00:00 2001 From: plu Date: Sat, 25 Oct 2025 13:59:14 +0100 Subject: [PATCH 02/16] Add support VBB --- src/ansys/speos/core/simulation.py | 1124 ++++++++++++++++++++++++++++ 1 file changed, 1124 insertions(+) diff --git a/src/ansys/speos/core/simulation.py b/src/ansys/speos/core/simulation.py index 1d22399e9..3e7618b0e 100644 --- a/src/ansys/speos/core/simulation.py +++ b/src/ansys/speos/core/simulation.py @@ -72,6 +72,164 @@ class BaseSimulation: This is a Super class, **Do not instantiate this class yourself** """ + class _SourceSampling: + """Source sampling mode. + + Parameters + ---------- + source_sampling : Union[ + simulation_template_pb2.RoughnessOnly, + simulation_template_pb2.Iridescence, + simulation_template_pb2.Isotropic, + simulation_template_pb2.Anisotropic] to complete. + stable_ctr : bool + Variable to indicate if usage is inside class scope + + Notes + ----- + **Do not instantiate this class yourself**, use set_weight method available in simulation + classes. + + """ + + class _Adaptive: + """Adaptive sampling mode. + + Parameters + ---------- + uniform : simulation_template_pb2.SourceSamplingAdaptive + uniform settings to complete. + stable_ctr : bool + Variable to indicate if usage is inside class scope + + """ + + def __init__( + self, adaptive: simulation_template_pb2.SourceSamplingAdaptive, stable_ctr: bool + ) -> None: + if not stable_ctr: + msg = "_Adaptive class instantiated outside the class scope" + raise RuntimeError(msg) + self._adaptive = adaptive + # Default setting + self.adaptive_uri = "" + + @property + def adaptive_uri(self) -> str: + """Get adaptive uri. + + Returns + ------- + str: + Adaptive sampling file uri. + + """ + return self._adaptive.file_uri + + @adaptive_uri.setter + def adaptive_uri(self, uri: Union[Path | str]) -> None: + """Set adaptive uri. + + Parameters + ---------- + uri: Union[Path | str] + Adaptive sampling file uri. + + Returns + ------- + None + + """ + self._adaptive.file_uri = str(uri) + + class _Uniform: + """Uniform sampling mode. + + Parameters + ---------- + uniform : simulation_template_pb2.SourceSamplingUniformIsotropic to complete. + stable_ctr : bool + Variable to indicate if usage is inside class scope + + """ + + def __init__( + self, + uniform: simulation_template_pb2.SourceSamplingUniformIsotropic, + stable_ctr: bool, + ) -> None: + if not stable_ctr: + msg = "_Uniform class instantiated outside the class scope" + raise RuntimeError(msg) + self._uniform = uniform + # Default setting + self.theta_sampling = 2 + + @property + def theta_sampling(self) -> int: + """Get theta sampling. + + Returns + ------- + int + theta sampling. + """ + return self._uniform.theta_sampling + + @theta_sampling.setter + def theta_sampling(self, theta_sampling: int) -> None: + """Set theta sampling. + + Parameters + ---------- + theta_sampling: int + theta sampling. + + Returns + ------- + None + + """ + self._uniform.theta_sampling = theta_sampling + + def __init__( + self, + source_sampling: Union[ + simulation_template_pb2.RoughnessOnly, + simulation_template_pb2.Iridescence, + simulation_template_pb2.Isotropic, + simulation_template_pb2.Anisotropic, + ], + stable_ctr: bool = False, + ) -> None: + if not stable_ctr: + msg = "_SourceSampling class instantiated outside of the class scope" + raise RuntimeError(msg) + self._source_sampling = source_sampling + + def set_uniform(self) -> BaseSimulation._SourceSampling._Uniform: + """Set uniform type of source sampling. + + Returns + ------- + BaseSimulation._SourceSampling._Uniform + + """ + return self._Uniform( + self._source_sampling.uniform_isotropic, + stable_ctr=True, + ) + + def set_adaptive(self) -> BaseSimulation._SourceSampling._Adaptive: + """Set adaptive type of source sampling. + + Returns + ------- + BaseSimulation._SourceSampling._Adaptive + + """ + return self._Adaptive(self._source_sampling.adaptive, stable_ctr=True) + class Weight: """The Weight represents the ray energy. @@ -1575,3 +1733,969 @@ def set_impact_report(self, value: bool = False) -> SimulationInteractive: """ self._job.interactive_simulation_properties.impact_report = value return self + + +class SimulationVirtualBSDF(BaseSimulation): + """Type of simulation : Virtual BSDF Bench. + + By default, + geometry distance tolerance is set to 0.01, + maximum number of impacts is set to 100, + a colorimetric standard is set to CIE 1931, + and weight's minimum energy percentage is set to 0.005. + + Parameters + ---------- + project : ansys.speos.core.project.Project + Project in which simulation shall be created. + name : str + Name of the simulation. + description : str + Description of the Simulation. + By default, ``""``. + metadata : Optional[Mapping[str, str]] + Metadata of the feature. + By default, ``{}``. + simulation_instance : ansys.api.speos.scene.v2.scene_pb2.Scene.SimulationInstance, optional + Simulation instance to provide if the feature does not have to be created from scratch + By default, ``None``, means that the feature is created from scratch by default. + default_values : bool + Uses default values when True. + """ + + class RoughnessOnly(BaseSimulation._SourceSampling): + """Roughness only mode of BSDF bench measurement. + + By default, + 2 degrees uniform type sampling is set + + Parameters + ---------- + mode_template : simulation_template_pb2.RoughnessOnly + roughness settings to complete. + stable_ctr : bool + Variable to indicate if usage is inside class scope + """ + + def __init__( + self, + mode_template: simulation_template_pb2.RoughnessOnly, + stable_ctr: bool = False, + ) -> None: + if not stable_ctr: + msg = "RoughnessOnly class instantiated outside of class scope" + raise RuntimeError(msg) + super().__init__(source_sampling=mode_template, stable_ctr=True) + + class AllCharacteristics: + """BSDF depends on all properties mode of BSDF bench measurement. + + By default, + is_bsdf180 is true + reflection_and_transmission is true + Color does not depend on viewing direction is set + Source sampling is set to be isotropic + + Parameters + ---------- + mode_template : simulation_template_pb2.AllCharacteristics + all properties dependent BSDF settings to complete. + stable_ctr : bool + Variable to indicate if usage is inside class scope + """ + + class Iridescence(BaseSimulation._SourceSampling): + """Color depends on viewing direction of BSDF measurement settings. + + By default, + 2 degrees uniform type sampling is set + + Parameters + ---------- + mode_template : simulation_template_pb2.Iridescence + Iridescence settings to complete. + stable_ctr : bool + Variable to indicate if usage is inside class scope + """ + + def __init__( + self, + iridescence_mode: simulation_template_pb2.Iridescence, + stable_ctr: bool = False, + ) -> None: + if not stable_ctr: + msg = "AllCharacteristics class instantiated outside of class scope" + raise RuntimeError(msg) + super().__init__(source_sampling=iridescence_mode, stable_ctr=True) + + class NonIridescence: + """Color does not depend on viewing direction of BSDF measurement settings. + + By default, + 2 degrees set_isotropic uniform type source sampling is set + + Parameters + ---------- + non_iridescence_mode : simulation_template_pb2.NoIridescence + NonIridescence settings to complete. + stable_ctr : bool + Variable to indicate if usage is inside class scope + """ + + class Isotropic(BaseSimulation._SourceSampling): + """Uniform Isotropic source sampling. + + By default, + 2 degrees uniform type source sampling is set + + Parameters + ---------- + non_iridescence_isotropic : simulation_template_pb2.Isotropic + Isotropic settings to complete. + stable_ctr : bool + Variable to indicate if usage is inside class scope + """ + + def __init__( + self, + non_iridescence_isotropic: simulation_template_pb2.Isotropic, + stable_ctr: bool = False, + ): + if not stable_ctr: + msg = "Isotropic class instantiated outside of class scope" + raise RuntimeError(msg) + super().__init__(source_sampling=non_iridescence_isotropic, stable_ctr=True) + + class Anisotropic(BaseSimulation._SourceSampling): + """Anisotropic source sampling. + + Parameters + ---------- + non_iridescence_anisotropic : simulation_template_pb2.Anisotropic + Anisotropic settings to complete. + stable_ctr : bool + Variable to indicate if usage is inside class scope + """ + + def __init__( + self, + non_iridescence_anisotropic: simulation_template_pb2.Anisotropic, + stable_ctr: bool = False, + ): + if not stable_ctr: + msg = "Anisotropic class instantiated outside of class scope" + raise RuntimeError(msg) + super().__init__(source_sampling=non_iridescence_anisotropic, stable_ctr=True) + + class _Uniform: + """Anisotorpic Uniform sampling mode. + + Parameters + ---------- + uniform : simulation_template_pb2.SourceSamplingUniformAnisotropic to complete. + stable_ctr : bool + Variable to indicate if usage is inside class scope + + """ + + def __init__( + self, + uniform: simulation_template_pb2.SourceSamplingUniformAnisotropic, + stable_ctr: bool = False, + ) -> None: + if not stable_ctr: + msg = "_Uniform class instantiated outside of class scope" + raise RuntimeError(msg) + self._uniform = uniform + + @property + def theta_sampling(self) -> int: + """Get theta_sampling. + + Returns + ------- + int + theta sampling. + + """ + return self._uniform.theta_sampling + + @theta_sampling.setter + def theta_sampling(self, theta_sampling: int) -> None: + """Set theta_sampling. + + Parameters + ---------- + theta_sampling: int + theta sampling. + + Returns + ------- + None + + """ + self._uniform.theta_sampling = theta_sampling + + @property + def phi_sampling(self) -> int: + """Get phi_sampling. + + Returns + ------- + int + phi sampling. + + """ + return self._uniform.phi_sampling + + @phi_sampling.setter + def phi_sampling(self, phi_sampling: int) -> None: + """Set phi_sampling. + + Parameters + ---------- + phi_sampling: int + phi sampling. + + Returns + ------- + None + + """ + self._uniform.phi_sampling = phi_sampling + + def set_symmetric_unspecified( + self, + ) -> ( + SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Anisotropic._Uniform + ): + """Set symmetric type as unspecified. + + Returns + ------- + SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Anisotropic._Uniform + + """ + self._uniform.symmetry_type = 0 + return self + + def set_semmetric_none( + self, + ) -> ( + SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Anisotropic._Uniform + ): + """Set symmetric type as non-specified. + + Returns + ------- + SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Anisotropic._Uniform + + """ + self._uniform.symmetry_type = 1 + return self + + def set_symmetric_1_plane_symmetric( + self, + ) -> ( + SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Anisotropic._Uniform + ): + """Set symmetric type as plane symmetric. + + Returns + ------- + SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Anisotropic._Uniform + + """ + self._uniform.symmetry_type = 2 + return self + + def set_symmetric_2_plane_symmetric( + self, + ) -> ( + SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Anisotropic._Uniform + ): + """Set symmetric type as 2 planes symmetric. + + Returns + ------- + SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Anisotropic._Uniform + + """ + self._uniform.symmetry_type = 3 + return self + + def set_uniform( + self, + ) -> SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Anisotropic._Uniform: + """Set anisotropic uniform type. + + Returns + ------- + SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Anisotropic._Uniform + + """ + return self._Uniform(self._source_sampling.uniform_anisotropic, stable_ctr=True) + + def __init__( + self, + non_iridescence_mode, + stable_ctr: bool = False, + ): + if not stable_ctr: + msg = "NonIridescence class instantiated outside of class scope" + raise RuntimeError(msg) + self._non_iridescence = non_iridescence_mode + self.set_isotropic() + + def set_isotropic( + self, + ) -> SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Isotropic: + """Set isotropic type of uniform source. + + Returns + ------- + SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Isotropic + Isotropic source settings + + """ + return self.Isotropic( + self._non_iridescence.isotropic, + stable_ctr=True, + ) + + def set_anisotropic( + self, + ) -> SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Anisotropic: + """Set anisotropic type of uniform source. + + Returns + ------- + SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Anisotropic + Anisotropic source settings + + """ + return self.Anisotropic( + self._non_iridescence.anisotropic, + stable_ctr=True, + ) + + def __init__( + self, + mode_template: simulation_template_pb2.VirtualBSDFBench, + stable_ctr: bool = False, + ) -> None: + if not stable_ctr: + msg = "AllCharacteristics class instantiated outside of class scope" + raise RuntimeError(msg) + self._mode = mode_template + # Default values + self.is_bsdf180 = True + self.reflection_and_transmission = True + self.set_non_iridescence() + + @property + def is_bsdf180(self) -> bool: + """Get settings if bsdf is bsdf180. + + Returns + ------- + bool + True if bsdf180 is to be generated, False otherwise. + + """ + return self._mode.is_bsdf180 + + @is_bsdf180.setter + def is_bsdf180(self, value: bool) -> None: + """Set settings if bsdf180. + + Parameters + ---------- + value: bool + True if bsdf180 is to be generated, False otherwise. + + Returns + ------- + None + + """ + self._mode.is_bsdf180 = value + + @property + def reflection_and_transmission(self) -> bool: + """Get settings if reflection and transmission is to be generated. + + Returns + ------- + bool + True if reflection and transmission is to be generated, False otherwise. + + """ + return self._mode.sensor_reflection_and_transmission + + @reflection_and_transmission.setter + def reflection_and_transmission(self, value: bool) -> None: + """Set settings if reflection and transmission is to be generated. + + Parameters + ---------- + value: bool + True if reflection and transmission is to be generated, False otherwise. + + Returns + ------- + None + + """ + self._mode.sensor_reflection_and_transmission = value + + def set_non_iridescence(self) -> SimulationVirtualBSDF.AllCharacteristics.NonIridescence: + """Set bsdf color does not depend on viewing direction. + + Returns + ------- + SimulationVirtualBSDF.AllCharacteristics.NonIridescence + NonIridescence settings to be complete + """ + return self.NonIridescence( + non_iridescence_mode=self._mode.no_iridescence, + stable_ctr=True, + ) + + def set_iridescence(self) -> SimulationVirtualBSDF.AllCharacteristics.Iridescence: + """Set bsdf color depends on viewing direction. + + Returns + ------- + SimulationVirtualBSDF.AllCharacteristics.Iridescence + Iridescence settings to be complete + """ + return self.Iridescence( + iridescence_mode=self._mode.iridescence, + stable_ctr=True, + ) + + class WavelengthsRange: + """Range of wavelengths. + + By default, a range from 400nm to 700nm is chosen, with a sampling of 13. + + Parameters + ---------- + wavelengths_range : ansys.api.speos.sensor.v1.common_pb2.WavelengthsRange + Wavelengths range protobuf object to modify. + default_values : bool + Uses default values when True. + stable_ctr : bool + Variable to indicate if usage is inside class scope + + Notes + ----- + **Do not instantiate this class yourself**, use set_wavelengths_range method available in + sensor classes. + """ + + def __init__( + self, + wavelengths_range, + default_values: bool = True, + stable_ctr: bool = False, + ) -> None: + if not stable_ctr: + msg = "WavelengthsRange class instantiated outside of class scope" + raise RuntimeError(msg) + self._wavelengths_range = wavelengths_range + + if default_values: + # Default values + self.set_start().set_end().set_sampling() + + @property + def start(self) -> float: + """Return start wavelength. + + Returns + ------- + float + Start wavelength. + + """ + return self._wavelengths_range.w_start + + @start.setter + def start(self, value: float) -> None: + """Set start wavelength. + + Parameters + ---------- + value: float + Start wavelength. + + Returns + ------- + None + """ + self._wavelengths_range.w_start = value + + @property + def end(self) -> float: + """ + Return end wavelength. + + Returns + ------- + float + End wavelength. + + """ + return self._wavelengths_range.w_end + + @end.setter + def end(self, value: float) -> None: + """ + Set end wavelength. + + Parameters + ---------- + value: float + End wavelength. + + Returns + ------- + None + + """ + self._wavelengths_range.w_end = value + + @property + def sampling(self) -> int: + """ + Return sampling. + + Returns + ------- + int + Wavelength sampling. + + """ + return self._wavelengths_range.w_sampling + + @sampling.setter + def sampling(self, value: int) -> None: + """ + Set sampling. + + Parameters + ---------- + value: int + wavelength sampling. + + Returns + ------- + None + + """ + self._wavelengths_range.w_sampling = value + + class SensorUniform: + """BSDF bench sensor settings.""" + + def __init__( + self, + sensor_uniform_mode, + stable_ctr: bool = False, + ): + if not stable_ctr: + msg = "SensorUniform class instantiated outside of class scope" + raise RuntimeError(msg) + self._sensor_uniform_mode = sensor_uniform_mode + + @property + def theta_sampling(self) -> int: + """Get theta sampling. + + Returns + ------- + int + theta sampling. + + """ + return self._sensor_uniform_mode.theta_sampling + + @theta_sampling.setter + def theta_sampling(self, value: int) -> None: + """ + Set theta sampling. + + Parameters + ---------- + value: int + theta sampling. + + Returns + ------- + None + + """ + self._sensor_uniform_mode.theta_sampling = value + + @property + def phi_sampling(self) -> int: + """ + Get phi sampling. + + Returns + ------- + int + phi sampling. + + """ + return self._sensor_uniform_mode.phi_sampling + + @phi_sampling.setter + def phi_sampling(self, value: int) -> None: + """ + Set phi sampling. + + Parameters + ---------- + value: int + phi sampling. + + Returns + ------- + None + + """ + self._sensor_uniform_mode.phi_sampling = value + + def __init__( + self, + project: project.Project, + name: str, + description: str = "", + metadata: Optional[Mapping[str, str]] = None, + simulation_instance: Optional[ProtoScene.SimulationInstance] = None, + default_values: bool = True, + ) -> None: + if metadata is None: + metadata = {} + + super().__init__( + project=project, + name=name, + description=description, + metadata=metadata, + simulation_instance=simulation_instance, + ) + + self._wavelengths_range = self.set_wavelengths_range() + self._sensor_sampling_mode = self.set_sensor_sampling_uniform() + + if default_values: + self.geom_distance_tolerance = 0.01 + self.max_impact = 100 + self.set_weight() + self.set_colorimetric_standard_CIE_1931() + self.axis_system = [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1] + + @property + def geom_distance_tolerance(self) -> float: + """Return the geometry distance tolerance. + + Returns + ------- + float + Maximum distance in mm to consider two faces as tangent. + """ + return ( + self._simulation_template.virtual_bsdf_bench_simulation_template.geom_distance_tolerance + ) + + @geom_distance_tolerance.setter + def geom_distance_tolerance(self, value: float) -> None: + """Set the geometry distance tolerance. + + Parameters + ---------- + value : float + Maximum distance in mm to consider two faces as tangent. + By default, ``0.01`` + + Returns + ------- + None + """ + self._simulation_template.virtual_bsdf_bench_simulation_template.geom_distance_tolerance = ( + value + ) + + @property + def max_impact(self) -> int: + """Return the maximum number of impacts. + + Returns + ------- + int + The maximum number of impacts. + """ + return self._simulation_template.virtual_bsdf_bench_simulation_template.max_impact + + @max_impact.setter + def max_impact(self, value: int) -> None: + """Define a value to determine the maximum number of ray impacts during propagation. + + When a ray has interacted N times with the geometry, the propagation of the ray stops. + + Parameters + ---------- + value : int + The maximum number of impacts. + By default, ``100``. + + Returns + ------- + None + """ + self._simulation_template.virtual_bsdf_bench_simulation_template.max_impact = value + + @property + def integration_angle(self) -> float: + """Return the sensor integration angle. + + Returns + ------- + float + The sensor integration angle. + + """ + tmp_sensor = self._simulation_template.virtual_bsdf_bench_simulation_template.sensor + return tmp_sensor.integration_angle + + @integration_angle.setter + def integration_angle(self, angle: float) -> None: + """Set the sensor integration angle. + + Parameters + ---------- + angle: float + The sensor integration angle. + + Returns + ------- + None + + """ + tmp_sensor = self._simulation_template.virtual_bsdf_bench_simulation_template.sensor + tmp_sensor.integration_angle = angle + + @property + def axis_system(self) -> List[float]: + """Get axis system of the bsdf bench. + + Returns + ------- + List[float] + The axis system of the bsdf bench. + + """ + return self._simulation_instance.vbb_properties.axis_system + + @axis_system.setter + def axis_system(self, value: List[float]) -> None: + """Set axis system of the bsdf bench. + + Parameters + ---------- + value: List[float] + The axis system of the bsdf bench. + + Returns + ------- + None + + """ + self._simulation_instance.vbb_properties.axis_system[:] = value + + @property + def analysis_x_ratio(self) -> float: + """Get analysis x ratio, value must be in range [0., 100.]. + + Returns + ------- + float + Ratio to reduce the analysis area following x + + """ + return self._simulation_instance.vbb_properties.analysis_x_ratio + + @analysis_x_ratio.setter + def analysis_x_ratio(self, value: float) -> None: + """Set analysis x ratio, value must be in range [0., 100.]. + + Parameters + ---------- + value: float + The analysis x ratio in range [0., 100.] + + Returns + ------- + None + + """ + self._simulation_instance.vbb_properties.analysis_x_ratio = value + + @property + def analysis_y_ratio(self) -> float: + """Get analysis y ratio, value must be in range [0., 100.]. + + Returns + ------- + float + Ratio to reduce the analysis area following y + + """ + return self._simulation_instance.vbb_properties.analysis_y_ratio + + @analysis_y_ratio.setter + def analysis_y_ratio(self, value: float) -> None: + """Set analysis y ratio, value must be in range [0., 100.]. + + Parameters + ---------- + value: float + The analysis y ratio in range [0., 100.] + + Returns + ------- + None + + """ + self._simulation_instance.vbb_properties.analysis_y_ratio = value + + def set_weight(self) -> BaseSimulation.Weight: + """Activate weight. Highly recommended to fill. + + Returns + ------- + ansys.speos.core.simulation.BaseSimulation.Weight + Simulation.Weight + """ + return BaseSimulation.Weight( + self._simulation_template.virtual_bsdf_bench_simulation_template.weight, + stable_ctr=True, + ) + + def set_weight_none(self) -> SimulationVirtualBSDF: + """Deactivate weight. + + Returns + ------- + ansys.speos.core.simulation.SimulationVirtualBSDF + Inverse simulation + """ + self._simulation_template.virtual_bsdf_bench_simulation_template.ClearField("weight") + return self + + def set_colorimetric_standard_CIE_1931(self) -> SimulationVirtualBSDF: + """Set the colorimetric standard to CIE 1931. + + 2 degrees CIE Standard Colorimetric Observer Data. + + Returns + ------- + ansys.speos.core.simulation.SimulationVirtualBSDF + Inverse simulation + """ + self._simulation_template.virtual_bsdf_bench_simulation_template.colorimetric_standard = ( + simulation_template_pb2.CIE_1931 + ) + return self + + def set_colorimetric_standard_CIE_1964(self) -> SimulationVirtualBSDF: + """Set the colorimetric standard to CIE 1964. + + 10 degrees CIE Standard Colorimetric Observer Data. + + Returns + ------- + ansys.speos.core.simulation.SimulationVirtualBSDF + Inverse simulation + """ + self._simulation_template.virtual_bsdf_bench_simulation_template.colorimetric_standard = ( + simulation_template_pb2.CIE_1964 + ) + return self + + def set_mode_roughness_only(self) -> SimulationVirtualBSDF.RoughnessOnly: + """Set BSDF depends on surface roughness only. + + Returns + ------- + SimulationVirtualBSDF.RoughnessOnly + roughness only settings + """ + return SimulationVirtualBSDF.RoughnessOnly( + self._simulation_template.virtual_bsdf_bench_simulation_template.roughness_only, + stable_ctr=True, + ) + + def set_mode_all_characteristics(self) -> SimulationVirtualBSDF.AllCharacteristics: + """Set BSDF depends on all properties. + + Returns + ------- + SimulationVirtualBSDF.AllCharacteristics + all properties settings + """ + return SimulationVirtualBSDF.AllCharacteristics( + self._simulation_template.virtual_bsdf_bench_simulation_template.all_characteristics, + stable_ctr=True, + ) + + def set_wavelengths_range(self) -> SimulationVirtualBSDF.WavelengthsRange: + """Set the range of wavelengths. + + Returns + ------- + ansys.speos.core.sensor.BaseSensor.WavelengthsRange + Wavelengths range. + """ + if self._wavelengths_range._wavelengths_range is not ( + self._simulation_template.virtual_bsdf_bench_simulation_template.wavelengths_range + ): + # Happens in case of feature reset (to be sure to always modify correct data) + self._wavelengths_range._wavelengths_range = ( + self._simulation_template.virtual_bsdf_bench_simulation_template.wavelengths_range + ) + return self._wavelengths_range + + def set_sensor_sampling_uniform(self) -> SimulationVirtualBSDF.SensorUniform: + """Set sensor sampling uniform. + + Returns + ------- + ansys.speos.core.sensor.BaseSensor.SensorUniform + uniform type of sensor settings + + """ + if ( + self._sensor_sampling_mode._sensor_uniform_mode + is not self._simulation_template.virtual_bsdf_bench_simulation_template.sensor + ): + # Happens in case of feature reset (to be sure to always modify correct data) + self._sensor_sampling_mode._sensor_uniform_mode = ( + self._simulation_template.virtual_bsdf_bench_simulation_template.sensor + ) + return self._sensor_sampling_mode + + def set_sensor_sampling_automatic(self) -> SimulationVirtualBSDF: + """Set sensor sampling automatic. + + Returns + ------- + SimulationVirtualBSDF + + """ + self._simulation_template.virtual_bsdf_bench_simulation_template.sensor.automatic.SetInParent() + return self From 221d6205c07a04ccd8780e2afea9d81aef567d0b Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Sat, 25 Oct 2025 13:21:59 +0000 Subject: [PATCH 03/16] chore: adding changelog file 763.added.md [dependabot-skip] --- doc/changelog.d/763.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/763.added.md diff --git a/doc/changelog.d/763.added.md b/doc/changelog.d/763.added.md new file mode 100644 index 000000000..0cc4288b0 --- /dev/null +++ b/doc/changelog.d/763.added.md @@ -0,0 +1 @@ +Add supporting virtual bsdf bench From 8e4ee3b681fdf333ad84a57ec9d83637c5f77a58 Mon Sep 17 00:00:00 2001 From: plu Date: Tue, 4 Nov 2025 10:01:17 +0000 Subject: [PATCH 04/16] add unit test for creating virtual bsdf bench simulation --- src/ansys/speos/core/project.py | 8 + src/ansys/speos/core/simulation.py | 308 ++++++++++++++++++++++++----- tests/core/test_simulation.py | 257 ++++++++++++++++++++++++ 3 files changed, 522 insertions(+), 51 deletions(-) diff --git a/src/ansys/speos/core/project.py b/src/ansys/speos/core/project.py index b5c37fe8a..f57057feb 100644 --- a/src/ansys/speos/core/project.py +++ b/src/ansys/speos/core/project.py @@ -52,6 +52,7 @@ SimulationDirect, SimulationInteractive, SimulationInverse, + SimulationVirtualBSDF, ) from ansys.speos.core.source import ( SourceAmbientNaturalLight, @@ -296,6 +297,13 @@ def create_simulation( description=description, metadata=metadata, ) + case "SimulationVirtualBSDF": + feature = SimulationVirtualBSDF( + project=self, + name=name, + description=description, + metadata=metadata, + ) case _: msg = "Requested feature {} does not exist in supported list {}".format( feature_type, diff --git a/src/ansys/speos/core/simulation.py b/src/ansys/speos/core/simulation.py index 3e7618b0e..02f82ecf2 100644 --- a/src/ansys/speos/core/simulation.py +++ b/src/ansys/speos/core/simulation.py @@ -105,14 +105,19 @@ class _Adaptive: """ def __init__( - self, adaptive: simulation_template_pb2.SourceSamplingAdaptive, stable_ctr: bool + self, + adaptive: simulation_template_pb2.SourceSamplingAdaptive, + default_values: bool = True, + stable_ctr: bool = False, ) -> None: if not stable_ctr: msg = "_Adaptive class instantiated outside the class scope" raise RuntimeError(msg) self._adaptive = adaptive # Default setting - self.adaptive_uri = "" + + if default_values: + self.adaptive_uri = "" @property def adaptive_uri(self) -> str: @@ -156,14 +161,16 @@ class _Uniform: def __init__( self, uniform: simulation_template_pb2.SourceSamplingUniformIsotropic, - stable_ctr: bool, + default_values: bool = True, + stable_ctr: bool = False, ) -> None: if not stable_ctr: msg = "_Uniform class instantiated outside the class scope" raise RuntimeError(msg) self._uniform = uniform # Default setting - self.theta_sampling = 2 + if default_values: + self.theta_sampling = 18 @property def theta_sampling(self) -> int: @@ -200,12 +207,19 @@ def __init__( simulation_template_pb2.Isotropic, simulation_template_pb2.Anisotropic, ], + default_values: bool = True, stable_ctr: bool = False, ) -> None: if not stable_ctr: msg = "_SourceSampling class instantiated outside of the class scope" raise RuntimeError(msg) - self._source_sampling = source_sampling + + self._mode = source_sampling + + self._sampling_type = None + + if default_values: + self._sampling_type = self.set_uniform() def set_uniform(self) -> BaseSimulation._SourceSampling._Uniform: """Set uniform type of source sampling. @@ -215,10 +229,21 @@ def set_uniform(self) -> BaseSimulation._SourceSampling._Uniform: BaseSimulation._SourceSampling._Uniform """ - return self._Uniform( - self._source_sampling.uniform_isotropic, - stable_ctr=True, - ) + if self._sampling_type is None and self._mode.HasField("uniform_isotropic"): + self._sampling_type = self._Uniform( + self._mode.uniform_isotropic, + default_values=False, + stable_ctr=True, + ) + if not isinstance(self._sampling_type, BaseSimulation._SourceSampling._Uniform): + self._sampling_type = self._Uniform( + self._mode.uniform_isotropic, + default_values=True, + stable_ctr=True, + ) + if self._sampling_type._uniform is not self._mode.uniform_isotropic: + self._sampling_type = self._mode.uniform_isotropic + return self._sampling_type def set_adaptive(self) -> BaseSimulation._SourceSampling._Adaptive: """Set adaptive type of source sampling. @@ -228,7 +253,21 @@ def set_adaptive(self) -> BaseSimulation._SourceSampling._Adaptive: BaseSimulation._SourceSampling._Adaptive """ - return self._Adaptive(self._source_sampling.adaptive, stable_ctr=True) + if self._sampling_type is None and self._mode.HasField("adaptive"): + self._sampling_type = self._Adaptive( + self._mode.adaptive, + default_values=False, + stable_ctr=True, + ) + if not isinstance(self._sampling_type, BaseSimulation._SourceSampling._Adaptive): + self._sampling_type = self._Adaptive( + self._mode.adaptive, + default_values=True, + stable_ctr=True, + ) + if self._sampling_type._adaptive is not self._mode.adaptive: + self._sampling_type = self._mode.adaptive + return self._sampling_type class Weight: """The Weight represents the ray energy. @@ -1780,12 +1819,15 @@ class RoughnessOnly(BaseSimulation._SourceSampling): def __init__( self, mode_template: simulation_template_pb2.RoughnessOnly, + default_values: bool = True, stable_ctr: bool = False, ) -> None: if not stable_ctr: msg = "RoughnessOnly class instantiated outside of class scope" raise RuntimeError(msg) - super().__init__(source_sampling=mode_template, stable_ctr=True) + super().__init__( + source_sampling=mode_template, default_values=default_values, stable_ctr=True + ) class AllCharacteristics: """BSDF depends on all properties mode of BSDF bench measurement. @@ -1821,12 +1863,15 @@ class Iridescence(BaseSimulation._SourceSampling): def __init__( self, iridescence_mode: simulation_template_pb2.Iridescence, + default_values: bool = True, stable_ctr: bool = False, ) -> None: if not stable_ctr: msg = "AllCharacteristics class instantiated outside of class scope" raise RuntimeError(msg) - super().__init__(source_sampling=iridescence_mode, stable_ctr=True) + super().__init__( + source_sampling=iridescence_mode, default_values=default_values, stable_ctr=True + ) class NonIridescence: """Color does not depend on viewing direction of BSDF measurement settings. @@ -1859,12 +1904,17 @@ class Isotropic(BaseSimulation._SourceSampling): def __init__( self, non_iridescence_isotropic: simulation_template_pb2.Isotropic, + default_values: bool = True, stable_ctr: bool = False, ): if not stable_ctr: msg = "Isotropic class instantiated outside of class scope" raise RuntimeError(msg) - super().__init__(source_sampling=non_iridescence_isotropic, stable_ctr=True) + super().__init__( + source_sampling=non_iridescence_isotropic, + default_values=default_values, + stable_ctr=True, + ) class Anisotropic(BaseSimulation._SourceSampling): """Anisotropic source sampling. @@ -1880,12 +1930,17 @@ class Anisotropic(BaseSimulation._SourceSampling): def __init__( self, non_iridescence_anisotropic: simulation_template_pb2.Anisotropic, + default_values: bool = True, stable_ctr: bool = False, ): if not stable_ctr: msg = "Anisotropic class instantiated outside of class scope" raise RuntimeError(msg) - super().__init__(source_sampling=non_iridescence_anisotropic, stable_ctr=True) + super().__init__( + source_sampling=non_iridescence_anisotropic, + default_values=default_values, + stable_ctr=True, + ) class _Uniform: """Anisotorpic Uniform sampling mode. @@ -1901,12 +1956,17 @@ class _Uniform: def __init__( self, uniform: simulation_template_pb2.SourceSamplingUniformAnisotropic, + default_values: bool = True, stable_ctr: bool = False, ) -> None: if not stable_ctr: msg = "_Uniform class instantiated outside of class scope" raise RuntimeError(msg) self._uniform = uniform + if default_values: + self.theta_sampling = 18 + self.phi_sampling = 36 + self.set_semmetric_none() @property def theta_sampling(self) -> int: @@ -2034,18 +2094,22 @@ def set_uniform( SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Anisotropic._Uniform """ - return self._Uniform(self._source_sampling.uniform_anisotropic, stable_ctr=True) + return self._Uniform(self._mode.uniform_anisotropic, stable_ctr=True) def __init__( self, non_iridescence_mode, + default_values: bool = True, stable_ctr: bool = False, ): if not stable_ctr: msg = "NonIridescence class instantiated outside of class scope" raise RuntimeError(msg) self._non_iridescence = non_iridescence_mode - self.set_isotropic() + + self._iso_type = None + if default_values: + self._iso_type = self.set_isotropic() def set_isotropic( self, @@ -2058,10 +2122,24 @@ def set_isotropic( Isotropic source settings """ - return self.Isotropic( - self._non_iridescence.isotropic, - stable_ctr=True, - ) + if self._iso_type is None and self._non_iridescence.HasField("isotropic"): + self._iso_type = self.Isotropic( + self._non_iridescence.isotropic, + default_values=False, + stable_ctr=True, + ) + if not isinstance( + self._iso_type, + SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Isotropic, + ): + self._iso_type = self.Isotropic( + self._non_iridescence.isotropic, + default_values=True, + stable_ctr=True, + ) + if self._iso_type._mode is not self._non_iridescence.isotropic: + self._iso_type._mode = self._non_iridescence.isotropic + return self._iso_type def set_anisotropic( self, @@ -2074,24 +2152,38 @@ def set_anisotropic( Anisotropic source settings """ - return self.Anisotropic( - self._non_iridescence.anisotropic, - stable_ctr=True, - ) + if self._iso_type is None and self._non_iridescence.HasField("anisotropic"): + self._iso_type = self.Anisotropic( + self._non_iridescence.anisotropic, default_values=False, stable_ctr=True + ) + if not isinstance(self._iso_type, self.Anisotropic): + self._iso_type = self.Anisotropic( + self._non_iridescence.anisotropic, + default_values=True, + stable_ctr=True, + ) + if self._iso_type._mode is not self._non_iridescence.anisotropic: + self._iso_type._mode = self._non_iridescence.anisotropic + return self._iso_type def __init__( self, mode_template: simulation_template_pb2.VirtualBSDFBench, + default_values: bool = True, stable_ctr: bool = False, ) -> None: if not stable_ctr: msg = "AllCharacteristics class instantiated outside of class scope" raise RuntimeError(msg) - self._mode = mode_template + self._all_characteristics_mode = mode_template + + self._iridescence_mode = None + self._iridescence_mode = self.set_non_iridescence() + # Default values - self.is_bsdf180 = True - self.reflection_and_transmission = True - self.set_non_iridescence() + if default_values: + self.is_bsdf180 = True + self.reflection_and_transmission = False @property def is_bsdf180(self) -> bool: @@ -2103,7 +2195,7 @@ def is_bsdf180(self) -> bool: True if bsdf180 is to be generated, False otherwise. """ - return self._mode.is_bsdf180 + return self._all_characteristics_mode.is_bsdf180 @is_bsdf180.setter def is_bsdf180(self, value: bool) -> None: @@ -2119,7 +2211,7 @@ def is_bsdf180(self, value: bool) -> None: None """ - self._mode.is_bsdf180 = value + self._all_characteristics_mode.is_bsdf180 = value @property def reflection_and_transmission(self) -> bool: @@ -2131,7 +2223,7 @@ def reflection_and_transmission(self) -> bool: True if reflection and transmission is to be generated, False otherwise. """ - return self._mode.sensor_reflection_and_transmission + return self._all_characteristics_mode.sensor_reflection_and_transmission @reflection_and_transmission.setter def reflection_and_transmission(self, value: bool) -> None: @@ -2147,7 +2239,7 @@ def reflection_and_transmission(self, value: bool) -> None: None """ - self._mode.sensor_reflection_and_transmission = value + self._all_characteristics_mode.sensor_reflection_and_transmission = value def set_non_iridescence(self) -> SimulationVirtualBSDF.AllCharacteristics.NonIridescence: """Set bsdf color does not depend on viewing direction. @@ -2157,10 +2249,30 @@ def set_non_iridescence(self) -> SimulationVirtualBSDF.AllCharacteristics.NonIri SimulationVirtualBSDF.AllCharacteristics.NonIridescence NonIridescence settings to be complete """ - return self.NonIridescence( - non_iridescence_mode=self._mode.no_iridescence, - stable_ctr=True, - ) + if self._iridescence_mode is None and self._all_characteristics_mode.HasField( + "no_iridescence" + ): + self._iridescence_mode = self.NonIridescence( + non_iridescence_mode=self._all_characteristics_mode.no_iridescence, + default_values=False, + stable_ctr=True, + ) + if not isinstance( + self._iridescence_mode, SimulationVirtualBSDF.AllCharacteristics.NonIridescence + ): + self._iridescence_mode = self.NonIridescence( + non_iridescence_mode=self._all_characteristics_mode.no_iridescence, + default_values=True, + stable_ctr=True, + ) + if ( + self._iridescence_mode._non_iridescence + is not self._all_characteristics_mode.no_iridescence + ): + self._iridescence_mode._non_iridescence = ( + self._all_characteristics_mode.no_iridescence + ) + return self._iridescence_mode def set_iridescence(self) -> SimulationVirtualBSDF.AllCharacteristics.Iridescence: """Set bsdf color depends on viewing direction. @@ -2170,10 +2282,25 @@ def set_iridescence(self) -> SimulationVirtualBSDF.AllCharacteristics.Iridescenc SimulationVirtualBSDF.AllCharacteristics.Iridescence Iridescence settings to be complete """ - return self.Iridescence( - iridescence_mode=self._mode.iridescence, - stable_ctr=True, - ) + if self._iridescence_mode is None and self._all_characteristics_mode.HasField( + "iridescence" + ): + self._iridescence_mode = self.Iridescence( + iridescence_mode=self._all_characteristics_mode.iridescence, + default_values=False, + stable_ctr=True, + ) + if not isinstance( + self._iridescence_mode, SimulationVirtualBSDF.AllCharacteristics.Iridescence + ): + self._iridescence_mode = self.Iridescence( + iridescence_mode=self._all_characteristics_mode.iridescence, + default_values=True, + stable_ctr=True, + ) + if self._iridescence_mode._mode is not self._all_characteristics_mode.iridescence: + self._iridescence_mode._mode = self._all_characteristics_mode.iridescence + return self._iridescence_mode class WavelengthsRange: """Range of wavelengths. @@ -2208,7 +2335,9 @@ def __init__( if default_values: # Default values - self.set_start().set_end().set_sampling() + self.start = 400 + self.end = 700 + self.sampling = 13 @property def start(self) -> float: @@ -2303,12 +2432,16 @@ class SensorUniform: def __init__( self, sensor_uniform_mode, + default_values: bool = True, stable_ctr: bool = False, ): if not stable_ctr: msg = "SensorUniform class instantiated outside of class scope" raise RuntimeError(msg) self._sensor_uniform_mode = sensor_uniform_mode + if default_values: + self.theta_sampling = 45 + self.phi_sampling = 180 @property def theta_sampling(self) -> int: @@ -2389,15 +2522,23 @@ def __init__( simulation_instance=simulation_instance, ) + self._wavelengths_range = None + self._sensor_sampling_mode = None + self._mode = None + self._wavelengths_range = self.set_wavelengths_range() self._sensor_sampling_mode = self.set_sensor_sampling_uniform() + self._mode = self.set_mode_all_characteristics() if default_values: self.geom_distance_tolerance = 0.01 self.max_impact = 100 self.set_weight() self.set_colorimetric_standard_CIE_1931() + self.analysis_x_ratio = 100 + self.analysis_y_ratio = 100 self.axis_system = [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1] + self.integration_angle = 2 @property def geom_distance_tolerance(self) -> float: @@ -2635,10 +2776,31 @@ def set_mode_roughness_only(self) -> SimulationVirtualBSDF.RoughnessOnly: SimulationVirtualBSDF.RoughnessOnly roughness only settings """ - return SimulationVirtualBSDF.RoughnessOnly( - self._simulation_template.virtual_bsdf_bench_simulation_template.roughness_only, - stable_ctr=True, - ) + if ( + self._mode is None + and self._simulation_template.virtual_bsdf_bench_simulation_template.HasField( + "roughness_only" + ) + ): + self._mode = SimulationVirtualBSDF.RoughnessOnly( + self._simulation_template.virtual_bsdf_bench_simulation_template.roughness_only, + default_values=False, + stable_ctr=True, + ) + if not isinstance(self._mode, SimulationVirtualBSDF.RoughnessOnly): + self._mode = SimulationVirtualBSDF.RoughnessOnly( + self._simulation_template.virtual_bsdf_bench_simulation_template.roughness_only, + default_values=True, + stable_ctr=True, + ) + if ( + self._mode._mode + is not self._simulation_template.virtual_bsdf_bench_simulation_template.roughness_only + ): + self._mode._mode = ( + self._simulation_template.virtual_bsdf_bench_simulation_template.roughness_only + ) + return self._mode def set_mode_all_characteristics(self) -> SimulationVirtualBSDF.AllCharacteristics: """Set BSDF depends on all properties. @@ -2648,10 +2810,31 @@ def set_mode_all_characteristics(self) -> SimulationVirtualBSDF.AllCharacteristi SimulationVirtualBSDF.AllCharacteristics all properties settings """ - return SimulationVirtualBSDF.AllCharacteristics( - self._simulation_template.virtual_bsdf_bench_simulation_template.all_characteristics, - stable_ctr=True, - ) + if ( + self._mode is None + and self._simulation_template.virtual_bsdf_bench_simulation_template.HasField( + "all_characteristics" + ) + ): + self._mode = SimulationVirtualBSDF.AllCharacteristics( + self._simulation_template.virtual_bsdf_bench_simulation_template.all_characteristics, + default_values=False, + stable_ctr=True, + ) + elif not isinstance(self._mode, SimulationVirtualBSDF.AllCharacteristics): + # if the _type is not Colorimetric then we create a new type. + self._mode = SimulationVirtualBSDF.AllCharacteristics( + self._simulation_template.virtual_bsdf_bench_simulation_template.all_characteristics, + default_values=True, + stable_ctr=True, + ) + if self._mode._all_characteristics_mode is not ( + self._simulation_template.virtual_bsdf_bench_simulation_template.all_characteristics + ): + self._mode._all_characteristics_mode = ( + self._simulation_template.virtual_bsdf_bench_simulation_template.all_characteristics + ) + return self._mode def set_wavelengths_range(self) -> SimulationVirtualBSDF.WavelengthsRange: """Set the range of wavelengths. @@ -2661,6 +2844,12 @@ def set_wavelengths_range(self) -> SimulationVirtualBSDF.WavelengthsRange: ansys.speos.core.sensor.BaseSensor.WavelengthsRange Wavelengths range. """ + if self._wavelengths_range is None: + return SimulationVirtualBSDF.WavelengthsRange( + wavelengths_range=self._simulation_template.virtual_bsdf_bench_simulation_template.wavelengths_range, + default_values=True, + stable_ctr=True, + ) if self._wavelengths_range._wavelengths_range is not ( self._simulation_template.virtual_bsdf_bench_simulation_template.wavelengths_range ): @@ -2679,13 +2868,30 @@ def set_sensor_sampling_uniform(self) -> SimulationVirtualBSDF.SensorUniform: uniform type of sensor settings """ + if ( + self._sensor_sampling_mode is None + and self._simulation_template.virtual_bsdf_bench_simulation_template.sensor.HasField( + "uniform" + ) + ): + self._sensor_sampling_mode = SimulationVirtualBSDF.SensorUniform( + self._simulation_template.virtual_bsdf_bench_simulation_template.sensor.uniform, + default_values=False, + stable_ctr=True, + ) + if not isinstance(self._sensor_sampling_mode, SimulationVirtualBSDF.SensorUniform): + self._sensor_sampling_mode = SimulationVirtualBSDF.SensorUniform( + self._simulation_template.virtual_bsdf_bench_simulation_template.sensor.uniform, + default_values=True, + stable_ctr=True, + ) if ( self._sensor_sampling_mode._sensor_uniform_mode - is not self._simulation_template.virtual_bsdf_bench_simulation_template.sensor + is not self._simulation_template.virtual_bsdf_bench_simulation_template.sensor.uniform ): # Happens in case of feature reset (to be sure to always modify correct data) self._sensor_sampling_mode._sensor_uniform_mode = ( - self._simulation_template.virtual_bsdf_bench_simulation_template.sensor + self._simulation_template.virtual_bsdf_bench_simulation_template.sensor.uniform ) return self._sensor_sampling_mode diff --git a/tests/core/test_simulation.py b/tests/core/test_simulation.py index 775233c81..49b375cba 100644 --- a/tests/core/test_simulation.py +++ b/tests/core/test_simulation.py @@ -33,6 +33,7 @@ SimulationDirect, SimulationInteractive, SimulationInverse, + SimulationVirtualBSDF, ) from ansys.speos.core.source import SourceLuminaire from tests.conftest import config, test_path @@ -369,6 +370,262 @@ def test_create_interactive(speos: Speos): sim1.delete() +def test_create_virtual_bsdf_bench(speos: Speos): + """Test creation of Virtual BSDF Bench Simulation.""" + p = Project(speos=speos) + vbb = p.create_simulation("virtual_bsdf_bench_1", feature_type=SimulationVirtualBSDF) + + # Check default properties + # Check backend property + assert vbb._simulation_template.HasField("virtual_bsdf_bench_simulation_template") + assert ( + vbb._simulation_template.virtual_bsdf_bench_simulation_template.geom_distance_tolerance + == 0.01 + ) + assert vbb._simulation_template.virtual_bsdf_bench_simulation_template.max_impact == 100 + assert vbb._simulation_template.virtual_bsdf_bench_simulation_template.HasField("weight") + assert ( + vbb._simulation_template.virtual_bsdf_bench_simulation_template.weight.minimum_energy_percentage + == 0.005 + ) + assert ( + vbb._simulation_template.virtual_bsdf_bench_simulation_template.colorimetric_standard + is simulation_template_pb2.CIE_1931 + ) + assert ( + vbb._simulation_template.virtual_bsdf_bench_simulation_template.wavelengths_range.w_start + == 400 + ) + assert ( + vbb._simulation_template.virtual_bsdf_bench_simulation_template.wavelengths_range.w_end + == 700 + ) + assert ( + vbb._simulation_template.virtual_bsdf_bench_simulation_template.wavelengths_range.w_sampling + == 13 + ) + assert vbb._simulation_instance.vbb_properties.axis_system[:] == [ + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + ] + assert vbb._simulation_instance.vbb_properties.analysis_x_ratio == 100 + assert vbb._simulation_instance.vbb_properties.analysis_y_ratio == 100 + + # Check mode and source settings + assert vbb._simulation_template.virtual_bsdf_bench_simulation_template.HasField( + "all_characteristics" + ) + backend_properties = ( + vbb._simulation_template.virtual_bsdf_bench_simulation_template.all_characteristics + ) + assert backend_properties.is_bsdf180 is True + assert backend_properties.sensor_reflection_and_transmission is False + assert backend_properties.HasField("no_iridescence") + assert backend_properties.no_iridescence.HasField("isotropic") + assert backend_properties.no_iridescence.isotropic.HasField("uniform_isotropic") + assert backend_properties.no_iridescence.isotropic.uniform_isotropic.theta_sampling == 18 + + # Check sensor settings + assert ( + vbb._simulation_template.virtual_bsdf_bench_simulation_template.sensor.integration_angle + == 2 + ) + assert vbb._simulation_template.virtual_bsdf_bench_simulation_template.sensor.HasField( + "uniform" + ) + assert ( + vbb._simulation_template.virtual_bsdf_bench_simulation_template.sensor.uniform.theta_sampling + == 45 + ) + assert ( + vbb._simulation_template.virtual_bsdf_bench_simulation_template.sensor.uniform.phi_sampling + == 180 + ) + + # Check frontend property + assert vbb.geom_distance_tolerance == 0.01 + assert vbb.max_impact == 100 + assert vbb.integration_angle == 2 + assert vbb.axis_system == [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1] + assert vbb.analysis_x_ratio == 100 + assert vbb.analysis_y_ratio == 100 + assert vbb.set_wavelengths_range().start == 400 + assert vbb.set_wavelengths_range().end == 700 + assert vbb.set_wavelengths_range().sampling == 13 + assert vbb.set_mode_all_characteristics().is_bsdf180 is True + assert vbb.set_mode_all_characteristics().reflection_and_transmission is False + assert ( + vbb.set_mode_all_characteristics() + .set_non_iridescence() + .set_isotropic() + .set_uniform() + .theta_sampling + == 18 + ) + assert vbb.set_sensor_sampling_uniform().theta_sampling == 45 + assert vbb.set_sensor_sampling_uniform().phi_sampling == 180 + + # Change isotropic adaptive source sampling + vbb.set_mode_all_characteristics().set_non_iridescence().set_isotropic().set_adaptive() + # Check backend properties + backend_properties = ( + vbb._simulation_template.virtual_bsdf_bench_simulation_template.all_characteristics + ) + assert backend_properties.no_iridescence.isotropic.HasField("adaptive") + assert backend_properties.no_iridescence.isotropic.adaptive.file_uri == "" + # Check frontend properties + assert ( + vbb.set_mode_all_characteristics() + .set_non_iridescence() + .set_isotropic() + .set_adaptive() + .adaptive_uri + == "" + ) + + # Check if properties are saved + vbb.set_mode_all_characteristics().set_non_iridescence().set_isotropic() + # Check backend properties + backend_properties = ( + vbb._simulation_template.virtual_bsdf_bench_simulation_template.all_characteristics + ) + assert backend_properties.no_iridescence.isotropic.HasField("adaptive") + assert backend_properties.no_iridescence.isotropic.adaptive.file_uri == "" + # Check frontend properties + assert ( + vbb.set_mode_all_characteristics() + .set_non_iridescence() + .set_isotropic() + .set_adaptive() + .adaptive_uri + == "" + ) + + # Change back to isotropic uniform source sampling + vbb.set_mode_all_characteristics().set_non_iridescence().set_isotropic().set_uniform() + # Check backend properties + backend_properties = ( + vbb._simulation_template.virtual_bsdf_bench_simulation_template.all_characteristics + ) + assert backend_properties.no_iridescence.isotropic.HasField("uniform_isotropic") + # Check frontend properties + assert backend_properties.no_iridescence.isotropic.uniform_isotropic.theta_sampling == 18 + + # Change anisotropic uniform + vbb.set_mode_all_characteristics().set_non_iridescence().set_anisotropic() + # Check backend properties + backend_properties = ( + vbb._simulation_template.virtual_bsdf_bench_simulation_template.all_characteristics + ) + assert backend_properties.no_iridescence.HasField("anisotropic") + assert backend_properties.no_iridescence.anisotropic.HasField("uniform_anisotropic") + assert backend_properties.no_iridescence.anisotropic.uniform_anisotropic.theta_sampling == 18 + assert backend_properties.no_iridescence.anisotropic.uniform_anisotropic.phi_sampling == 36 + assert backend_properties.no_iridescence.anisotropic.uniform_anisotropic.symmetry_type == 1 + # Check frontend properties + assert ( + vbb.set_mode_all_characteristics() + .set_non_iridescence() + .set_anisotropic() + .set_uniform() + .theta_sampling + == 18 + ) + assert ( + vbb.set_mode_all_characteristics() + .set_non_iridescence() + .set_anisotropic() + .set_uniform() + .phi_sampling + == 36 + ) + + # Change anisotropic adaptive + vbb.set_mode_all_characteristics().set_non_iridescence().set_anisotropic().set_adaptive() + # Check backend properties + backend_properties = ( + vbb._simulation_template.virtual_bsdf_bench_simulation_template.all_characteristics + ) + assert backend_properties.no_iridescence.anisotropic.HasField("adaptive") + assert backend_properties.no_iridescence.anisotropic.adaptive.file_uri == "" + # Check frontend properties + assert ( + vbb.set_mode_all_characteristics() + .set_non_iridescence() + .set_anisotropic() + .set_adaptive() + .adaptive_uri + == "" + ) + + # Change color depending on viewing angle + vbb.set_mode_all_characteristics().set_iridescence() + # Check backend properties + backend_properties = ( + vbb._simulation_template.virtual_bsdf_bench_simulation_template.all_characteristics + ) + assert backend_properties.HasField("iridescence") + assert backend_properties.iridescence.HasField("uniform_isotropic") + assert backend_properties.iridescence.uniform_isotropic.theta_sampling == 18 + # Check frontend properties + assert vbb.set_mode_all_characteristics().set_iridescence().set_uniform().theta_sampling == 18 + + # Change color depending on viewing angle with adaptive source sampling + vbb.set_mode_all_characteristics().set_iridescence().set_adaptive() + # Check backend properties + backend_properties = ( + vbb._simulation_template.virtual_bsdf_bench_simulation_template.all_characteristics + ) + assert backend_properties.iridescence.HasField("adaptive") + assert backend_properties.iridescence.adaptive.file_uri == "" + # Check frontend properties + assert vbb.set_mode_all_characteristics().set_iridescence().set_adaptive().adaptive_uri == "" + + # Change mode to surface roughness only + vbb.set_mode_roughness_only() + # Check backend property + assert not vbb._simulation_template.virtual_bsdf_bench_simulation_template.HasField( + "all_characteristics" + ) + assert vbb._simulation_template.virtual_bsdf_bench_simulation_template.HasField( + "roughness_only" + ) + backend_properties = ( + vbb._simulation_template.virtual_bsdf_bench_simulation_template.roughness_only + ) + assert backend_properties.HasField("uniform_isotropic") + assert backend_properties.uniform_isotropic.theta_sampling == 18 + # Check frontend properties + assert vbb.set_mode_roughness_only().set_uniform().theta_sampling == 18 + + # Change to adaptive source sampling + vbb.set_mode_roughness_only().set_adaptive() + # Check backend properties + backend_properties = ( + vbb._simulation_template.virtual_bsdf_bench_simulation_template.roughness_only + ) + assert backend_properties.HasField("adaptive") + assert backend_properties.adaptive.file_uri == "" + # Check frontend properties + assert vbb.set_mode_roughness_only().set_adaptive().adaptive_uri == "" + + # Change sensor to automatic + vbb.set_sensor_sampling_automatic() + assert vbb._simulation_template.virtual_bsdf_bench_simulation_template.sensor.HasField( + "automatic" + ) + + def test_commit(speos: Speos): """Test commit of simulation.""" p = Project(speos=speos) From a29c1d2bc3909ae5108c7b32eb8886d1fb6a2540 Mon Sep 17 00:00:00 2001 From: plu Date: Tue, 4 Nov 2025 11:15:28 +0000 Subject: [PATCH 05/16] add example --- examples/core/simulation.py | 11 ++++++++++- src/ansys/speos/core/simulation.py | 31 +++++++++++++++++++++++++++++- tests/core/test_simulation.py | 2 ++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/examples/core/simulation.py b/examples/core/simulation.py index e95950587..04b16733b 100644 --- a/examples/core/simulation.py +++ b/examples/core/simulation.py @@ -14,7 +14,11 @@ from pathlib import Path from ansys.speos.core import Project, Speos, launcher -from ansys.speos.core.simulation import SimulationInteractive, SimulationInverse +from ansys.speos.core.simulation import ( + SimulationInteractive, + SimulationInverse, + SimulationVirtualBSDF, +) # - @@ -183,4 +187,9 @@ simulation4.set_source_paths(source_paths=[SOURCE_NAME]).commit() print(simulation4) +# ### Virtual BSDF Bench simulation + +vbb = p.create_simulation(name="virtual_BSDF", feature_type=SimulationVirtualBSDF) +vbb.compute_CPU() + speos.close() diff --git a/src/ansys/speos/core/simulation.py b/src/ansys/speos/core/simulation.py index 02f82ecf2..e5116f68a 100644 --- a/src/ansys/speos/core/simulation.py +++ b/src/ansys/speos/core/simulation.py @@ -2182,7 +2182,7 @@ def __init__( # Default values if default_values: - self.is_bsdf180 = True + self.is_bsdf180 = False self.reflection_and_transmission = False @property @@ -2539,6 +2539,7 @@ def __init__( self.analysis_y_ratio = 100 self.axis_system = [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1] self.integration_angle = 2 + self.stop_condition_ray_number = 100000 @property def geom_distance_tolerance(self) -> float: @@ -2714,6 +2715,34 @@ def analysis_y_ratio(self, value: float) -> None: """ self._simulation_instance.vbb_properties.analysis_y_ratio = value + @property + def stop_condition_ray_number(self) -> int: + """Get ray stop condition ray number. + + Returns + ------- + float + The ray stop condition ray number. + + """ + return self._job.virtualbsdfbench_simulation_properties.stop_condition_rays_number + + @stop_condition_ray_number.setter + def stop_condition_ray_number(self, value: int) -> None: + """Set ray stop condition ray number. + + Parameters + ---------- + value: int + The ray stop condition ray number. + + Returns + ------- + None + + """ + self._job.virtualbsdfbench_simulation_properties.stop_condition_rays_number = value + def set_weight(self) -> BaseSimulation.Weight: """Activate weight. Highly recommended to fill. diff --git a/tests/core/test_simulation.py b/tests/core/test_simulation.py index 49b375cba..66e16a44c 100644 --- a/tests/core/test_simulation.py +++ b/tests/core/test_simulation.py @@ -370,6 +370,7 @@ def test_create_interactive(speos: Speos): sim1.delete() +@pytest.mark.supported_speos_versions(min=252) def test_create_virtual_bsdf_bench(speos: Speos): """Test creation of Virtual BSDF Bench Simulation.""" p = Project(speos=speos) @@ -624,6 +625,7 @@ def test_create_virtual_bsdf_bench(speos: Speos): assert vbb._simulation_template.virtual_bsdf_bench_simulation_template.sensor.HasField( "automatic" ) + vbb.delete() def test_commit(speos: Speos): From 8859063c226b8e81a4df0c839708f433d1c2918d Mon Sep 17 00:00:00 2001 From: plu Date: Tue, 4 Nov 2025 11:44:45 +0000 Subject: [PATCH 06/16] improve vbsdf example --- examples/core/simulation.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/examples/core/simulation.py b/examples/core/simulation.py index 04b16733b..a16d1c048 100644 --- a/examples/core/simulation.py +++ b/examples/core/simulation.py @@ -190,6 +190,25 @@ # ### Virtual BSDF Bench simulation vbb = p.create_simulation(name="virtual_BSDF", feature_type=SimulationVirtualBSDF) -vbb.compute_CPU() +opt_prop.set_surface_library( + path=str(assets_data_path / "R_test.anisotropicbsdf") +).commit() # change the material property from mirror to bsdf type +vbb.axis_system = [ + 0.36, + 1.73, + 2.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, +] # change the coordinate VBSDF to body center +vbb.commit() +results = vbb.compute_CPU() +print(results) speos.close() From 5d5bdb227e5711628c26ca4b364787762fc608bc Mon Sep 17 00:00:00 2001 From: plu Date: Tue, 4 Nov 2025 12:24:51 +0000 Subject: [PATCH 07/16] add support load an exported vbsdf simulation --- src/ansys/speos/core/project.py | 12 ++++++++++++ tests/core/test_simulation.py | 10 ++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/ansys/speos/core/project.py b/src/ansys/speos/core/project.py index f57057feb..7ab4e922d 100644 --- a/src/ansys/speos/core/project.py +++ b/src/ansys/speos/core/project.py @@ -643,6 +643,11 @@ def _to_dict(self) -> dict: name=inside_dict["name"], feature_type=SimulationInteractive, ) + if len(sim_feat) == 0: + sim_feat = self.find( + name=inside_dict["name"], + feature_type=SimulationVirtualBSDF, + ) sim_feat = sim_feat[0] if sim_feat.job_link is None: inside_dict["simulation_properties"] = ( @@ -886,6 +891,13 @@ def _fill_features(self): simulation_instance=sim_inst, default_values=False, ) + elif simulation_template_link.HasField("virtual_bsdf_bench_simulation_template"): + sim_feat = SimulationVirtualBSDF( + project=self, + name=sim_inst.name, + simulation_instance=sim_inst, + default_values=False, + ) if sim_feat is not None: self._features.append(sim_feat) diff --git a/tests/core/test_simulation.py b/tests/core/test_simulation.py index 66e16a44c..7e5161c2b 100644 --- a/tests/core/test_simulation.py +++ b/tests/core/test_simulation.py @@ -628,6 +628,16 @@ def test_create_virtual_bsdf_bench(speos: Speos): vbb.delete() +def test_load_virtual_bsdf_bench(speos: Speos): + """Test load of a exported virtual bsdf bench simulation.""" + p = Project( + speos=speos, path=str(Path(test_path) / "nx_vbb_export.speos" / "nx_vbb_export.speos") + ) + assert p is not None + sims = p.find(name=".*", name_regex=True, feature_type=SimulationVirtualBSDF) + assert len(sims) > 0 + + def test_commit(speos: Speos): """Test commit of simulation.""" p = Project(speos=speos) From c31458bcc9e9018ea077eba9aa2802f28e04be2e Mon Sep 17 00:00:00 2001 From: plu Date: Tue, 4 Nov 2025 12:40:46 +0000 Subject: [PATCH 08/16] fix default type error --- tests/core/test_simulation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/core/test_simulation.py b/tests/core/test_simulation.py index 7e5161c2b..694c7c252 100644 --- a/tests/core/test_simulation.py +++ b/tests/core/test_simulation.py @@ -429,7 +429,7 @@ def test_create_virtual_bsdf_bench(speos: Speos): backend_properties = ( vbb._simulation_template.virtual_bsdf_bench_simulation_template.all_characteristics ) - assert backend_properties.is_bsdf180 is True + assert backend_properties.is_bsdf180 is False assert backend_properties.sensor_reflection_and_transmission is False assert backend_properties.HasField("no_iridescence") assert backend_properties.no_iridescence.HasField("isotropic") From 8070f1777a2681b878b6367106335881adb40313 Mon Sep 17 00:00:00 2001 From: plu Date: Tue, 4 Nov 2025 12:50:40 +0000 Subject: [PATCH 09/16] fix default type error --- tests/core/test_simulation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/core/test_simulation.py b/tests/core/test_simulation.py index 694c7c252..ba1bbf9d4 100644 --- a/tests/core/test_simulation.py +++ b/tests/core/test_simulation.py @@ -463,7 +463,7 @@ def test_create_virtual_bsdf_bench(speos: Speos): assert vbb.set_wavelengths_range().start == 400 assert vbb.set_wavelengths_range().end == 700 assert vbb.set_wavelengths_range().sampling == 13 - assert vbb.set_mode_all_characteristics().is_bsdf180 is True + assert vbb.set_mode_all_characteristics().is_bsdf180 is False assert vbb.set_mode_all_characteristics().reflection_and_transmission is False assert ( vbb.set_mode_all_characteristics() From ea8526f84d7b13501e3573b877527c3b5245b7b1 Mon Sep 17 00:00:00 2001 From: plu Date: Tue, 4 Nov 2025 12:59:21 +0000 Subject: [PATCH 10/16] add vbsdf speos asset file --- .../nx_vbb_export.speos/nx_vbb_export.speos | Bin 0 -> 64343 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/assets/nx_vbb_export.speos/nx_vbb_export.speos diff --git a/tests/assets/nx_vbb_export.speos/nx_vbb_export.speos b/tests/assets/nx_vbb_export.speos/nx_vbb_export.speos new file mode 100644 index 0000000000000000000000000000000000000000..cc008bb4f6919d33aab4cb365cea7a29cbd9c1ce GIT binary patch literal 64343 zcmV(pK=8li0{{R30DwRM0?>|%8UO&eK0TEE_j>UMZ)W>?A&;T)EfGcH3vJIIJXKt}^

=(h;QwDhywY*Fo}=|70wGy%9WsKQ2Ks9c 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 literal 0 HcmV?d00001 From 7e15dccc2dbf26be911165cd8a2353b5ba3af5e3 Mon Sep 17 00:00:00 2001 From: plu Date: Sun, 16 Nov 2025 14:07:01 +0000 Subject: [PATCH 11/16] fix issue of changing uniform properties --- src/ansys/speos/core/simulation.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/ansys/speos/core/simulation.py b/src/ansys/speos/core/simulation.py index e5116f68a..1fe5942d5 100644 --- a/src/ansys/speos/core/simulation.py +++ b/src/ansys/speos/core/simulation.py @@ -2094,7 +2094,30 @@ def set_uniform( SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Anisotropic._Uniform """ - return self._Uniform(self._mode.uniform_anisotropic, stable_ctr=True) + if self._sampling_type is None and self._mode.HasField("uniform_anisotropic"): + _non_iridescence_cls = ( + SimulationVirtualBSDF.AllCharacteristics.NonIridescence + ) # done to pass PEP8 E501 + self._sampling_type = _non_iridescence_cls.Anisotropic._Uniform( + self._mode.uniform_anisotropic, + default_values=False, + stable_ctr=True, + ) + if not isinstance( + self._sampling_type, + SimulationVirtualBSDF.AllCharacteristics.NonIridescence.Anisotropic._Uniform, + ): + _non_iridescence_cls = ( + SimulationVirtualBSDF.AllCharacteristics.NonIridescence + ) # done to pass PEP8 E501 + self._sampling_type = _non_iridescence_cls.Anisotropic._Uniform( + self._mode.uniform_anisotropic, + default_values=True, + stable_ctr=True, + ) + if self._sampling_type._uniform is not self._mode.uniform_anisotropic: + self._sampling_type = self._mode.uniform_anisotropic + return self._sampling_type def __init__( self, From f940f55f0fd20922100ab7c59e65722b0b1cfc34 Mon Sep 17 00:00:00 2001 From: plu Date: Mon, 17 Nov 2025 20:35:59 +0000 Subject: [PATCH 12/16] add un-support set_sensor and set_source paths mathod in virtualbsdf --- src/ansys/speos/core/simulation.py | 50 ++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/ansys/speos/core/simulation.py b/src/ansys/speos/core/simulation.py index 1fe5942d5..5190c3776 100644 --- a/src/ansys/speos/core/simulation.py +++ b/src/ansys/speos/core/simulation.py @@ -2766,6 +2766,56 @@ def stop_condition_ray_number(self, value: int) -> None: """ self._job.virtualbsdfbench_simulation_properties.stop_condition_rays_number = value + def set_sensor_paths(self, sensor_paths: List[str]) -> None: + """ + Disable setting sensor paths for this subclass. + + This method is intentionally not supported in ``SimulationVirtualBSDF``. + It exists only to satisfy the interface defined in the base class and + will always raise a ``NotImplementedError`` when called. + + Parameters + ---------- + sensor_paths : list of str + Ignored. Present only for compatibility with the base class. + + Returns + ------- + None + This method does not return anything. It always raises an exception. + + Raises + ------ + NotImplementedError + Always raised, since this method is disabled in this subclass. + """ + raise NotImplementedError("This method is disabled in SimulationVirtualBSDF") + + def set_source_paths(self, source_paths: List[str]) -> None: + """ + Disable setting source paths for this subclass. + + This method is intentionally not supported in ``SimulationVirtualBSDF``. + It exists only to satisfy the interface defined in the base class and + will always raise a ``NotImplementedError`` when called. + + Parameters + ---------- + source_paths : list of str + Ignored. Present only for compatibility with the base class. + + Returns + ------- + None + This method does not return anything. It always raises an exception. + + Raises + ------ + NotImplementedError + Always raised, since this method is disabled in this subclass. + """ + raise NotImplementedError("This method is disabled in SimulationVirtualBSDF") + def set_weight(self) -> BaseSimulation.Weight: """Activate weight. Highly recommended to fill. From 62932d843a4a953e6566687a64664e1d401107ff Mon Sep 17 00:00:00 2001 From: plu Date: Mon, 17 Nov 2025 20:37:13 +0000 Subject: [PATCH 13/16] refactor geom_distance_tolerance from child to base --- src/ansys/speos/core/simulation.py | 158 +++++++++++------------------ tests/core/test_simulation.py | 16 +-- 2 files changed, 70 insertions(+), 104 deletions(-) diff --git a/src/ansys/speos/core/simulation.py b/src/ansys/speos/core/simulation.py index 5190c3776..dca75c9d1 100644 --- a/src/ansys/speos/core/simulation.py +++ b/src/ansys/speos/core/simulation.py @@ -424,6 +424,54 @@ def set_source_paths(self, source_paths: List[str]) -> BaseSimulation: # geo_paths = [gr.to_native_link() for gr in geometries] # self._simulation_instance.geometries.geo_paths[:] = geo_paths # return self + @property + def geom_distance_tolerance(self) -> float: + """Return the geometry distance tolerance. + + Returns + ------- + float + Maximum distance in mm to consider two faces as tangent. + """ + tmpl = self._simulation_template + match tmpl: + case _ if tmpl.HasField("virtual_bsdf_bench_simulation_template"): + return tmpl.virtual_bsdf_bench_simulation_template.geom_distance_tolerance + case _ if tmpl.HasField("direct_mc_simulation_template"): + return tmpl.direct_mc_simulation_template.geom_distance_tolerance + case _ if tmpl.HasField("inverse_mc_simulation_template"): + return tmpl.inverse_mc_simulation_template.geom_distance_tolerance + case _ if tmpl.HasField("interactive_simulation_template"): + return tmpl.interactive_simulation_template.geom_distance_tolerance + case _: + raise TypeError(f"Unknown simulation template type: {tmpl}") + + @geom_distance_tolerance.setter + def geom_distance_tolerance(self, value: float) -> None: + """Set the geometry distance tolerance. + + Parameters + ---------- + value : float + Maximum distance in mm to consider two faces as tangent. + By default, ``0.01`` + + Returns + ------- + None + """ + tmpl = self._simulation_template + match tmpl: + case _ if tmpl.HasField("virtual_bsdf_bench_simulation_template"): + tmpl.virtual_bsdf_bench_simulation_template.geom_distance_tolerance = value + case _ if tmpl.HasField("direct_mc_simulation_template"): + tmpl.direct_mc_simulation_template.geom_distance_tolerance = value + case _ if tmpl.HasField("inverse_mc_simulation_template"): + tmpl.inverse_mc_simulation_template.geom_distance_tolerance = value + case _ if tmpl.HasField("interactive_simulation_template"): + tmpl.interactive_simulation_template.geom_distance_tolerance = value + case _: + raise TypeError(f"Unknown simulation template type: {tmpl}") def export(self, export_path: Union[str, Path]) -> None: """Export simulation. @@ -864,34 +912,17 @@ def __init__( ) if default_values: - # Default values - self.set_geom_distance_tolerance() - self.set_max_impact() - self.set_colorimetric_standard_CIE_1931() - self.set_dispersion() # self.set_fast_transmission_gathering() self.set_ambient_material_file_uri() self.set_weight() self.set_light_expert() # Default job properties self.set_stop_condition_rays_number().set_stop_condition_duration().set_automatic_save_frequency() - - def set_geom_distance_tolerance(self, value: float = 0.01) -> SimulationDirect: - """Set the geometry distance tolerance. - - Parameters - ---------- - value : float - Maximum distance in mm to consider two faces as tangent. - By default, ``0.01``. - - Returns - ------- - ansys.speos.core.simulation.SimulationDirect - Direct simulation - """ - self._simulation_template.direct_mc_simulation_template.geom_distance_tolerance = value - return self + # Default values + self.set_max_impact() + self.set_colorimetric_standard_CIE_1931() + self.set_dispersion() + self.geom_distance_tolerance = 0.01 def set_max_impact(self, value: int = 100) -> SimulationDirect: """Define a value to determine the maximum number of ray impacts during propagation. @@ -1193,8 +1224,12 @@ def __init__( ) if default_values: + # self.set_fast_transmission_gathering() + self.set_ambient_material_file_uri() + # Default job properties + self.set_stop_condition_duration().set_stop_condition_passes_number().set_automatic_save_frequency() # Default values - self.set_geom_distance_tolerance() + self.geom_distance_tolerance = 0.01 self.set_max_impact() self.set_weight() self.set_colorimetric_standard_CIE_1931() @@ -1202,27 +1237,6 @@ def __init__( self.set_splitting() self.set_number_of_gathering_rays_per_source() self.set_maximum_gathering_error() - # self.set_fast_transmission_gathering() - self.set_ambient_material_file_uri() - # Default job properties - self.set_stop_condition_duration().set_stop_condition_passes_number().set_automatic_save_frequency() - - def set_geom_distance_tolerance(self, value: float = 0.01) -> SimulationInverse: - """Set the geometry distance tolerance. - - Parameters - ---------- - value : float - Maximum distance in mm to consider two faces as tangent. - By default, ``0.01`` - - Returns - ------- - ansys.speos.core.simulation.SimulationInverse - Inverse simulation - """ - self._simulation_template.inverse_mc_simulation_template.geom_distance_tolerance = value - return self def set_max_impact(self, value: int = 100) -> SimulationInverse: """Define a value to determine the maximum number of ray impacts during propagation. @@ -1592,31 +1606,14 @@ def __init__( ) if default_values: + self.set_ambient_material_file_uri() + # Default job parameters + self.set_light_expert().set_impact_report() # Default values - self.set_geom_distance_tolerance() + self.geom_distance_tolerance = 0.01 self.set_max_impact() self.set_weight() self.set_colorimetric_standard_CIE_1931() - self.set_ambient_material_file_uri() - # Default job parameters - self.set_light_expert().set_impact_report() - - def set_geom_distance_tolerance(self, value: float = 0.01) -> SimulationInteractive: - """Set the geometry distance tolerance. - - Parameters - ---------- - value : float - Maximum distance in mm to consider two faces as tangent. - By default, ``0.01`` - - Returns - ------- - ansys.speos.core.simulation.SimulationInteractive - Interactive simulation - """ - self._simulation_template.interactive_simulation_template.geom_distance_tolerance = value - return self def set_max_impact(self, value: int = 100) -> SimulationInteractive: """Define a value to determine the maximum number of ray impacts during propagation. @@ -2564,37 +2561,6 @@ def __init__( self.integration_angle = 2 self.stop_condition_ray_number = 100000 - @property - def geom_distance_tolerance(self) -> float: - """Return the geometry distance tolerance. - - Returns - ------- - float - Maximum distance in mm to consider two faces as tangent. - """ - return ( - self._simulation_template.virtual_bsdf_bench_simulation_template.geom_distance_tolerance - ) - - @geom_distance_tolerance.setter - def geom_distance_tolerance(self, value: float) -> None: - """Set the geometry distance tolerance. - - Parameters - ---------- - value : float - Maximum distance in mm to consider two faces as tangent. - By default, ``0.01`` - - Returns - ------- - None - """ - self._simulation_template.virtual_bsdf_bench_simulation_template.geom_distance_tolerance = ( - value - ) - @property def max_impact(self) -> int: """Return the maximum number of impacts. diff --git a/tests/core/test_simulation.py b/tests/core/test_simulation.py index ba1bbf9d4..32d668f41 100644 --- a/tests/core/test_simulation.py +++ b/tests/core/test_simulation.py @@ -72,7 +72,7 @@ def test_create_direct(speos: Speos): # Change value # geom_distance_tolerance - sim1.set_geom_distance_tolerance(value=0.1) + sim1.geom_distance_tolerance = 0.1 assert simulation_template.geom_distance_tolerance == 0.1 # max_impact @@ -173,7 +173,7 @@ def test_create_inverse(speos: Speos): # Change value # geom_distance_tolerance - sim1.set_geom_distance_tolerance(value=0.1) + sim1.geom_distance_tolerance = 0.1 assert simulation_template.geom_distance_tolerance == 0.1 # max_impact @@ -288,7 +288,7 @@ def test_create_interactive(speos: Speos): # Change value # geom_distance_tolerance - sim1.set_geom_distance_tolerance(value=0.1) + sim1.geom_distance_tolerance = 0.1 assert sim1._simulation_template.interactive_simulation_template.geom_distance_tolerance == 0.1 # max_impact @@ -684,7 +684,7 @@ def test_commit(speos: Speos): assert p.scene_link.get().simulations[0] == sim1._simulation_instance # Change only in local not committed (on template, on instance) - sim1.set_geom_distance_tolerance(value=0.1) + sim1.geom_distance_tolerance = 0.1 assert sim1.simulation_template_link.get() != sim1._simulation_template sim1.set_sensor_paths(["Irradiance.1, Irradiance.2"]) assert p.scene_link.get().simulations[0] != sim1._simulation_instance @@ -733,7 +733,7 @@ def test_reset(speos: Speos): assert sim1._job.HasField("direct_mc_simulation_properties") # local # Change local data (on template, on instance) - sim1.set_geom_distance_tolerance(value=0.1) + sim1.geom_distance_tolerance = 0.1 assert sim1.simulation_template_link.get() != sim1._simulation_template sim1.set_sensor_paths(["Irradiance.1, Irradiance.2"]) assert p.scene_link.get().simulations[0] != sim1._simulation_instance @@ -801,7 +801,7 @@ def test_direct_modify_after_reset(speos: Speos): # Modify after a reset # Template assert sim1._simulation_template.direct_mc_simulation_template.geom_distance_tolerance == 0.01 - sim1.set_geom_distance_tolerance(value=0.05) + sim1.geom_distance_tolerance = 0.05 assert sim1._simulation_template.direct_mc_simulation_template.geom_distance_tolerance == 0.05 # Props @@ -872,7 +872,7 @@ def test_inverse_modify_after_reset(speos: Speos): # Modify after a reset # Template assert sim1._simulation_template.inverse_mc_simulation_template.geom_distance_tolerance == 0.01 - sim1.set_geom_distance_tolerance(value=0.05) + sim1.geom_distance_tolerance = 0.05 assert sim1._simulation_template.inverse_mc_simulation_template.geom_distance_tolerance == 0.05 # Props @@ -935,7 +935,7 @@ def test_interactive_modify_after_reset(speos: Speos): # Modify after a reset # Template assert sim1._simulation_template.interactive_simulation_template.geom_distance_tolerance == 0.01 - sim1.set_geom_distance_tolerance(value=0.05) + sim1.geom_distance_tolerance = 0.05 assert sim1._simulation_template.interactive_simulation_template.geom_distance_tolerance == 0.05 # Props From 8047d6ff10b237395c765fd9c147789489b81d93 Mon Sep 17 00:00:00 2001 From: plu Date: Mon, 17 Nov 2025 20:46:59 +0000 Subject: [PATCH 14/16] refactor max_impact from child to base --- src/ansys/speos/core/simulation.py | 152 +++++++++++------------------ tests/core/test_simulation.py | 6 +- 2 files changed, 62 insertions(+), 96 deletions(-) diff --git a/src/ansys/speos/core/simulation.py b/src/ansys/speos/core/simulation.py index dca75c9d1..cd2ed70a6 100644 --- a/src/ansys/speos/core/simulation.py +++ b/src/ansys/speos/core/simulation.py @@ -473,6 +473,57 @@ def geom_distance_tolerance(self, value: float) -> None: case _: raise TypeError(f"Unknown simulation template type: {tmpl}") + @property + def max_impact(self) -> int: + """Return the maximum number of impacts. + + Returns + ------- + int + The maximum number of impacts. + """ + tmpl = self._simulation_template + match tmpl: + case _ if tmpl.HasField("virtual_bsdf_bench_simulation_template"): + return tmpl.virtual_bsdf_bench_simulation_template.max_impact + case _ if tmpl.HasField("direct_mc_simulation_template"): + return tmpl.direct_mc_simulation_template.max_impact + case _ if tmpl.HasField("inverse_mc_simulation_template"): + return tmpl.inverse_mc_simulation_template.max_impact + case _ if tmpl.HasField("interactive_simulation_template"): + return tmpl.interactive_simulation_template.max_impact + case _: + raise TypeError(f"Unknown simulation template type: {tmpl}") + + @max_impact.setter + def max_impact(self, value: int) -> None: + """Define a value to determine the maximum number of ray impacts during propagation. + + When a ray has interacted N times with the geometry, the propagation of the ray stops. + + Parameters + ---------- + value : int + The maximum number of impacts. + By default, ``100``. + + Returns + ------- + None + """ + tmpl = self._simulation_template + match tmpl: + case _ if tmpl.HasField("virtual_bsdf_bench_simulation_template"): + tmpl.virtual_bsdf_bench_simulation_template.max_impact = value + case _ if tmpl.HasField("direct_mc_simulation_template"): + tmpl.direct_mc_simulation_template.max_impact = value + case _ if tmpl.HasField("inverse_mc_simulation_template"): + tmpl.inverse_mc_simulation_template.max_impact = value + case _ if tmpl.HasField("interactive_simulation_template"): + tmpl.interactive_simulation_template.max_impact = value + case _: + raise TypeError(f"Unknown simulation template type: {tmpl}") + def export(self, export_path: Union[str, Path]) -> None: """Export simulation. @@ -919,29 +970,10 @@ def __init__( # Default job properties self.set_stop_condition_rays_number().set_stop_condition_duration().set_automatic_save_frequency() # Default values - self.set_max_impact() self.set_colorimetric_standard_CIE_1931() self.set_dispersion() self.geom_distance_tolerance = 0.01 - - def set_max_impact(self, value: int = 100) -> SimulationDirect: - """Define a value to determine the maximum number of ray impacts during propagation. - - When a ray has interacted N times with the geometry, the propagation of the ray stops. - - Parameters - ---------- - value : int - The maximum number of impacts. - By default, ``100``. - - Returns - ------- - ansys.speos.core.simulation.SimulationDirect - Direct simulation - """ - self._simulation_template.direct_mc_simulation_template.max_impact = value - return self + self.max_impact = 100 def set_weight(self) -> BaseSimulation.Weight: """Activate weight. Highly recommended to fill. @@ -1230,7 +1262,7 @@ def __init__( self.set_stop_condition_duration().set_stop_condition_passes_number().set_automatic_save_frequency() # Default values self.geom_distance_tolerance = 0.01 - self.set_max_impact() + self.max_impact = 100 self.set_weight() self.set_colorimetric_standard_CIE_1931() self.set_dispersion() @@ -1238,25 +1270,6 @@ def __init__( self.set_number_of_gathering_rays_per_source() self.set_maximum_gathering_error() - def set_max_impact(self, value: int = 100) -> SimulationInverse: - """Define a value to determine the maximum number of ray impacts during propagation. - - When a ray has interacted N times with the geometry, the propagation of the ray stops. - - Parameters - ---------- - value : int - The maximum number of impacts. - By default, ``100``. - - Returns - ------- - ansys.speos.core.simulation.SimulationInverse - Inverse simulation - """ - self._simulation_template.inverse_mc_simulation_template.max_impact = value - return self - def set_weight(self) -> BaseSimulation.Weight: """Activate weight. Highly recommended to fill. @@ -1611,29 +1624,10 @@ def __init__( self.set_light_expert().set_impact_report() # Default values self.geom_distance_tolerance = 0.01 - self.set_max_impact() + self.max_impact = 100 self.set_weight() self.set_colorimetric_standard_CIE_1931() - def set_max_impact(self, value: int = 100) -> SimulationInteractive: - """Define a value to determine the maximum number of ray impacts during propagation. - - When a ray has interacted N times with the geometry, the propagation of the ray stops. - - Parameters - ---------- - value : int - The maximum number of impacts. - By default, ``100``. - - Returns - ------- - ansys.speos.core.simulation.SimulationInteractive - Interactive simulation - """ - self._simulation_template.interactive_simulation_template.max_impact = value - return self - def set_weight(self) -> BaseSimulation.Weight: """Activate weight. Highly recommended to fill. @@ -2551,44 +2545,16 @@ def __init__( self._mode = self.set_mode_all_characteristics() if default_values: - self.geom_distance_tolerance = 0.01 - self.max_impact = 100 - self.set_weight() - self.set_colorimetric_standard_CIE_1931() self.analysis_x_ratio = 100 self.analysis_y_ratio = 100 self.axis_system = [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1] self.integration_angle = 2 self.stop_condition_ray_number = 100000 - - @property - def max_impact(self) -> int: - """Return the maximum number of impacts. - - Returns - ------- - int - The maximum number of impacts. - """ - return self._simulation_template.virtual_bsdf_bench_simulation_template.max_impact - - @max_impact.setter - def max_impact(self, value: int) -> None: - """Define a value to determine the maximum number of ray impacts during propagation. - - When a ray has interacted N times with the geometry, the propagation of the ray stops. - - Parameters - ---------- - value : int - The maximum number of impacts. - By default, ``100``. - - Returns - ------- - None - """ - self._simulation_template.virtual_bsdf_bench_simulation_template.max_impact = value + # default simulation properties + self.geom_distance_tolerance = 0.01 + self.max_impact = 100 + self.set_weight() + self.set_colorimetric_standard_CIE_1931() @property def integration_angle(self) -> float: diff --git a/tests/core/test_simulation.py b/tests/core/test_simulation.py index 32d668f41..0ab0a96a3 100644 --- a/tests/core/test_simulation.py +++ b/tests/core/test_simulation.py @@ -76,7 +76,7 @@ def test_create_direct(speos: Speos): assert simulation_template.geom_distance_tolerance == 0.1 # max_impact - sim1.set_max_impact(value=200) + sim1.max_impact = 200 assert simulation_template.max_impact == 200 # weight - minimum_energy_percentage @@ -177,7 +177,7 @@ def test_create_inverse(speos: Speos): assert simulation_template.geom_distance_tolerance == 0.1 # max_impact - sim1.set_max_impact(value=200) + sim1.max_impact = 200 assert simulation_template.max_impact == 200 # weight - minimum_energy_percentage @@ -292,7 +292,7 @@ def test_create_interactive(speos: Speos): assert sim1._simulation_template.interactive_simulation_template.geom_distance_tolerance == 0.1 # max_impact - sim1.set_max_impact(value=200) + sim1.max_impact = 200 assert sim1._simulation_template.interactive_simulation_template.max_impact == 200 # weight - minimum_energy_percentage From ba35267359f289bddc69edf84dbae377ddef0a4a Mon Sep 17 00:00:00 2001 From: plu Date: Fri, 21 Nov 2025 12:02:36 +0000 Subject: [PATCH 15/16] check type in the function init --- src/ansys/speos/core/simulation.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/ansys/speos/core/simulation.py b/src/ansys/speos/core/simulation.py index cd2ed70a6..d54c53e83 100644 --- a/src/ansys/speos/core/simulation.py +++ b/src/ansys/speos/core/simulation.py @@ -434,14 +434,14 @@ def geom_distance_tolerance(self) -> float: Maximum distance in mm to consider two faces as tangent. """ tmpl = self._simulation_template - match tmpl: - case _ if tmpl.HasField("virtual_bsdf_bench_simulation_template"): + match self._type.__name__: + case "SimulationVirtualBSDF": return tmpl.virtual_bsdf_bench_simulation_template.geom_distance_tolerance - case _ if tmpl.HasField("direct_mc_simulation_template"): + case "SimulationDirect": return tmpl.direct_mc_simulation_template.geom_distance_tolerance - case _ if tmpl.HasField("inverse_mc_simulation_template"): + case "SimulationInverse": return tmpl.inverse_mc_simulation_template.geom_distance_tolerance - case _ if tmpl.HasField("interactive_simulation_template"): + case "SimulationInteractive": return tmpl.interactive_simulation_template.geom_distance_tolerance case _: raise TypeError(f"Unknown simulation template type: {tmpl}") @@ -461,14 +461,14 @@ def geom_distance_tolerance(self, value: float) -> None: None """ tmpl = self._simulation_template - match tmpl: - case _ if tmpl.HasField("virtual_bsdf_bench_simulation_template"): + match self._type.__name__: + case "SimulationVirtualBSDF": tmpl.virtual_bsdf_bench_simulation_template.geom_distance_tolerance = value - case _ if tmpl.HasField("direct_mc_simulation_template"): + case "SimulationDirect": tmpl.direct_mc_simulation_template.geom_distance_tolerance = value - case _ if tmpl.HasField("inverse_mc_simulation_template"): + case "SimulationInverse": tmpl.inverse_mc_simulation_template.geom_distance_tolerance = value - case _ if tmpl.HasField("interactive_simulation_template"): + case "SimulationInteractive": tmpl.interactive_simulation_template.geom_distance_tolerance = value case _: raise TypeError(f"Unknown simulation template type: {tmpl}") @@ -961,6 +961,7 @@ def __init__( metadata=metadata, simulation_instance=simulation_instance, ) + self._type = type(self) if default_values: # self.set_fast_transmission_gathering() @@ -1254,6 +1255,7 @@ def __init__( metadata=metadata, simulation_instance=simulation_instance, ) + self._type = type(self) if default_values: # self.set_fast_transmission_gathering() @@ -1617,6 +1619,7 @@ def __init__( metadata=metadata, simulation_instance=simulation_instance, ) + self._type = type(self) if default_values: self.set_ambient_material_file_uri() @@ -2535,6 +2538,7 @@ def __init__( metadata=metadata, simulation_instance=simulation_instance, ) + self._type = type(self) self._wavelengths_range = None self._sensor_sampling_mode = None From 8888e1d36cf5654b330782d2ecb02d83499d18de Mon Sep 17 00:00:00 2001 From: plu Date: Fri, 21 Nov 2025 12:55:10 +0000 Subject: [PATCH 16/16] simplify the class type name --- src/ansys/speos/core/simulation.py | 73 +++++++++--------------------- 1 file changed, 21 insertions(+), 52 deletions(-) diff --git a/src/ansys/speos/core/simulation.py b/src/ansys/speos/core/simulation.py index d54c53e83..c00e14d46 100644 --- a/src/ansys/speos/core/simulation.py +++ b/src/ansys/speos/core/simulation.py @@ -344,6 +344,7 @@ def __init__( metadata = {} # Attribute representing the kind of simulation. self._type = None + self._template_class = None self._light_expert_changed = False if simulation_instance is None: @@ -433,18 +434,10 @@ def geom_distance_tolerance(self) -> float: float Maximum distance in mm to consider two faces as tangent. """ - tmpl = self._simulation_template - match self._type.__name__: - case "SimulationVirtualBSDF": - return tmpl.virtual_bsdf_bench_simulation_template.geom_distance_tolerance - case "SimulationDirect": - return tmpl.direct_mc_simulation_template.geom_distance_tolerance - case "SimulationInverse": - return tmpl.inverse_mc_simulation_template.geom_distance_tolerance - case "SimulationInteractive": - return tmpl.interactive_simulation_template.geom_distance_tolerance - case _: - raise TypeError(f"Unknown simulation template type: {tmpl}") + if self._template_class is not None: + return getattr(self._simulation_template, self._template_class).geom_distance_tolerance + else: + raise TypeError(f"Unknown simulation template type: {self._template_class}") @geom_distance_tolerance.setter def geom_distance_tolerance(self, value: float) -> None: @@ -460,18 +453,10 @@ def geom_distance_tolerance(self, value: float) -> None: ------- None """ - tmpl = self._simulation_template - match self._type.__name__: - case "SimulationVirtualBSDF": - tmpl.virtual_bsdf_bench_simulation_template.geom_distance_tolerance = value - case "SimulationDirect": - tmpl.direct_mc_simulation_template.geom_distance_tolerance = value - case "SimulationInverse": - tmpl.inverse_mc_simulation_template.geom_distance_tolerance = value - case "SimulationInteractive": - tmpl.interactive_simulation_template.geom_distance_tolerance = value - case _: - raise TypeError(f"Unknown simulation template type: {tmpl}") + if self._template_class is not None: + getattr(self._simulation_template, self._template_class).geom_distance_tolerance = value + else: + raise TypeError(f"Unknown simulation template type: {self._template_class}") @property def max_impact(self) -> int: @@ -482,18 +467,10 @@ def max_impact(self) -> int: int The maximum number of impacts. """ - tmpl = self._simulation_template - match tmpl: - case _ if tmpl.HasField("virtual_bsdf_bench_simulation_template"): - return tmpl.virtual_bsdf_bench_simulation_template.max_impact - case _ if tmpl.HasField("direct_mc_simulation_template"): - return tmpl.direct_mc_simulation_template.max_impact - case _ if tmpl.HasField("inverse_mc_simulation_template"): - return tmpl.inverse_mc_simulation_template.max_impact - case _ if tmpl.HasField("interactive_simulation_template"): - return tmpl.interactive_simulation_template.max_impact - case _: - raise TypeError(f"Unknown simulation template type: {tmpl}") + if self._template_class is not None: + return getattr(self._simulation_template, self._template_class).max_impact + else: + raise TypeError(f"Unknown simulation template type: {self._template_class}") @max_impact.setter def max_impact(self, value: int) -> None: @@ -511,18 +488,10 @@ def max_impact(self, value: int) -> None: ------- None """ - tmpl = self._simulation_template - match tmpl: - case _ if tmpl.HasField("virtual_bsdf_bench_simulation_template"): - tmpl.virtual_bsdf_bench_simulation_template.max_impact = value - case _ if tmpl.HasField("direct_mc_simulation_template"): - tmpl.direct_mc_simulation_template.max_impact = value - case _ if tmpl.HasField("inverse_mc_simulation_template"): - tmpl.inverse_mc_simulation_template.max_impact = value - case _ if tmpl.HasField("interactive_simulation_template"): - tmpl.interactive_simulation_template.max_impact = value - case _: - raise TypeError(f"Unknown simulation template type: {tmpl}") + if self._template_class is not None: + getattr(self._simulation_template, self._template_class).max_impact = value + else: + raise TypeError(f"Unknown simulation template type: {self._template_class}") def export(self, export_path: Union[str, Path]) -> None: """Export simulation. @@ -961,7 +930,7 @@ def __init__( metadata=metadata, simulation_instance=simulation_instance, ) - self._type = type(self) + self._template_class = "direct_mc_simulation_template" if default_values: # self.set_fast_transmission_gathering() @@ -1255,7 +1224,7 @@ def __init__( metadata=metadata, simulation_instance=simulation_instance, ) - self._type = type(self) + self._template_class = "inverse_mc_simulation_template" if default_values: # self.set_fast_transmission_gathering() @@ -1619,7 +1588,7 @@ def __init__( metadata=metadata, simulation_instance=simulation_instance, ) - self._type = type(self) + self._template_class = "interactive_simulation_template" if default_values: self.set_ambient_material_file_uri() @@ -2538,7 +2507,7 @@ def __init__( metadata=metadata, simulation_instance=simulation_instance, ) - self._type = type(self) + self._template_class = "virtual_bsdf_bench_simulation_template" self._wavelengths_range = None self._sensor_sampling_mode = None