Skip to content

feat: enhance the project preview: irrad, rad, camera sensor features #528

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 41 commits into from
Apr 16, 2025
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
7d78e7f
add preview irradiance sensor
Mar 29, 2025
38e1a40
add preview radiance sensor
Mar 29, 2025
0f60d13
add preview camera sensor
Mar 29, 2025
3cca6ee
chore: adding changelog file 528.added.md [dependabot-skip]
pyansys-ci-bot Mar 29, 2025
058e88a
create pyvista visualization data at each sensor class
Apr 1, 2025
b25fede
refactor the _create_preview() method where visualization data is ret…
Apr 1, 2025
6cadc6a
Merge branch 'main' into feat/preview_sensor
pluAtAnsys Apr 1, 2025
3977e3c
update the plotter method to add sensor PolyData
Apr 1, 2025
95a1bfd
add camera object field visualization sphere position
Apr 2, 2025
0bcf125
add comments about camera object field visualization
Apr 3, 2025
5a1cb31
refactor VisualData class into visualization_methods.py
Apr 8, 2025
0b090f5
refactor the _GRAPHICS_AVAILABLE
Apr 9, 2025
d3077ff
Add more methods in visualization_methods.py
Apr 9, 2025
3d489ec
Add cache saving and add vector calculation methods
Apr 9, 2025
ef2673a
refactor the sensor classes to de-couple using pyvista
Apr 9, 2025
89ec180
some corrections
Apr 9, 2025
34ad293
Merge branch 'main' into feat/preview_sensor
pluAtAnsys Apr 9, 2025
27743e8
Add vector class
Apr 10, 2025
013021a
add divide and get_item method for Vector class
Apr 10, 2025
9f7c007
refactor to using Vector rather than List
Apr 10, 2025
121a177
Merge branch 'main' into feat/preview_sensor
StefanThoene Apr 11, 2025
bd3a8ac
remove Vector class and refactor using numpy
Apr 11, 2025
54062b1
refactor using as list[float] as input
Apr 11, 2025
6461729
refactor
Apr 11, 2025
fbf37ec
add unit test for preview sensor visual data
Apr 12, 2025
8ffed79
chore: adding changelog file 528.added.md [dependabot-skip]
pyansys-ci-bot Apr 12, 2025
54686b2
Merge branch 'main' into feat/preview_sensor
pluAtAnsys Apr 15, 2025
2ac3667
Merge branch 'main' into feat/preview_sensor
pluAtAnsys Apr 15, 2025
ee00909
ci: auto fixes from pre-commit.com hooks.
pre-commit-ci[bot] Apr 15, 2025
8a9fad3
refactor the attribute names
Apr 15, 2025
86c6e5e
some corrections
Apr 15, 2025
c654005
come correction to allow to inherit
Apr 15, 2025
cb2fc1b
remove the "graphic_required" decorator from sensor visual_data method
Apr 16, 2025
60777b4
change from "if instance" to "match case"
Apr 16, 2025
45b2d08
refactor the plot speos feature preview to private function
Apr 16, 2025
4a71f39
merge the case SensorIrradiance and SensorCamera axis plot
Apr 16, 2025
2d69c1d
Update src/ansys/speos/core/sensor.py
pluAtAnsys Apr 16, 2025
19772a8
Merge branch 'main' into feat/preview_sensor
StefanThoene Apr 16, 2025
ef93443
Merge branch 'main' into feat/preview_sensor
pluAtAnsys Apr 16, 2025
d5ccc70
error message correction
Apr 16, 2025
69e786b
Merge remote-tracking branch 'origin/feat/preview_sensor' into feat/p…
Apr 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changelog.d/528.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
enhance the project preview: irrad, rad, camera sensor features
67 changes: 58 additions & 9 deletions src/ansys/speos/core/generic/general_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,19 @@
this includes decorator and methods
"""

from functools import wraps
from functools import lru_cache, wraps
import os
from pathlib import Path
from typing import Optional, Union
from typing import List, Optional, Union
import warnings

import numpy as np

from ansys.speos.core.generic.constants import DEFAULT_VERSION
from ansys.tools.path import get_available_ansys_installations

__GRAPHICS_AVAILABLE = None
_GRAPHICS_AVAILABLE = None

GRAPHICS_ERROR = (
"Preview unsupported without 'ansys-tools-visualization_interface' installed. "
"You can install this using `pip install ansys-speos-core[graphics]`."
Expand Down Expand Up @@ -76,22 +79,23 @@ def wrapper(*args, **kwargs):
return decorator


@lru_cache
def run_if_graphics_required(warning=False):
"""Check if graphics are available."""
global __GRAPHICS_AVAILABLE
if __GRAPHICS_AVAILABLE is None:
global _GRAPHICS_AVAILABLE
if _GRAPHICS_AVAILABLE is None:
try:
import pyvista as pv # noqa: F401

from ansys.tools.visualization_interface import Plotter # noqa: F401

__GRAPHICS_AVAILABLE = True
_GRAPHICS_AVAILABLE = True
except ImportError: # pragma: no cover
__GRAPHICS_AVAILABLE = False
_GRAPHICS_AVAILABLE = False

if __GRAPHICS_AVAILABLE is False and warning is False: # pragma: no cover
if _GRAPHICS_AVAILABLE is False and warning is False: # pragma: no cover
raise ImportError(GRAPHICS_ERROR)
elif __GRAPHICS_AVAILABLE is False: # pragma: no cover
elif _GRAPHICS_AVAILABLE is False: # pragma: no cover
warnings.warn(GRAPHICS_ERROR)


Expand All @@ -116,6 +120,51 @@ def wrapper(*args, **kwargs):
return wrapper


def magnitude_vector(vector: Union[List[float], np.array]) -> float:
"""
Compute the magnitude (length) of a 2D or 3D vector using NumPy.

Parameters
----------
vector: List[float]
A 2D or 3D vector as a list [x, y] or [x, y, z].

Returns
-------
float
The magnitude (length) of the vector.
"""
vector_np = np.array(vector, dtype=float)
if vector_np.size not in (2, 3):
raise ValueError("Input vector must be either 2D or 3D")
return np.linalg.norm(vector_np)


def normalize_vector(vector: Union[List[float], np.array]) -> List[float]:
"""
Normalize a 2D or 3D vector to have a length of 1 using NumPy.

Parameters
----------
vector: List[float]
A vector as a list [x, y] for 2D or [x, y, z] for 3D.

Returns
-------
List[float]
The normalized vector.
"""
vector_np = np.array(vector, dtype=float)
if vector_np.size not in (2, 3):
raise ValueError("Input vector must be either 2D or 3D")

magnitude = magnitude_vector(vector_np)
if magnitude == 0:
raise ValueError("Cannot normalize the zero vector")

return (vector_np / magnitude).tolist()


def error_no_install(install_path: Union[Path, str], version: Union[int, str]):
"""Raise error that installation was not found at a location.

Expand Down
289 changes: 289 additions & 0 deletions src/ansys/speos/core/generic/visualization_methods.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
# Copyright (C) 2021 - 2025 ANSYS, Inc. and/or its affiliates.
# SPDX-License-Identifier: MIT
#
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

"""Provides the ``VisualData`` class."""

from typing import TYPE_CHECKING, List

from ansys.speos.core.generic.general_methods import (
graphics_required,
magnitude_vector,
normalize_vector,
)

if TYPE_CHECKING: # pragma: no cover
import pyvista as pv


@graphics_required
class _VisualCoordinateSystem:
"""Visualization data for the coordinate system..

By default, there is empty visualization data.

Notes
-----
**Do not instantiate this class yourself**.
"""

def __init__(self):
import pyvista as pv

self.__origin = [0.0, 0.0, 0.0]
self.__x_axis = pv.Arrow(
start=self.__origin,
direction=[1.0, 0.0, 0.0],
scale=10.0,
tip_radius=0.05,
shaft_radius=0.01,
)
self.__y_axis = pv.Arrow(
start=self.__origin,
direction=[0.0, 1.0, 0.0],
scale=10.0,
tip_radius=0.05,
shaft_radius=0.01,
)
self.__z_axis = pv.Arrow(
start=self.__origin,
direction=[0.0, 0.0, 1.0],
scale=10.0,
tip_radius=0.05,
shaft_radius=0.01,
)

@property
def origin(self) -> List[float]:
"""Returns the origin of the coordinate system.

Returns
-------
List[float]
The origin of the coordinate system.

"""
return self.__origin

@origin.setter
def origin(self, value: List[float]) -> None:
"""Set the origin of the coordinate system.

Parameters
----------
value: List[float]
The origin of the coordinate system.

Returns
-------
None
"""
if len(value) != 3:
raise ValueError("origin must be a list with three elements.")
self.__origin = value

@property
def x_axis(self) -> "pv.Arrow":
"""Returns the x-axis of the coordinate system.

Returns
-------
pv.Arrow
pyvista.Arrow the x-axis of the coordinate system.

"""
return self.__x_axis

@x_axis.setter
def x_axis(self, x_vector: List[float]) -> None:
"""Set the x-axis of the coordinate system.

Parameters
----------
x_vector: List[float]
The x-axis of the coordinate system.

Returns
-------
None
"""
import pyvista as pv

if len(x_vector) != 3:
raise ValueError("x_axis must be a list with three elements.")
self.__x_axis = pv.Arrow(
start=self.__origin,
direction=normalize_vector(vector=x_vector),
scale=magnitude_vector(vector=x_vector),
tip_radius=0.05,
shaft_radius=0.01,
)

@property
def y_axis(self) -> "pv.Arrow":
"""
Returns the y-axis of the coordinate system.

Returns
-------
pv.Arrow
pyvista.Arrow the y-axis of the coordinate system.

"""
return self.__y_axis

@y_axis.setter
def y_axis(self, y_vector: List[float]) -> None:
"""Set the y-axis of the coordinate system.

Parameters
----------
y_vector: List[float]
The y-axis of the coordinate system.

Returns
-------
None
"""
import pyvista as pv

if len(y_vector) != 3:
raise ValueError("y_axis must be a list with three elements.")
self.__y_axis = pv.Arrow(
start=self.__origin,
direction=normalize_vector(vector=y_vector),
scale=magnitude_vector(vector=y_vector),
tip_radius=0.05,
shaft_radius=0.01,
)

@property
def z_axis(self) -> "pv.Arrow":
"""
Returns the z-axis of the coordinate system.

Returns
-------
pv.Arrow
pyvista.Arrow the z-axis of the coordinate system.

"""
return self.__z_axis

@z_axis.setter
def z_axis(self, z_vector: List[float]) -> None:
"""Set the z-axis of the coordinate system.

Parameters
----------
z_vector: List[float]
The z-axis of the coordinate system.

Returns
-------
None
"""
import pyvista as pv

if len(z_vector) != 3:
raise ValueError("z_axis must be a list with three elements.")
self.__z_axis = pv.Arrow(
start=self.__origin,
direction=normalize_vector(vector=z_vector),
scale=magnitude_vector(vector=z_vector),
tip_radius=0.05,
shaft_radius=0.01,
)


@graphics_required
class _VisualData:
"""Visualization data for the sensor.

By default, there is empty visualization data.

Notes
-----
**Do not instantiate this class yourself**
"""

def __init__(self, coordinate_system=True):
import pyvista as pv

self.__data = pv.PolyData()
self.coordinates = _VisualCoordinateSystem() if coordinate_system else None
self.updated = False

@property
def data(self) -> "pv.PolyData":
"""
Returns the data of the sensor.

Returns
-------
pv.PolyData
The data of the surface visualization.

"""
return self.__data

def add_data_triangle(self, triangle_vertices: List[List[float]]) -> None:
"""
Add surface data triangle to Visualization data.

Parameters
----------
triangle_vertices: List[List[float]]
The vertices of the triangle.

Returns
-------
None
"""
import pyvista as pv

if len(triangle_vertices) != 3 or any(len(vertex) != 3 for vertex in triangle_vertices):
raise ValueError(
"triangle_vertices is expected to be composed of 3 vertices with 3 elements each."
)
faces = [[3, 0, 1, 2]]
self.__data = self.__data.append_polydata(pv.PolyData(triangle_vertices, faces))

def add_data_rectangle(self, rectangle_vertices: List[List[float]]) -> None:
"""
Add surface data rectangle to Visualization data.

Parameters
----------
rectangle_vertices: List[List[float]]
The vertices of the rectangle.

Returns
-------
None
"""
import pyvista as pv

if len(rectangle_vertices) != 3 or any(len(vertex) != 3 for vertex in rectangle_vertices):
raise ValueError(
"rectangle_vertices is expected to be composed of 3 vertices with 3 elements each."
)
self.__data = self.__data.append_polydata(pv.Rectangle(rectangle_vertices))
Loading