fix numpy typing #4579

On older numpy versions, np.ndarray was less forgiving and wasn't allowing passing 1 argument instead of required 2.

And turned out numpy doesn't yet have typing for shapes (https://github.com/numpy/numpy/issues/16544), so all matrices and other shapes specified as `npt.NDArray[np.float64]`.

Fixed type discrepancies for `get_edges` and `get_faces` and also had to fix `import_ifc` as Blender apparently has problems with storing np.int32 in custom attributes (https://projects.blender.org/blender/blender/issues/121072), tested that Blender is okay with np.int32 in other cases we had (addressing BMesh.verts[i] where `i` is np.int32).
This commit is contained in:
Andrej730
2024-04-25 15:55:32 +05:00
parent 721466c429
commit 5b877b549a
4 changed files with 42 additions and 29 deletions
+3 -1
View File
@@ -1935,7 +1935,9 @@ class IfcImporter:
# See bug 3546 # See bug 3546
# ios_edges holds true edges that aren't triangulated. # ios_edges holds true edges that aren't triangulated.
mesh["ios_edges"] = list(set(tuple(e) for e in ifcopenshell.util.shape.get_edges(geometry))) #
# we do `.tolist()` because Blender can't assign `np.int32` to it's custom attributes
mesh["ios_edges"] = list(set(tuple(e) for e in ifcopenshell.util.shape.get_edges(geometry).tolist()))
mesh.vertices.add(num_vertices) mesh.vertices.add(num_vertices)
mesh.vertices.foreach_set("co", verts) mesh.vertices.foreach_set("co", verts)
@@ -17,10 +17,13 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>. # along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import numpy as np import numpy as np
import numpy.typing as npt
import ifcopenshell import ifcopenshell
from typing import Literal, Iterable from typing import Literal, Iterable
MatrixType = np.ndarray[np.ndarray[float]]
MatrixType = npt.NDArray[np.float64]
"""`npt.NDArray[np.float64]`"""
def a2p(o: Iterable[float], z: Iterable[float], x: Iterable[float]) -> MatrixType: def a2p(o: Iterable[float], z: Iterable[float], x: Iterable[float]) -> MatrixType:
@@ -36,7 +39,7 @@ def a2p(o: Iterable[float], z: Iterable[float], x: Iterable[float]) -> MatrixTyp
:param x: The +X vector / axis of the matrix :param x: The +X vector / axis of the matrix
:type x: iterable[float] :type x: iterable[float]
:return: A 4x4 numpy matrix :return: A 4x4 numpy matrix
:rtype: np.ndarray[np.ndarray[float]] :rtype: MatrixType
""" """
x = x / np.linalg.norm(x) x = x / np.linalg.norm(x)
z = z / np.linalg.norm(z) z = z / np.linalg.norm(z)
@@ -59,7 +62,7 @@ def get_axis2placement(placement: ifcopenshell.entity_instance) -> MatrixType:
:param placement: The IfcLocalPlacement enitity :param placement: The IfcLocalPlacement enitity
:type placement: ifcopenshell.entity_instance.entity_instance :type placement: ifcopenshell.entity_instance.entity_instance
:return: A 4x4 numpy matrix :return: A 4x4 numpy matrix
:rtype: np.ndarray[np.ndarray[float]] :rtype: MatrixType
""" """
ifc_class = placement.is_a() ifc_class = placement.is_a()
if ifc_class in ("IfcAxis2Placement3D", "IfcAxis2PlacementLinear"): if ifc_class in ("IfcAxis2Placement3D", "IfcAxis2PlacementLinear"):
@@ -72,7 +75,7 @@ def get_axis2placement(placement: ifcopenshell.entity_instance) -> MatrixType:
ifc_class = location.is_a() ifc_class = location.is_a()
print( print(
f'WARNING. Placement location of type "{ifc_class}" ' f'WARNING. Placement location of type "{ifc_class}" '
f'is not yet supported and placement {placement} may be placed incorrectly.' f"is not yet supported and placement {placement} may be placed incorrectly."
) )
o = (0.0, 0.0, 0.0) o = (0.0, 0.0, 0.0)
@@ -117,7 +120,7 @@ def get_local_placement(placement: ifcopenshell.entity_instance) -> MatrixType:
:param placement: The IfcLocalPlacement entity :param placement: The IfcLocalPlacement entity
:type placement: ifcopenshell.entity_instance.entity_instance :type placement: ifcopenshell.entity_instance.entity_instance
:return: A 4x4 numpy matrix :return: A 4x4 numpy matrix
:rtype: np.ndarray[np.ndarray[float]] :rtype: MatrixType
""" """
if placement is None: if placement is None:
return np.eye(4) return np.eye(4)
@@ -137,7 +140,7 @@ def get_cartesiantransformationoperator3d(inst: ifcopenshell.entity_instance) ->
:param item: The IfcCartesianTransformationOperator entity :param item: The IfcCartesianTransformationOperator entity
:type item: ifcopenshell.entity_instance.entity_instance :type item: ifcopenshell.entity_instance.entity_instance
:return: A 4x4 numpy transformation matrix :return: A 4x4 numpy transformation matrix
:rtype: np.ndarray[np.ndarray[float]] :rtype: MatrixType
""" """
origin = np.array(inst.LocalOrigin.Coordinates) origin = np.array(inst.LocalOrigin.Coordinates)
axis1 = np.array((1.0, 0.0, 0.0)) axis1 = np.array((1.0, 0.0, 0.0))
@@ -183,7 +186,7 @@ def get_mappeditem_transformation(item: ifcopenshell.entity_instance) -> MatrixT
:param item: The IfcMappedItem entity :param item: The IfcMappedItem entity
:type item: ifcopenshell.entity_instance.entity_instance :type item: ifcopenshell.entity_instance.entity_instance
:return: A 4x4 numpy transformation matrix :return: A 4x4 numpy transformation matrix
:rtype: np.ndarray[np.ndarray[float]] :rtype: MatrixType
""" """
m4 = get_axis2placement(item.MappingSource.MappingOrigin) m4 = get_axis2placement(item.MappingSource.MappingOrigin)
# TODO 2d # TODO 2d
@@ -219,7 +222,7 @@ def rotation(angle: float, axis: Literal["X", "Y", "Z"], is_degrees=True) -> Mat
radians. Defaults to true (i.e. degrees). radians. Defaults to true (i.e. degrees).
:type is_degrees: bool :type is_degrees: bool
:return: A 4x4 numpy rotation matrix :return: A 4x4 numpy rotation matrix
:rtype: np.ndarray[np.ndarray[float]] :rtype: MatrixType
""" """
theta = np.radians(angle) if is_degrees else angle theta = np.radians(angle) if is_degrees else angle
cos, sin = np.cos(theta), np.sin(theta) cos, sin = np.cos(theta), np.sin(theta)
@@ -19,6 +19,7 @@
import shapely import shapely
import shapely.ops import shapely.ops
import numpy as np import numpy as np
import numpy.typing as npt
import ifcopenshell.util.element import ifcopenshell.util.element
import ifcopenshell.util.placement import ifcopenshell.util.placement
import ifcopenshell.util.representation import ifcopenshell.util.representation
@@ -28,6 +29,9 @@ tol = 1e-6
AXIS_LITERAL = Literal["X", "Y", "Z"] AXIS_LITERAL = Literal["X", "Y", "Z"]
VECTOR_3D = tuple[float, float, float] VECTOR_3D = tuple[float, float, float]
MatrixType = npt.NDArray[np.float64]
"""`npt.NDArray[np.float64]`"""
def is_x(value: float, x: float, tolerance: Optional[float] = None) -> bool: def is_x(value: float, x: float, tolerance: Optional[float] = None) -> bool:
"""Checks whether a value is equivalent to X given a tolerance """Checks whether a value is equivalent to X given a tolerance
@@ -113,19 +117,19 @@ def get_z(geometry) -> float:
return max(z_values) - min(z_values) return max(z_values) - min(z_values)
def get_shape_matrix(shape) -> np.ndarray: def get_shape_matrix(shape) -> MatrixType:
"""Formats the transformation matrix of a shape as a 4x4 numpy array """Formats the transformation matrix of a shape as a 4x4 numpy array
:param shape: Shape output calculated by IfcOpenShell :param shape: Shape output calculated by IfcOpenShell
:type shape: shape :type shape: shape
:return: A 4x4 numpy array representing the transformation matrix :return: A 4x4 numpy array representing the transformation matrix
:rtype: np.array :rtype: MatrixType
""" """
m = shape.transformation.matrix.data m = shape.transformation.matrix.data
return np.array(([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1])) return np.array(([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1]))
def get_bbox_centroid(geometry) -> tuple[float]: def get_bbox_centroid(geometry) -> tuple[float, float, float]:
"""Calculates the bounding box centroid of the geometry """Calculates the bounding box centroid of the geometry
The centroid is in local coordinates relative to the object's placement. The centroid is in local coordinates relative to the object's placement.
@@ -133,11 +137,14 @@ def get_bbox_centroid(geometry) -> tuple[float]:
:param geometry: Geometry output calculated by IfcOpenShell :param geometry: Geometry output calculated by IfcOpenShell
:type geometry: geometry :type geometry: geometry
:return: A tuple representing the XYZ centroid :return: A tuple representing the XYZ centroid
:rtype: tuple[float] :rtype: tuple[float, float, float]
""" """
x_values = [geometry.verts[i] for i in range(0, len(geometry.verts), 3)] x_values = [geometry.verts[i] for i in range(0, len(geometry.verts), 3)]
y_values = [geometry.verts[i + 1] for i in range(0, len(geometry.verts), 3)] y_values = [geometry.verts[i + 1] for i in range(0, len(geometry.verts), 3)]
z_values = [geometry.verts[i + 2] for i in range(0, len(geometry.verts), 3)] z_values = [geometry.verts[i + 2] for i in range(0, len(geometry.verts), 3)]
x_values: list[float]
y_values: list[float]
z_values: list[float]
minx = min(x_values) minx = min(x_values)
maxx = max(x_values) maxx = max(x_values)
miny = min(y_values) miny = min(y_values)
@@ -147,7 +154,7 @@ def get_bbox_centroid(geometry) -> tuple[float]:
return (minx + ((maxx - minx) / 2), miny + ((maxy - miny) / 2), minz + ((maxz - minz) / 2)) return (minx + ((maxx - minx) / 2), miny + ((maxy - miny) / 2), minz + ((maxz - minz) / 2))
def get_element_bbox_centroid(element: ifcopenshell.entity_instance, geometry) -> tuple[float]: def get_element_bbox_centroid(element: ifcopenshell.entity_instance, geometry) -> npt.NDArray[np.float64]:
"""Calculates the element's bounding box centroid """Calculates the element's bounding box centroid
The centroid is in global coordinates. Note that if you have the shape, it The centroid is in global coordinates. Note that if you have the shape, it
@@ -158,16 +165,16 @@ def get_element_bbox_centroid(element: ifcopenshell.entity_instance, geometry) -
:param geometry: Geometry output calculated by IfcOpenShell :param geometry: Geometry output calculated by IfcOpenShell
:type geometry: geometry :type geometry: geometry
:return: A tuple representing the XYZ centroid :return: A tuple representing the XYZ centroid
:rtype: tuple[float] :rtype: npt.NDArray[np.float64]
""" """
centroid = get_bbox_centroid(geometry) centroid = get_bbox_centroid(geometry)
if not element.ObjectPlacement or not element.ObjectPlacement.is_a("IfcLocalPlacement"): if not element.ObjectPlacement or not element.ObjectPlacement.is_a("IfcLocalPlacement"):
return centroid return np.array(centroid)
mat = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement) mat = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
return (mat @ np.array([*centroid, 1.0]))[0:3] return (mat @ np.array([*centroid, 1.0]))[0:3]
def get_shape_bbox_centroid(shape, geometry) -> tuple[float]: def get_shape_bbox_centroid(shape, geometry) -> npt.NDArray[np.float64]:
"""Calculates the shape's bounding box centroid """Calculates the shape's bounding box centroid
The centroid is in global coordinates. Note that if you do not have the The centroid is in global coordinates. Note that if you do not have the
@@ -178,13 +185,13 @@ def get_shape_bbox_centroid(shape, geometry) -> tuple[float]:
:param geometry: Geometry output calculated by IfcOpenShell :param geometry: Geometry output calculated by IfcOpenShell
:type geometry: geometry :type geometry: geometry
:return: A tuple representing the XYZ centroid :return: A tuple representing the XYZ centroid
:rtype: tuple[float] :rtype: npt.NDArray[np.float64]
""" """
centroid = get_bbox_centroid(geometry) centroid = get_bbox_centroid(geometry)
return (get_shape_matrix(shape) @ np.array([*centroid, 1.0]))[0:3] return (get_shape_matrix(shape) @ np.array([*centroid, 1.0]))[0:3]
def get_vertices(geometry) -> np.ndarray[np.ndarray[float]]: def get_vertices(geometry) -> npt.NDArray[np.float64]:
"""Get all the vertices as a numpy array """Get all the vertices as a numpy array
Vertices are in local coordinates. Vertices are in local coordinates.
@@ -200,7 +207,7 @@ def get_vertices(geometry) -> np.ndarray[np.ndarray[float]]:
return np.array([np.array([verts[i], verts[i + 1], verts[i + 2]]) for i in range(0, len(verts), 3)]) return np.array([np.array([verts[i], verts[i + 1], verts[i + 2]]) for i in range(0, len(verts), 3)])
def get_edges(geometry) -> np.ndarray[np.ndarray[int]]: def get_edges(geometry) -> npt.NDArray[np.int32]:
"""Get all the edges as a numpy array """Get all the edges as a numpy array
Results are a nested numpy array e.g. [[e1v1, e1v2], [e2v1, e2v2], ...] Results are a nested numpy array e.g. [[e1v1, e1v2], [e2v1, e2v2], ...]
@@ -215,10 +222,10 @@ def get_edges(geometry) -> np.ndarray[np.ndarray[int]]:
:rtype: np.array[np.array[int]] :rtype: np.array[np.array[int]]
""" """
edges = geometry.edges edges = geometry.edges
return [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)] return np.array([[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)])
def get_faces(geometry) -> np.ndarray[np.ndarray[int]]: def get_faces(geometry) -> npt.NDArray[np.int32]:
"""Get all the faces as a numpy array """Get all the faces as a numpy array
Faces are always triangulated. If the shape is a BRep and you want to get Faces are always triangulated. If the shape is a BRep and you want to get
@@ -232,10 +239,10 @@ def get_faces(geometry) -> np.ndarray[np.ndarray[int]]:
:rtype: np.array[np.array[int]] :rtype: np.array[np.array[int]]
""" """
faces = geometry.faces faces = geometry.faces
return [[faces[i], faces[i + 1], faces[i + 2]] for i in range(0, len(faces), 3)] return np.array([[faces[i], faces[i + 1], faces[i + 2]] for i in range(0, len(faces), 3)])
def get_shape_vertices(shape, geometry) -> np.ndarray[np.ndarray[float]]: def get_shape_vertices(shape, geometry) -> npt.NDArray[np.float64]:
"""Get the shape's vertices as a numpy array """Get the shape's vertices as a numpy array
Vertices are in global coordinates. If you do not have the shape, you can Vertices are in global coordinates. If you do not have the shape, you can
@@ -255,7 +262,7 @@ def get_shape_vertices(shape, geometry) -> np.ndarray[np.ndarray[float]]:
return np.delete((mat @ np.hstack((verts, np.ones((len(verts), 1)))).T).T, -1, axis=1) return np.delete((mat @ np.hstack((verts, np.ones((len(verts), 1)))).T).T, -1, axis=1)
def get_element_vertices(element: ifcopenshell.entity_instance, geometry) -> np.ndarray[np.ndarray[float]]: def get_element_vertices(element: ifcopenshell.entity_instance, geometry) -> npt.NDArray[np.float64]:
"""Get the element's vertices as a numpy array """Get the element's vertices as a numpy array
Vertices are in global coordinates. Note that if you have the shape, it is Vertices are in global coordinates. Note that if you have the shape, it is
@@ -365,7 +372,7 @@ def get_element_top_elevation(element: ifcopenshell.entity_instance, geometry) -
return max([v[2] for v in get_element_vertices(element, geometry)]) return max([v[2] for v in get_element_vertices(element, geometry)])
def get_bbox(vertices: Iterable[VECTOR_3D]) -> tuple[np.ndarray[float]]: def get_bbox(vertices: Iterable[VECTOR_3D]) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
"""Gets the bounding box of vertices """Gets the bounding box of vertices
:param vertices: An iterable of vertices :param vertices: An iterable of vertices
@@ -388,7 +395,7 @@ def get_bbox(vertices: Iterable[VECTOR_3D]) -> tuple[np.ndarray[float]]:
return (np.array([minx, miny, minz]), np.array([maxx, maxy, maxz])) return (np.array([minx, miny, minz]), np.array([maxx, maxy, maxz]))
def get_area_vf(vertices: np.ndarray[VECTOR_3D], faces: np.ndarray[Iterable[int]]) -> float: def get_area_vf(vertices: npt.NDArray[np.float64], faces: npt.NDArray[np.int32]) -> float:
"""Calculates the surface area given a list of vertices and triangulated faces """Calculates the surface area given a list of vertices and triangulated faces
:param vertices: A list of 3D vertices, such as returned from get_vertices. :param vertices: A list of 3D vertices, such as returned from get_vertices.
@@ -17,6 +17,7 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>. # along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import numpy as np import numpy as np
import numpy.typing as npt
import collections import collections
import collections.abc import collections.abc
import ifcopenshell import ifcopenshell
@@ -533,13 +534,13 @@ class ShapeBuilder:
def create_axis2_placement_3d_from_matrix( def create_axis2_placement_3d_from_matrix(
self, self,
matrix: Union[np.ndarray, None] = None, matrix: Union[npt.NDArray[np.float64], None] = None,
) -> ifcopenshell.entity_instance: ) -> ifcopenshell.entity_instance:
""" """
Create IfcAxis2Placement3D from numpy matrix. Create IfcAxis2Placement3D from numpy matrix.
:param matrix: 4x4 transformation matrix, defaults to `np.eye(4)` :param matrix: 4x4 transformation matrix, defaults to `np.eye(4)`
:type matrix: np.array[np.array[float]], optional :type matrix: npt.NDArray[np.float64], optional
:return: IfcAxis2Placement3D :return: IfcAxis2Placement3D
:rtype: ifcopenshell.entity_instance :rtype: ifcopenshell.entity_instance
""" """