ifctrano 0.1.8__py3-none-any.whl → 0.1.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ifctrano/base.py +23 -1
- ifctrano/bounding_box.py +9 -2
- ifctrano/building.py +63 -11
- ifctrano/example/verification.ifc +3043 -0
- ifctrano/exceptions.py +8 -0
- ifctrano/main.py +28 -0
- ifctrano/space_boundary.py +78 -21
- {ifctrano-0.1.8.dist-info → ifctrano-0.1.9.dist-info}/METADATA +2 -2
- ifctrano-0.1.9.dist-info/RECORD +13 -0
- ifctrano-0.1.8.dist-info/RECORD +0 -12
- {ifctrano-0.1.8.dist-info → ifctrano-0.1.9.dist-info}/LICENSE +0 -0
- {ifctrano-0.1.8.dist-info → ifctrano-0.1.9.dist-info}/WHEEL +0 -0
- {ifctrano-0.1.8.dist-info → ifctrano-0.1.9.dist-info}/entry_points.txt +0 -0
ifctrano/base.py
CHANGED
@@ -3,12 +3,15 @@ from typing import Tuple, Literal, List, Annotated, Sequence
|
|
3
3
|
import ifcopenshell.geom
|
4
4
|
import numpy as np
|
5
5
|
from numpy import ndarray
|
6
|
-
from pydantic import BaseModel, BeforeValidator, ConfigDict
|
6
|
+
from pydantic import BaseModel, BeforeValidator, ConfigDict, model_validator
|
7
|
+
|
8
|
+
from ifctrano.exceptions import VectorWithNansError
|
7
9
|
|
8
10
|
settings = ifcopenshell.geom.settings() # type: ignore
|
9
11
|
Coordinate = Literal["x", "y", "z"]
|
10
12
|
AREA_TOLERANCE = 0.5
|
11
13
|
ROUNDING_FACTOR = 5
|
14
|
+
CLASH_CLEARANCE = 0.5
|
12
15
|
|
13
16
|
|
14
17
|
class BaseModelConfig(BaseModel):
|
@@ -61,6 +64,13 @@ class Sign(BaseModel):
|
|
61
64
|
|
62
65
|
|
63
66
|
class Vector(BasePoint):
|
67
|
+
|
68
|
+
@model_validator(mode="after")
|
69
|
+
def _validator(self) -> "Vector":
|
70
|
+
if any(np.isnan(v) for v in self.to_list()):
|
71
|
+
raise VectorWithNansError("Vector cannot have NaN values")
|
72
|
+
return self
|
73
|
+
|
64
74
|
def __mul__(self, other: "Vector") -> "Vector":
|
65
75
|
|
66
76
|
array = np.cross(self.to_array(), other.to_array())
|
@@ -69,6 +79,15 @@ class Vector(BasePoint):
|
|
69
79
|
def dot(self, other: "Vector") -> float:
|
70
80
|
return np.dot(self.to_array(), other.to_array()) # type: ignore
|
71
81
|
|
82
|
+
def angle(self, other: "Vector") -> int:
|
83
|
+
dot_product = np.dot(self.to_xy(), other.to_xy())
|
84
|
+
cross_product = np.cross(self.to_xy(), other.to_xy())
|
85
|
+
angle_rad = np.arctan2(cross_product, dot_product)
|
86
|
+
angle_deg = np.degrees(angle_rad)
|
87
|
+
if angle_deg < 0:
|
88
|
+
angle_deg += 360
|
89
|
+
return int(angle_deg)
|
90
|
+
|
72
91
|
def project(self, other: "Vector") -> "Vector":
|
73
92
|
a = self.dot(other) / other.dot(other)
|
74
93
|
return Vector(x=a * other.x, y=a * other.y, z=a * other.z)
|
@@ -82,6 +101,9 @@ class Vector(BasePoint):
|
|
82
101
|
def to_array(self) -> np.ndarray: # type: ignore
|
83
102
|
return np.array([self.x, self.y, self.z])
|
84
103
|
|
104
|
+
def to_xy(self) -> np.ndarray: # type: ignore
|
105
|
+
return np.array([self.x, self.y])
|
106
|
+
|
85
107
|
def get_normal_index(self) -> int:
|
86
108
|
normal_index_list = [abs(v) for v in self.to_list()]
|
87
109
|
return normal_index_list.index(max(normal_index_list))
|
ifctrano/bounding_box.py
CHANGED
@@ -25,7 +25,7 @@ from ifctrano.base import (
|
|
25
25
|
AREA_TOLERANCE,
|
26
26
|
ROUNDING_FACTOR,
|
27
27
|
)
|
28
|
-
from ifctrano.exceptions import BoundingBoxFaceError
|
28
|
+
from ifctrano.exceptions import BoundingBoxFaceError, VectorWithNansError
|
29
29
|
|
30
30
|
logger = getLogger(__name__)
|
31
31
|
|
@@ -175,7 +175,14 @@ class OrientedBoundingBox(BaseModel):
|
|
175
175
|
distance = abs(polygon_1.length - polygon_2.length)
|
176
176
|
area = intersection.area
|
177
177
|
direction_vector = (other.centroid - self.centroid).norm()
|
178
|
-
|
178
|
+
try:
|
179
|
+
orientation = direction_vector.project(face.normal).norm()
|
180
|
+
except VectorWithNansError as e:
|
181
|
+
logger.error(
|
182
|
+
"Orientation vector was not properly computed when computing the intersection between"
|
183
|
+
f"two elements. Error: {e}"
|
184
|
+
)
|
185
|
+
continue
|
179
186
|
extend_surfaces.append(
|
180
187
|
ExtendCommonSurface(
|
181
188
|
distance=distance, area=area, orientation=orientation
|
ifctrano/building.py
CHANGED
@@ -1,16 +1,17 @@
|
|
1
|
+
import re
|
1
2
|
from pathlib import Path
|
2
|
-
from typing import List
|
3
|
+
from typing import List, Tuple, Any, Optional
|
3
4
|
|
4
5
|
import ifcopenshell
|
5
6
|
from ifcopenshell import file, entity_instance
|
6
|
-
from pydantic import validate_call, Field, model_validator
|
7
|
+
from pydantic import validate_call, Field, model_validator, field_validator
|
7
8
|
from trano.elements import InternalElement # type: ignore
|
8
9
|
from trano.elements.library.library import Library # type: ignore
|
9
10
|
from trano.elements.types import Tilt # type: ignore
|
10
11
|
from trano.topology import Network # type: ignore
|
11
12
|
|
12
|
-
from ifctrano.base import BaseModelConfig, Libraries
|
13
|
-
from ifctrano.exceptions import IfcFileNotFoundError
|
13
|
+
from ifctrano.base import BaseModelConfig, Libraries, Vector
|
14
|
+
from ifctrano.exceptions import IfcFileNotFoundError, NoIfcSpaceFoundError
|
14
15
|
from ifctrano.space_boundary import (
|
15
16
|
SpaceBoundaries,
|
16
17
|
initialize_tree,
|
@@ -28,6 +29,26 @@ class IfcInternalElement(BaseModelConfig):
|
|
28
29
|
element: entity_instance
|
29
30
|
area: float
|
30
31
|
|
32
|
+
def __hash__(self) -> int:
|
33
|
+
return hash(
|
34
|
+
(
|
35
|
+
*sorted([space.global_id for space in self.spaces]),
|
36
|
+
self.element.GlobalId,
|
37
|
+
self.area,
|
38
|
+
)
|
39
|
+
)
|
40
|
+
|
41
|
+
def __eq__(self, other: "IfcInternalElement") -> bool: # type: ignore
|
42
|
+
return hash(self) == hash(other)
|
43
|
+
|
44
|
+
def description(self) -> Tuple[Any, Any, str, float]:
|
45
|
+
return (
|
46
|
+
*sorted([space.global_id for space in self.spaces]),
|
47
|
+
self.element.GlobalId,
|
48
|
+
self.element.is_a(),
|
49
|
+
self.area,
|
50
|
+
)
|
51
|
+
|
31
52
|
|
32
53
|
class InternalElements(BaseModelConfig):
|
33
54
|
elements: List[IfcInternalElement] = Field(default_factory=list)
|
@@ -35,6 +56,9 @@ class InternalElements(BaseModelConfig):
|
|
35
56
|
def internal_element_ids(self) -> List[str]:
|
36
57
|
return list({e.element.GlobalId for e in self.elements})
|
37
58
|
|
59
|
+
def description(self) -> List[Tuple[Any, Any, str, float]]:
|
60
|
+
return sorted([element.description() for element in self.elements])
|
61
|
+
|
38
62
|
|
39
63
|
class Building(BaseModelConfig):
|
40
64
|
name: str
|
@@ -43,8 +67,18 @@ class Building(BaseModelConfig):
|
|
43
67
|
parent_folder: Path
|
44
68
|
internal_elements: InternalElements = Field(default_factory=InternalElements)
|
45
69
|
|
70
|
+
@field_validator("name")
|
71
|
+
@classmethod
|
72
|
+
def _name_validator(cls, name: str) -> str:
|
73
|
+
name = name.replace(" ", "_")
|
74
|
+
name = re.sub(r"[^a-zA-Z0-9_]", "", name)
|
75
|
+
return name.lower()
|
76
|
+
|
46
77
|
@classmethod
|
47
|
-
def from_ifc(
|
78
|
+
def from_ifc(
|
79
|
+
cls, ifc_file_path: Path, selected_spaces_global_id: Optional[List[str]] = None
|
80
|
+
) -> "Building":
|
81
|
+
selected_spaces_global_id = selected_spaces_global_id or []
|
48
82
|
if not ifc_file_path.exists():
|
49
83
|
raise IfcFileNotFoundError(
|
50
84
|
f"File specified {ifc_file_path} does not exist."
|
@@ -52,6 +86,12 @@ class Building(BaseModelConfig):
|
|
52
86
|
ifc_file = ifcopenshell.open(str(ifc_file_path))
|
53
87
|
tree = initialize_tree(ifc_file)
|
54
88
|
spaces = get_spaces(ifc_file)
|
89
|
+
if selected_spaces_global_id:
|
90
|
+
spaces = [
|
91
|
+
space for space in spaces if space.GlobalId in selected_spaces_global_id
|
92
|
+
]
|
93
|
+
if not spaces:
|
94
|
+
raise NoIfcSpaceFoundError("No IfcSpace found in the file.")
|
55
95
|
space_boundaries = [
|
56
96
|
SpaceBoundaries.from_space_entity(ifc_file, tree, space) for space in spaces
|
57
97
|
]
|
@@ -93,26 +133,38 @@ class Building(BaseModelConfig):
|
|
93
133
|
boundary_.common_surface.orientation
|
94
134
|
* common_surface.orientation
|
95
135
|
).is_a_zero()
|
96
|
-
)
|
136
|
+
) and boundary.common_surface.orientation.dot(
|
137
|
+
boundary_.common_surface.orientation
|
138
|
+
) < 0:
|
97
139
|
elements.append( # noqa: PERF401
|
98
140
|
IfcInternalElement(
|
99
141
|
spaces=[space_1, space_2],
|
100
142
|
element=boundary_.entity,
|
101
|
-
area=
|
143
|
+
area=min(
|
144
|
+
common_surface.area,
|
145
|
+
boundary.common_surface.area,
|
146
|
+
boundary_.common_surface.area,
|
147
|
+
),
|
102
148
|
)
|
103
149
|
)
|
104
|
-
return InternalElements(elements=elements)
|
150
|
+
return InternalElements(elements=list(set(elements)))
|
105
151
|
|
106
152
|
@validate_call
|
107
|
-
def create_model(
|
153
|
+
def create_model(
|
154
|
+
self,
|
155
|
+
library: Libraries = "Buildings",
|
156
|
+
north_axis: Optional[Vector] = None,
|
157
|
+
) -> str:
|
158
|
+
north_axis = north_axis or Vector(x=0, y=1, z=0)
|
108
159
|
network = Network(name=self.name, library=Library.from_configuration(library))
|
109
160
|
spaces = {
|
110
161
|
space_boundary.space.global_id: space_boundary.model(
|
111
|
-
self.internal_elements.internal_element_ids()
|
162
|
+
self.internal_elements.internal_element_ids(), north_axis
|
112
163
|
)
|
113
164
|
for space_boundary in self.space_boundaries
|
114
165
|
}
|
115
|
-
|
166
|
+
spaces = {k: v for k, v in spaces.items() if v}
|
167
|
+
network.add_boiler_plate_spaces(list(spaces.values()), create_internal=False)
|
116
168
|
for internal_element in self.internal_elements.elements:
|
117
169
|
space_1 = internal_element.spaces[0]
|
118
170
|
space_2 = internal_element.spaces[1]
|