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 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
- orientation = direction_vector.project(face.normal).norm()
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(cls, ifc_file_path: Path) -> "Building":
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=common_surface.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(self, library: Libraries = "Buildings") -> str:
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
- network.add_boiler_plate_spaces(list(spaces.values()))
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]