ifctrano 0.2.0__py3-none-any.whl → 0.3.0__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
@@ -1,5 +1,7 @@
1
1
  import json
2
2
  import math
3
+ from itertools import combinations
4
+ from multiprocessing import Process
3
5
  from pathlib import Path
4
6
  from typing import Tuple, Literal, List, Annotated, Any, Dict, cast
5
7
 
@@ -18,7 +20,6 @@ from shapely.geometry.polygon import Polygon # type: ignore
18
20
  from vedo import Line, Arrow, Mesh, show, write # type: ignore
19
21
 
20
22
  from ifctrano.exceptions import VectorWithNansError
21
- from multiprocessing import Process
22
23
 
23
24
  settings = ifcopenshell.geom.settings() # type: ignore
24
25
  Coordinate = Literal["x", "y", "z"]
@@ -147,12 +148,15 @@ class Vector(BasePoint):
147
148
  a = self.dot(other) / other.dot(other)
148
149
  return Vector(x=a * other.x, y=a * other.y, z=a * other.z)
149
150
 
150
- def norm(self) -> "Vector":
151
+ def normalize(self) -> "Vector":
151
152
  normalized_vector = self.to_array() / np.linalg.norm(self.to_array())
152
153
  return Vector(
153
154
  x=normalized_vector[0], y=normalized_vector[1], z=normalized_vector[2]
154
155
  )
155
156
 
157
+ def norm(self) -> float:
158
+ return float(np.linalg.norm(self.to_array()))
159
+
156
160
  def to_array(self) -> np.ndarray: # type: ignore
157
161
  return np.array([self.x, self.y, self.z])
158
162
 
@@ -163,7 +167,7 @@ class Vector(BasePoint):
163
167
  normal_index_list = [abs(v) for v in self.to_list()]
164
168
  return normal_index_list.index(max(normal_index_list))
165
169
 
166
- def is_a_zero(self, tolerance: float = 0.1) -> bool:
170
+ def is_null(self, tolerance: float = 0.1) -> bool:
167
171
  return all(abs(value) < tolerance for value in self.to_list())
168
172
 
169
173
  @classmethod
@@ -247,18 +251,23 @@ class Vertices(BaseModel):
247
251
  return FaceVertices(points=self.points)
248
252
 
249
253
  def get_local_coordinate_system(self) -> CoordinateSystem:
250
- origin = self.points[0]
251
- x = self.points[1] - origin
254
+ vectors = [
255
+ (a - b).normalize().to_array() for a, b in combinations(self.points, 2)
256
+ ]
252
257
  found = False
253
- for point in self.points[2:]:
254
- y = point - origin
255
- if abs(x.dot(y)) < 0.00001:
258
+ for v1, v2, v3 in combinations(vectors, 3):
259
+ if (
260
+ np.isclose(abs(np.dot(v1, v2)), 0)
261
+ and np.isclose(abs(np.dot(v1, v3)), 0)
262
+ and np.isclose(abs(np.dot(v2, v3)), 0)
263
+ ):
256
264
  found = True
265
+ x = Vector.from_array(v1)
266
+ y = Vector.from_array(v2)
267
+ z = Vector.from_array(v3)
257
268
  break
258
269
  if not found:
259
- raise ValueError("No orthogonal vectors found.")
260
-
261
- z = x * y
270
+ raise ValueError("Cannot find coordinate system")
262
271
  return CoordinateSystem(x=x, y=y, z=z)
263
272
 
264
273
  def get_bounding_box(self) -> "Vertices":
ifctrano/bounding_box.py CHANGED
@@ -14,7 +14,7 @@ from pydantic import (
14
14
  Field,
15
15
  ConfigDict,
16
16
  )
17
- from shapely import Polygon # type: ignore
17
+ from scipy.spatial import ConvexHull # type: ignore
18
18
  from vedo import Line # type: ignore
19
19
 
20
20
  from ifctrano.base import (
@@ -98,31 +98,28 @@ class OrientedBoundingBox(BaseShow):
98
98
  lines.append(Line(a, b))
99
99
  return lines
100
100
 
101
- def contained(self, poly_1: Polygon, poly_2: Polygon) -> bool:
102
- include_1_in_2 = poly_1.contains(poly_2)
103
- include_2_in_1 = poly_2.contains(poly_1)
104
- return bool(include_2_in_1 or include_1_in_2)
105
-
106
101
  def intersect_faces(self, other: "OrientedBoundingBox") -> Optional[CommonSurface]:
107
102
  extend_surfaces = []
108
103
  for face in self.faces.faces:
109
104
 
110
105
  for other_face in other.faces.faces:
111
106
  vector = face.normal * other_face.normal
112
- if vector.is_a_zero():
107
+ if vector.is_null():
113
108
  projected_face_1 = face.vertices.project(face.vertices)
114
109
  projected_face_2 = face.vertices.project(other_face.vertices)
115
110
  polygon_1 = projected_face_1.to_polygon()
116
111
  polygon_2 = projected_face_2.to_polygon()
117
112
  intersection = polygon_2.intersection(polygon_1)
118
- if intersection.area > self.area_tolerance or self.contained(
119
- polygon_1, polygon_2
120
- ):
113
+ if intersection.area > self.area_tolerance:
121
114
  distance = projected_face_1.get_distance(projected_face_2)
122
115
  area = intersection.area
123
116
  try:
124
- direction_vector = (other.centroid - self.centroid).norm()
125
- orientation = direction_vector.project(face.normal).norm()
117
+ direction_vector = (
118
+ other.centroid - self.centroid
119
+ ).normalize()
120
+ orientation = direction_vector.project(
121
+ face.normal
122
+ ).normalize()
126
123
  except VectorWithNansError as e:
127
124
  logger.warning(
128
125
  "Orientation vector was not properly computed when computing the intersection between "
@@ -170,7 +167,9 @@ class OrientedBoundingBox(BaseShow):
170
167
  entity: Optional[entity_instance] = None,
171
168
  ) -> "OrientedBoundingBox":
172
169
  points_ = open3d.utility.Vector3dVector(vertices)
173
- mobb = open3d.geometry.OrientedBoundingBox.create_from_points_minimal(points_)
170
+ mobb = open3d.geometry.OrientedBoundingBox.create_from_points_minimal(
171
+ points_, robust=True
172
+ )
174
173
  height = (mobb.get_max_bound() - mobb.get_min_bound())[
175
174
  2
176
175
  ] # assuming that height is the z axis
@@ -191,6 +190,8 @@ class OrientedBoundingBox(BaseShow):
191
190
  entity_shape, entity_shape.geometry # type: ignore
192
191
  )
193
192
  vertices_ = Vertices.from_arrays(np.asarray(vertices))
194
-
195
- vertices_ = vertices_.get_bounding_box()
196
- return cls.from_vertices(vertices_.to_array(), entity)
193
+ hull = ConvexHull(vertices_.to_array())
194
+ vertices_ = Vertices.from_arrays(vertices_.to_array()[hull.vertices])
195
+ points_ = open3d.utility.Vector3dVector(vertices_.to_array())
196
+ aab = open3d.geometry.AxisAlignedBoundingBox.create_from_points(points_)
197
+ return cls.from_vertices(aab.get_box_points(), entity)
ifctrano/building.py CHANGED
@@ -102,11 +102,11 @@ def get_internal_elements(space1_boundaries: List[SpaceBoundaries]) -> InternalE
102
102
  and (
103
103
  boundary.common_surface.orientation
104
104
  * common_surface.orientation
105
- ).is_a_zero()
105
+ ).is_null()
106
106
  and (
107
107
  boundary_.common_surface.orientation
108
108
  * common_surface.orientation
109
- ).is_a_zero()
109
+ ).is_null()
110
110
  ) and boundary.common_surface.orientation.dot(
111
111
  boundary_.common_surface.orientation
112
112
  ) < 0:
@@ -200,7 +200,7 @@ class Building(BaseShow):
200
200
  return get_internal_elements(self.space_boundaries)
201
201
 
202
202
  @validate_call
203
- def create_model(
203
+ def create_network(
204
204
  self,
205
205
  library: Libraries = "Buildings",
206
206
  north_axis: Optional[Vector] = None,
@@ -232,6 +232,9 @@ class Building(BaseShow):
232
232
  )
233
233
  return network
234
234
 
235
+ def get_model(self) -> str:
236
+ return str(self.create_network().model())
237
+
235
238
  def save_model(self, library: Libraries = "Buildings") -> None:
236
- model_ = self.create_model(library)
239
+ model_ = self.create_network(library)
237
240
  Path(self.parent_folder.joinpath(f"{self.name}.mo")).write_text(model_.model())
ifctrano/exceptions.py CHANGED
@@ -24,3 +24,7 @@ class InvalidLibraryError(Exception):
24
24
 
25
25
  class VectorWithNansError(Exception):
26
26
  pass
27
+
28
+
29
+ class HasWindowsWithoutWallsError(Exception):
30
+ pass
ifctrano/main.py CHANGED
@@ -58,7 +58,7 @@ def create(
58
58
  if show_space_boundaries:
59
59
  print(f"{CHECKMARK} Showing space boundaries.")
60
60
  building.show()
61
- modelica_network = building.create_model(library=library) # type: ignore
61
+ modelica_network = building.create_network(library=library) # type: ignore
62
62
  progress.update(task, completed=True)
63
63
  task = progress.add_task(description="Writing model to file...", total=None)
64
64
  modelica_model_path.write_text(modelica_network.model())
@@ -68,7 +68,7 @@ def create(
68
68
  print("Simulating...")
69
69
  results = simulate(
70
70
  modelica_model_path.parent,
71
- building.create_model(
71
+ building.create_network(
72
72
  library=library # type: ignore
73
73
  ), # TODO: cannot use the network after cretingt he model
74
74
  )
@@ -79,7 +79,7 @@ def create(
79
79
  result_path = (
80
80
  Path(modelica_model_path.parent)
81
81
  / "results"
82
- / f"{modelica_model_path.stem}.building_res.mat"
82
+ / f"{modelica_model_path.stem.lower()}.building_res.mat"
83
83
  )
84
84
  if not result_path.exists():
85
85
  print(
@@ -87,7 +87,7 @@ def create(
87
87
  )
88
88
  return
89
89
  reporting = ModelDocumentation.from_network(
90
- building.create_model(library=library), # type: ignore
90
+ building.create_network(library=library), # type: ignore
91
91
  result=ResultFile(path=result_path),
92
92
  )
93
93
  html = to_html_reporting(reporting)
@@ -5,7 +5,7 @@ import ifcopenshell
5
5
  import ifcopenshell.geom
6
6
  import ifcopenshell.util.shape
7
7
  from ifcopenshell import entity_instance, file
8
- from pydantic import Field, BeforeValidator
8
+ from pydantic import Field, BeforeValidator, BaseModel
9
9
  from trano.data_models.conversion import SpaceParameter # type: ignore
10
10
  from trano.elements import Space as TranoSpace, ExternalWall, Window, BaseWall, ExternalDoor # type: ignore
11
11
  from trano.elements.system import Occupancy # type: ignore
@@ -23,6 +23,7 @@ from ifctrano.base import (
23
23
  )
24
24
  from ifctrano.bounding_box import OrientedBoundingBox
25
25
  from ifctrano.construction import glass, Constructions
26
+ from ifctrano.exceptions import HasWindowsWithoutWallsError
26
27
  from ifctrano.utils import remove_non_alphanumeric, _round, get_building_elements
27
28
 
28
29
  ROOF_VECTOR = Vector(x=0, y=0, z=1)
@@ -83,6 +84,87 @@ class Space(GlobalId):
83
84
  return f"space_{main_name}{remove_non_alphanumeric(self.entity.GlobalId)}"
84
85
 
85
86
 
87
+ class ExternalSpaceBoundaryGroup(BaseModelConfig):
88
+ constructions: List[BaseWall]
89
+ azimuth: float
90
+ tilt: Tilt
91
+
92
+ def __hash__(self) -> int:
93
+ return hash((self.azimuth, self.tilt.value))
94
+
95
+ def has_window(self) -> bool:
96
+ return any(
97
+ isinstance(construction, Window) for construction in self.constructions
98
+ )
99
+
100
+ def has_external_wall(self) -> bool:
101
+ return any(
102
+ isinstance(construction, ExternalWall)
103
+ for construction in self.constructions
104
+ )
105
+
106
+
107
+ class ExternalSpaceBoundaryGroups(BaseModelConfig):
108
+ space_boundary_groups: List[ExternalSpaceBoundaryGroup] = Field(
109
+ default_factory=list
110
+ )
111
+
112
+ @classmethod
113
+ def from_external_boundaries(
114
+ cls, external_boundaries: List[BaseWall]
115
+ ) -> "ExternalSpaceBoundaryGroups":
116
+ boundary_walls = [
117
+ ex
118
+ for ex in external_boundaries
119
+ if isinstance(ex, (ExternalWall, Window)) and ex.tilt == Tilt.wall
120
+ ]
121
+ space_boundary_groups = list(
122
+ {
123
+ ExternalSpaceBoundaryGroup(
124
+ constructions=[
125
+ ex_
126
+ for ex_ in boundary_walls
127
+ if ex_.azimuth == ex.azimuth and ex_.tilt == ex.tilt
128
+ ],
129
+ azimuth=ex.azimuth,
130
+ tilt=ex.tilt,
131
+ )
132
+ for ex in boundary_walls
133
+ }
134
+ )
135
+ return cls(space_boundary_groups=space_boundary_groups)
136
+
137
+ def has_windows_without_wall(self) -> bool:
138
+ return all(
139
+ not (group.has_window() and not group.has_external_wall())
140
+ for group in self.space_boundary_groups
141
+ )
142
+
143
+
144
+ class Azimuths(BaseModel):
145
+ north: List[float] = [0.0, 360]
146
+ east: List[float] = [90.0]
147
+ south: List[float] = [180.0]
148
+ west: List[float] = [270.0]
149
+ northeast: List[float] = [45.0]
150
+ southeast: List[float] = [135.0]
151
+ southwest: List[float] = [225.0]
152
+ northwest: List[float] = [315.0]
153
+ tolerance: float = 22.5
154
+
155
+ def get_azimuth(self, value: float) -> float:
156
+ fields = [field for field in self.model_fields if field not in ["tolerance"]]
157
+ for field in fields:
158
+ possibilities = getattr(self, field)
159
+ for possibility in possibilities:
160
+ if (
161
+ value >= possibility - self.tolerance
162
+ and value <= possibility + self.tolerance
163
+ ):
164
+ return float(possibilities[0])
165
+ raise ValueError(f"Value {value} is not within tolerance of any azimuths.")
166
+
167
+
86
168
  class SpaceBoundary(BaseModelConfig):
87
169
  bounding_box: OrientedBoundingBox
88
170
  entity: entity_instance
@@ -108,7 +190,7 @@ class SpaceBoundary(BaseModelConfig):
108
190
  return ExternalWall(
109
191
  name=self.boundary_name(),
110
192
  surface=self.common_surface.area,
111
- azimuth=azimuth,
193
+ azimuth=Azimuths().get_azimuth(azimuth),
112
194
  tilt=Tilt.wall,
113
195
  construction=constructions.get_construction(self.entity),
114
196
  )
@@ -116,7 +198,7 @@ class SpaceBoundary(BaseModelConfig):
116
198
  return ExternalDoor(
117
199
  name=self.boundary_name(),
118
200
  surface=self.common_surface.area,
119
- azimuth=azimuth,
201
+ azimuth=Azimuths().get_azimuth(azimuth),
120
202
  tilt=Tilt.wall,
121
203
  construction=constructions.get_construction(self.entity),
122
204
  )
@@ -124,7 +206,7 @@ class SpaceBoundary(BaseModelConfig):
124
206
  return Window(
125
207
  name=self.boundary_name(),
126
208
  surface=self.common_surface.area,
127
- azimuth=azimuth,
209
+ azimuth=Azimuths().get_azimuth(azimuth),
128
210
  tilt=Tilt.wall,
129
211
  construction=glass,
130
212
  )
@@ -169,25 +251,6 @@ class SpaceBoundary(BaseModelConfig):
169
251
  )
170
252
 
171
253
 
172
- def _reassign_constructions(external_boundaries: List[BaseWall]) -> None:
173
- results = {
174
- tuple(sorted([ex.name, ex_.name])): (ex, ex_)
175
- for ex in external_boundaries
176
- for ex_ in external_boundaries
177
- if ex.construction.name != ex_.construction.name
178
- and ex.azimuth == ex_.azimuth
179
- and isinstance(ex, ExternalWall)
180
- and isinstance(ex_, ExternalWall)
181
- and ex.tilt == Tilt.wall
182
- and ex_.tilt == Tilt.wall
183
- }
184
- if results:
185
- for walls in results.values():
186
- construction = next(w.construction for w in walls)
187
- for w in walls:
188
- w.construction = construction.model_copy(deep=True)
189
-
190
-
191
254
  class SpaceBoundaries(BaseShow):
192
255
  space: Space
193
256
  boundaries: List[SpaceBoundary] = Field(default_factory=list)
@@ -220,8 +283,13 @@ class SpaceBoundaries(BaseShow):
220
283
  if boundary_model:
221
284
  external_boundaries.append(boundary_model)
222
285
 
223
- _reassign_constructions(external_boundaries)
224
-
286
+ external_space_boundaries_group = (
287
+ ExternalSpaceBoundaryGroups.from_external_boundaries(external_boundaries)
288
+ )
289
+ if not external_space_boundaries_group.has_windows_without_wall():
290
+ raise HasWindowsWithoutWallsError(
291
+ f"Space {self.space.global_id} has a boundary that has a windows but without walls."
292
+ )
225
293
  return TranoSpace(
226
294
  name=self.space.space_name(),
227
295
  occupancy=Occupancy(),
@@ -258,7 +326,7 @@ class SpaceBoundaries(BaseShow):
258
326
  if entity.is_a() not in ["IfcSpace"]
259
327
  }
260
328
 
261
- for element in elements_:
329
+ for element in list(elements_):
262
330
  space_boundary = SpaceBoundary.from_space_and_element(
263
331
  space_.bounding_box, element
264
332
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ifctrano
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Package for generating building energy simulation model from IFC
5
5
  License: GPL V3
6
6
  Keywords: BIM,IFC,energy simulation,modelica,building energy simulation,buildings,ideas
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3.12
15
15
  Requires-Dist: ifcopenshell (>=0.8.1.post1,<0.9.0)
16
16
  Requires-Dist: open3d (>=0.19.0,<0.20.0)
17
17
  Requires-Dist: shapely (>=2.0.7,<3.0.0)
18
- Requires-Dist: trano (>=0.2.0,<0.3.0)
18
+ Requires-Dist: trano (>=0.5.0,<0.6.0)
19
19
  Requires-Dist: typer (>=0.12.5,<0.13.0)
20
20
  Requires-Dist: vedo (>=2025.5.3,<2026.0.0)
21
21
  Project-URL: Repository, https://github.com/andoludo/ifctrano
@@ -0,0 +1,16 @@
1
+ ifctrano/__init__.py,sha256=UT1-izHuXPq06nSeKzgs6qoVRfc6RqIhabf-ZKgAXf4,76
2
+ ifctrano/base.py,sha256=BQpMZwSq0tiUN0g56CfzF11lIlijwi7VuLjC2XY4mx0,14224
3
+ ifctrano/bounding_box.py,sha256=k74UIutxaNxfdfAjSqvr6Jj3gu6OP4sbQB_kTnZlltw,7431
4
+ ifctrano/building.py,sha256=MUaSqWQP_4kZbH6-yrjRahm8poxLBHiBlMjOFNUV88o,8910
5
+ ifctrano/construction.py,sha256=KGiJJ6L0D7mK254GYl6CO7TKoTNoUSJeZCSHzbkhslo,7244
6
+ ifctrano/example/verification.ifc,sha256=tQ9QcubT_wrbb-sc1WRRwYpb2cbkWm3dnRfXdP5GTTg,131
7
+ ifctrano/exceptions.py,sha256=SKFiFEhn0bW2Dm4fVHeMGIQ1C84nlLhapxy8y46SkfM,488
8
+ ifctrano/main.py,sha256=cXev3w-uC_wgi4lxtvn4zrAogX-6pUSR0GNav3s0Z5g,4793
9
+ ifctrano/space_boundary.py,sha256=lDhq05FC8BMZ07vGhU9smXj-gnOy-zoqIpkoW0QFCeI,11922
10
+ ifctrano/types.py,sha256=wxKVb2R4Dz58YzN4PzgXhuuVw-UYobSh9fnYWQrYlqQ,130
11
+ ifctrano/utils.py,sha256=AjQrIqbKAk4CWCcfeRHSZtWT6AAxQLyj-uDWPdUU6UU,712
12
+ ifctrano-0.3.0.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
13
+ ifctrano-0.3.0.dist-info/METADATA,sha256=5hdAUTbC0kirbMFK_uvxKk76HGHHf-f8sHKJxkhkaWs,3533
14
+ ifctrano-0.3.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
15
+ ifctrano-0.3.0.dist-info/entry_points.txt,sha256=_2daDejazkphufyEu0m3lOeTio53WYmjol3KmSN0JM4,46
16
+ ifctrano-0.3.0.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- ifctrano/__init__.py,sha256=UT1-izHuXPq06nSeKzgs6qoVRfc6RqIhabf-ZKgAXf4,76
2
- ifctrano/base.py,sha256=upXv93ahpQHN-AKkl9MEsMvS3BGw7SFjG-oGJ2VNdWM,13812
3
- ifctrano/bounding_box.py,sha256=ctHImomBY7C0sLWGxwXP0VzqdmtLzWeH0CNo6Y-4TAo,7334
4
- ifctrano/building.py,sha256=E3D6Bg6MeHQ3JeBMMxMFwLRvmo38uPYyGON-G8uTE_k,8827
5
- ifctrano/construction.py,sha256=KGiJJ6L0D7mK254GYl6CO7TKoTNoUSJeZCSHzbkhslo,7244
6
- ifctrano/example/verification.ifc,sha256=tQ9QcubT_wrbb-sc1WRRwYpb2cbkWm3dnRfXdP5GTTg,131
7
- ifctrano/exceptions.py,sha256=1FQeuuq_lVZ4CawwewQvkE8OlDQwhBTbKfmc2FiZodo,431
8
- ifctrano/main.py,sha256=k6FyWD2hp_5tYkV-dqL42jdmfke53Jlkvz8-vc-VufA,4779
9
- ifctrano/space_boundary.py,sha256=OOIZJFWU3fQ_nm4RcXjC4Y55mUxGe789eParP533LEY,9512
10
- ifctrano/types.py,sha256=wxKVb2R4Dz58YzN4PzgXhuuVw-UYobSh9fnYWQrYlqQ,130
11
- ifctrano/utils.py,sha256=AjQrIqbKAk4CWCcfeRHSZtWT6AAxQLyj-uDWPdUU6UU,712
12
- ifctrano-0.2.0.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
13
- ifctrano-0.2.0.dist-info/METADATA,sha256=j0yo3sbSbAAcxg-ceS8vDXrlPD-DPScHfGgDF1mU-lQ,3533
14
- ifctrano-0.2.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
15
- ifctrano-0.2.0.dist-info/entry_points.txt,sha256=_2daDejazkphufyEu0m3lOeTio53WYmjol3KmSN0JM4,46
16
- ifctrano-0.2.0.dist-info/RECORD,,