diff --git a/doc/changelog.d/653.added.md b/doc/changelog.d/653.added.md new file mode 100644 index 000000000..eb1531076 --- /dev/null +++ b/doc/changelog.d/653.added.md @@ -0,0 +1 @@ +Intensitysensor \ No newline at end of file diff --git a/src/ansys/speos/core/sensor.py b/src/ansys/speos/core/sensor.py index ae122bfd6..11995a316 100644 --- a/src/ansys/speos/core/sensor.py +++ b/src/ansys/speos/core/sensor.py @@ -25,7 +25,7 @@ from __future__ import annotations from difflib import SequenceMatcher -from typing import List, Mapping, Optional, Union +from typing import Collection, List, Mapping, Optional, Union import uuid import warnings @@ -3777,3 +3777,630 @@ def set_geometries(self, geometries: [List[GeoRef]]) -> Sensor3DIrradiance: gr.to_native_link() for gr in geometries ] return self + + +class SensorXMPIntensity(BaseSensor): + """Class for XMP intensity sensor. + + Parameters + ---------- + project + name + description + metadata + sensor_instance + default_values + """ + + def __init__( + self, + project: project.Project, + name: str, + description: str = "", + metadata: Optional[Mapping[str, str]] = None, + sensor_instance: Optional[ProtoScene.SensorInstance] = None, + default_values: bool = True, + ) -> None: + if metadata is None: + metadata = {} + + super().__init__( + project=project, + name=name, + description=description, + metadata=metadata, + sensor_instance=sensor_instance, + ) + + # Attribute gathering more complex intensity type + self._type = None + self._layer_type = None + self._cell_diameter = None + + if default_values: + # Default values template + self.set_type_photometric() + self.set_orientation_x_as_meridian() + self.set_viewing_direction_from_source() + # Default values properties + self.set_axis_system().set_layer_type_none() + + @property + def nearfield(self) -> bool: + """Property containing if the sensor is positioned in nearfield or infinity.""" + if self._sensor_template.intensity_sensor_template.HasField("near_field"): + return True + else: + return False + + @nearfield.setter + def nearfield(self, value): + if value: + if not self._sensor_template.intensity_sensor_template.HasField("near_field"): + self._sensor_template.intensity_sensor_template.near_field.SetInParent() + self.cell_distance = 10 + self.cell_diameter = 0.3491 + else: + if self._sensor_template.intensity_sensor_template.HasField("near_field"): + self._sensor_template.intensity_sensor_template.ClearField("near_field") + + @property + def cell_distance(self): + """Distance of the Detector to origin in mm. + + By default, ``10`` + """ + if self.nearfield: + return self._sensor_template.intensity_sensor_template.near_field.cell_distance + else: + return None + + @cell_distance.setter + def cell_distance(self, value): + if self.nearfield: + self._sensor_template.intensity_sensor_template.near_field.cell_distance = value + + else: + raise TypeError("Sensor position is not in nearfield") + + @property + def cell_diameter(self): + """Cell diameter in mm. + + By default, ``0.3491`` + """ + if self.nearfield: + diameter = self.cell_distance * np.tan( + np.radians( + self._sensor_template.intensity_sensor_template.near_field.cell_integration_angle + ) + ) + return diameter + else: + return None + + @cell_diameter.setter + def cell_diameter(self, value): + if self.nearfield: + self._sensor_template.intensity_sensor_template.near_field.cell_integration_angle = ( + np.degrees(np.arctan(value / 2 / self.cell_distance)) + ) + else: + raise TypeError("Sensor position is not in nearfield") + + @property + def type(self) -> str: + """Type of sensor. + + Returns + ------- + str + Sensor type as string + """ + if type(self._type) is str: + return self._type + elif isinstance(self._type, BaseSensor.Colorimetric): + return "Colorimetric" + elif isinstance(self._type, BaseSensor.Spectral): + return "Spectral" + else: + return self._type + + @property + def colorimetric(self) -> Union[None, BaseSensor.Colorimetric]: + """Property containing all options in regard to the Colorimetric sensor properties. + + Returns + ------- + Union[None, ansys.speos.core.sensor.BaseSensor.Colorimetric] + Instance of Colorimetric Class for this sensor feature + """ + if isinstance(self._type, BaseSensor.Colorimetric): + return self._type + else: + return None + + @property + def spectral(self) -> Union[None, BaseSensor.Spectral]: + """Property containing all options in regard to the Spectral sensor properties. + + Returns + ------- + Union[None, ansys.speos.core.sensor.BaseSensor.Spectral] + Instance of Spectral Class for this sensor feature + """ + if isinstance(self._type, BaseSensor.Spectral): + return self._type + else: + return None + + @property + def layer( + self, + ) -> Union[ + None, + SensorIrradiance, + BaseSensor.LayerTypeFace, + BaseSensor.LayerTypeSequence, + BaseSensor.LayerTypeIncidenceAngle, + ]: + """Property containing all options in regard to the layer separation properties. + + Returns + ------- + Union[\ + None,\ + ansys.speos.core.sensor.SensorIrradiance,\ + ansys.speos.core.sensor.BaseSensor.LayerTypeFace,\ + ansys.speos.core.sensor.BaseSensor.LayerTypeSequence,\ + ansys.speos.core.sensor.BaseSensor.LayerTypeIncidenceAngle\ + ] + Instance of Layertype Class for this sensor feature + """ + return self._layer_type + + def set_orientation_x_as_meridian(self): + """Set Orientation type: X As Meridian, Y as Parallel.""" + self._sensor_template.intensity_sensor_template.intensity_orientation_x_as_meridian.SetInParent() + self._set_default_dimension_values() + + def set_orientation_x_as_parallel(self): + """Set Orientation type: X as Parallel, Y As Meridian.""" + self._sensor_template.intensity_sensor_template.intensity_orientation_x_as_parallel.SetInParent() + self._set_default_dimension_values() + + def set_orientation_conoscopic(self): + """Set Orientation type: Conoscopic.""" + self._sensor_template.intensity_sensor_template.intensity_orientation_conoscopic.SetInParent() + self._set_default_dimension_values() + + def set_viewing_direction_from_source(self): + """Set viewing direction from Source Looking At Sensor.""" + self._sensor_template.intensity_sensor_template.from_source_looking_at_sensor.SetInParent() + + def set_viewing_direction_from_sensor(self): + """Set viewing direction from Sensor Looking At Source.""" + self._sensor_template.intensity_sensor_template.from_sensor_looking_at_source.SetInParent() + + def _set_default_dimension_values(self): + template = self._sensor_template.intensity_sensor_template + if template.HasField("intensity_orientation_conoscopic"): + self.theta_max = 45 + self.theta_sampling = 90 + elif template.HasField("intensity_orientation_x_as_parallel"): + self.x_start = -30 + self.x_end = 30 + self.x_sampling = 120 + self.y_start = -45 + self.y_end = 45 + self.y_sampling = 180 + elif template.HasField("intensity_orientation_x_as_meridian"): + self.y_start = -30 + self.y_end = 30 + self.y_sampling = 120 + self.x_start = -45 + self.x_end = 45 + self.x_sampling = 180 + + @property + def x_start(self) -> float: + """The minimum value on x-axis (deg). + + By default, ``45``. + """ + template = self._sensor_template.intensity_sensor_template + if template.HasField("intensity_orientation_conoscopic"): + return None + elif template.HasField("intensity_orientation_x_as_parallel"): + return template.intensity_orientation_x_as_parallel.intensity_dimensions.x_start + elif template.HasField("intensity_orientation_x_as_meridian"): + return template.intensity_orientation_x_as_meridian.intensity_dimensions.x_start + + @x_start.setter + def x_start(self, value: float): + template = self._sensor_template.intensity_sensor_template + if template.HasField("intensity_orientation_conoscopic"): + raise TypeError("Conoscopic Sensor has no x_start dimension") + elif template.HasField("intensity_orientation_x_as_parallel"): + template.intensity_orientation_x_as_parallel.intensity_dimensions.x_start = value + elif template.HasField("intensity_orientation_x_as_meridian"): + template.intensity_orientation_x_as_meridian.intensity_dimensions.x_start = value + + @property + def x_end(self) -> float: + """The maximum value on x-axis (deg). + + By default, ``45``. + """ + template = self._sensor_template.intensity_sensor_template + if template.HasField("intensity_orientation_conoscopic"): + return None + elif template.HasField("intensity_orientation_x_as_parallel"): + return template.intensity_orientation_x_as_parallel.intensity_dimensions.x_end + elif template.HasField("intensity_orientation_x_as_meridian"): + return template.intensity_orientation_x_as_meridian.intensity_dimensions.x_end + + @x_end.setter + def x_end(self, value: float): + template = self._sensor_template.intensity_sensor_template + if template.HasField("intensity_orientation_conoscopic"): + raise TypeError("Conoscopic Sensor has no x_end dimension") + elif template.HasField("intensity_orientation_x_as_parallel"): + template.intensity_orientation_x_as_parallel.intensity_dimensions.x_end = value + elif template.HasField("intensity_orientation_x_as_meridian"): + template.intensity_orientation_x_as_meridian.intensity_dimensions.x_end = value + + @property + def x_sampling(self) -> int: + """Pixel sampling along x-Axis. + + By default, ``100`` + """ + template = self._sensor_template.intensity_sensor_template + if template.HasField("intensity_orientation_conoscopic"): + return None + elif template.HasField("intensity_orientation_x_as_parallel"): + return template.intensity_orientation_x_as_parallel.intensity_dimensions.x_sampling + elif template.HasField("intensity_orientation_x_as_meridian"): + return template.intensity_orientation_x_as_meridian.intensity_dimensions.x_sampling + + @x_sampling.setter + def x_sampling(self, value: int): + template = self._sensor_template.intensity_sensor_template + if template.HasField("intensity_orientation_conoscopic"): + raise TypeError("Conoscopic Sensor has no x_sampling dimension") + elif template.HasField("intensity_orientation_x_as_parallel"): + template.intensity_orientation_x_as_parallel.intensity_dimensions.x_sampling = value + elif template.HasField("intensity_orientation_x_as_meridian"): + template.intensity_orientation_x_as_meridian.intensity_dimensions.x_sampling = value + + @property + def y_end(self) -> float: + """The maximum value on y-axis (deg). + + By default, ``30``. + """ + template = self._sensor_template.intensity_sensor_template + if template.HasField("intensity_orientation_conoscopic"): + return None + elif template.HasField("intensity_orientation_x_as_parallel"): + return template.intensity_orientation_x_as_parallel.intensity_dimensions.y_end + elif template.HasField("intensity_orientation_x_as_meridian"): + return template.intensity_orientation_x_as_meridian.intensity_dimensions.y_end + + @y_end.setter + def y_end(self, value: float): + template = self._sensor_template.intensity_sensor_template + if template.HasField("intensity_orientation_conoscopic"): + raise TypeError("Conoscopic Sensor has no y_end dimension") + elif template.HasField("intensity_orientation_x_as_parallel"): + template.intensity_orientation_x_as_parallel.intensity_dimensions.y_end = value + elif template.HasField("intensity_orientation_x_as_meridian"): + template.intensity_orientation_x_as_meridian.intensity_dimensions.y_end = value + + @property + def y_start(self) -> float: + """The minimum value on x-axis (deg). + + By default, ``-30``. + """ + template = self._sensor_template.intensity_sensor_template + if template.HasField("intensity_orientation_conoscopic"): + return None + elif template.HasField("intensity_orientation_x_as_parallel"): + return template.intensity_orientation_x_as_parallel.intensity_dimensions.y_start + elif template.HasField("intensity_orientation_x_as_meridian"): + return template.intensity_orientation_x_as_meridian.intensity_dimensions.y_start + + @y_start.setter + def y_start(self, value: float): + template = self._sensor_template.intensity_sensor_template + if template.HasField("intensity_orientation_conoscopic"): + raise TypeError("Conoscopic Sensor has no y_start dimension") + elif template.HasField("intensity_orientation_x_as_parallel"): + template.intensity_orientation_x_as_parallel.intensity_dimensions.y_start = value + elif template.HasField("intensity_orientation_x_as_meridian"): + template.intensity_orientation_x_as_meridian.intensity_dimensions.y_start = value + + @property + def y_sampling(self) -> int: + """Sampling along y-axis. + + By default, ``100``. + """ + template = self._sensor_template.intensity_sensor_template + if template.HasField("intensity_orientation_conoscopic"): + return None + elif template.HasField("intensity_orientation_x_as_parallel"): + return template.intensity_orientation_x_as_parallel.intensity_dimensions.y_sampling + elif template.HasField("intensity_orientation_x_as_meridian"): + return template.intensity_orientation_x_as_meridian.intensity_dimensions.y_sampling + + @y_sampling.setter + def y_sampling(self, value: int): + template = self._sensor_template.intensity_sensor_template + if template.HasField("intensity_orientation_conoscopic"): + raise TypeError("Conoscopic Sensor has no y_sampling dimension") + elif template.HasField("intensity_orientation_x_as_parallel"): + template.intensity_orientation_x_as_parallel.intensity_dimensions.y_sampling = value + elif template.HasField("intensity_orientation_x_as_meridian"): + template.intensity_orientation_x_as_meridian.intensity_dimensions.y_sampling = value + + @property + def theta_max(self) -> float: + """Maximum theta angle on consocopic type (in deg). + + By default, ``45``. + """ + template = self._sensor_template.intensity_sensor_template + if template.HasField("intensity_orientation_conoscopic"): + return ( + template.intensity_orientation_conoscopic.conoscopic_intensity_dimensions.theta_max + ) + else: + return None + + @theta_max.setter + def theta_max(self, value: float): + template = self._sensor_template.intensity_sensor_template + if template.HasField("intensity_orientation_conoscopic"): + template.intensity_orientation_conoscopic.conoscopic_intensity_dimensions.theta_max = ( + value + ) + else: + raise TypeError("Only Conoscopic Sensor has theta_max dimension") + + @property + def theta_sampling(self) -> int: + """Sampling on conoscopic type. + + By default, ``90``. + """ + template = self._sensor_template.intensity_sensor_template + if template.HasField("intensity_orientation_conoscopic"): + return ( + template.intensity_orientation_conoscopic.conoscopic_intensity_dimensions.sampling + ) + else: + return None + + @theta_sampling.setter + def theta_sampling(self, value: int): + template = self._sensor_template.intensity_sensor_template + if template.HasField("intensity_orientation_conoscopic"): + template.intensity_orientation_conoscopic.conoscopic_intensity_dimensions.sampling = ( + value + ) + else: + raise TypeError("Only Conoscopic Sensor has theta_max dimension") + + def set_type_photometric(self) -> SensorXMPIntensity: + """Set type photometric. + + The sensor considers the visible spectrum and gets the results in lm/m2 or lx. + + Returns + ------- + ansys.speos.core.sensor.SensorIrradiance + Irradiance sensor + """ + self._sensor_template.intensity_sensor_template.sensor_type_photometric.SetInParent() + self._type = "Photometric" + return self + + def set_type_colorimetric(self) -> BaseSensor.Colorimetric: + """Set type colorimetric. + + The sensor will generate color results without any spectral data or layer separation + in lx or W//m2. + + Returns + ------- + ansys.speos.core.sensor.BaseSensor.Colorimetric + Colorimetric type. + """ + if self._type is None and self._sensor_template.intensity_sensor_template.HasField( + "sensor_type_colorimetric" + ): + # Happens in case of project created via load of speos file + self._type = BaseSensor.Colorimetric( + sensor_type_colorimetric=self._sensor_template.intensity_sensor_template.sensor_type_colorimetric, + default_values=False, + stable_ctr=True, + ) + elif not isinstance(self._type, BaseSensor.Colorimetric): + # if the _type is not Colorimetric then we create a new type. + self._type = BaseSensor.Colorimetric( + sensor_type_colorimetric=self._sensor_template.intensity_sensor_template.sensor_type_colorimetric, + stable_ctr=True, + ) + elif ( + self._type._sensor_type_colorimetric + is not self._sensor_template.intensity_sensor_template.sensor_type_colorimetric + ): + # Happens in case of feature reset (to be sure to always modify correct data) + self._type._sensor_type_colorimetric = ( + self._sensor_template.intensity_sensor_template.sensor_type_colorimetric + ) + return self._type + + def set_type_radiometric(self) -> SensorXMPIntensity: + """Set type radiometric. + + The sensor considers the entire spectrum and gets the results in W/m2. + + Returns + ------- + ansys.speos.core.sensor.SensorIrradiance + Irradiance sensor. + """ + self._sensor_template.intensity_sensor_template.sensor_type_radiometric.SetInParent() + self._type = "Radiometric" + return self + + def set_type_spectral(self) -> BaseSensor.Spectral: + """Set type spectral. + + The sensor will generate color results and spectral data separated by wavelength + in lx or W/m2. + + Returns + ------- + ansys.speos.core.sensor.BaseSensor.Spectral + Spectral type. + """ + if self._type is None and self._sensor_template.intensity_sensor_template.HasField( + "sensor_type_spectral" + ): + # Happens in case of project created via load of speos file + self._type = BaseSensor.Spectral( + sensor_type_spectral=self._sensor_template.intensity_sensor_template.sensor_type_spectral, + default_values=False, + stable_ctr=True, + ) + elif not isinstance(self._type, BaseSensor.Spectral): + # if the _type is not Spectral then we create a new type. + self._type = BaseSensor.Spectral( + sensor_type_spectral=self._sensor_template.intensity_sensor_template.sensor_type_spectral, + stable_ctr=True, + ) + elif ( + self._type._sensor_type_spectral + is not self._sensor_template.intensity_sensor_template.sensor_type_spectral + ): + # Happens in case of feature reset (to be sure to always modify correct data) + self._type._sensor_type_spectral = ( + self._sensor_template.intensity_sensor_template.sensor_type_spectral + ) + return self._type + + def set_layer_type_none(self) -> SensorRadiance: + """Define layer separation type as None. + + Returns + ------- + ansys.speos.core.sensor.SensorXMPIntensity + Intensity sensor + + """ + self._sensor_instance.intensity_properties.layer_type_none.SetInParent() + self._layer_type = None + return self + + def set_layer_type_source(self) -> SensorXMPIntensity: + """Define layer separation as by source. + + Returns + ------- + ansys.speos.core.sensor.SensorXMPIntensity + Intensity sensor + + """ + self._sensor_instance.intensity_properties.layer_type_source.SetInParent() + self._layer_type = None + return self + + def set_layer_type_face(self) -> BaseSensor.LayerTypeFace: + """Define layer separation as by face. + + Returns + ------- + ansys.speos.core.sensor.BaseSensor.LayerTypeFace + LayerTypeFace property instance + """ + if self._layer_type is None and self._sensor_instance.intensity_properties.HasField( + "layer_type_face" + ): + # Happens in case of project created via load of speos file + self._layer_type = BaseSensor.LayerTypeFace( + layer_type_face=self._sensor_instance.intensity_properties.layer_type_face, + default_values=False, + stable_ctr=True, + ) + elif not isinstance(self._layer_type, BaseSensor.LayerTypeFace): + # if the _layer_type is not LayerTypeFace then we create a new type. + self._layer_type = BaseSensor.LayerTypeFace( + layer_type_face=self._sensor_instance.intensity_properties.layer_type_face, + stable_ctr=True, + ) + elif ( + self._layer_type._layer_type_face + is not self._sensor_instance.intensity_properties.layer_type_face + ): + # Happens in case of feature reset (to be sure to always modify correct data) + self._layer_type._layer_type_face = ( + self._sensor_instance.intensity_properties.layer_type_face + ) + return self._layer_type + + def set_layer_type_sequence(self) -> BaseSensor.LayerTypeSequence: + """Define layer separation as by sequence. + + Returns + ------- + ansys.speos.core.sensor.BaseSensor.LayerTypeSequence + LayerTypeSequence property instance + """ + if self._layer_type is None and self._sensor_instance.intensity_properties.HasField( + "layer_type_sequence" + ): + # Happens in case of project created via load of speos file + self._layer_type = BaseSensor.LayerTypeSequence( + layer_type_sequence=self._sensor_instance.intensity_properties.layer_type_sequence, + default_values=False, + stable_ctr=True, + ) + elif not isinstance(self._layer_type, BaseSensor.LayerTypeSequence): + # if the _layer_type is not LayerTypeSequence then we create a new type. + self._layer_type = BaseSensor.LayerTypeSequence( + layer_type_sequence=self._sensor_instance.intensity_properties.layer_type_sequence, + stable_ctr=True, + ) + elif ( + self._layer_type._layer_type_sequence + is not self._sensor_instance.intensity_properties.layer_type_sequence + ): + # Happens in case of feature reset (to be sure to always modify correct data) + self._layer_type._layer_type_sequence = ( + self._sensor_instance.intensity_properties.layer_type_sequence + ) + return self._layer_type + + @property + def axis_system(self) -> np.array: + """Position of the sensor. + + Position of the sensor [Ox Oy Oz Xx Xy Xz Yx Yy Yz Zx Zy Zz]. + By default, ``[0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1]``. + + Returns + ------- + np.array[float] + Axis system information as np.array. + """ + return np.array(self._sensor_instance.intensity_properties.axis_system) + + @axis_system.setter + def axis_system(self, value: Collection[float]): + value = np.array(value) + self._sensor_instance.intensity_properties.axis_system[:] = value.flatten().tolist()