ifctrano 0.2.0__tar.gz → 0.3.0__tar.gz
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-0.2.0 → ifctrano-0.3.0}/PKG-INFO +2 -2
- {ifctrano-0.2.0 → ifctrano-0.3.0}/ifctrano/base.py +20 -11
- {ifctrano-0.2.0 → ifctrano-0.3.0}/ifctrano/bounding_box.py +17 -16
- {ifctrano-0.2.0 → ifctrano-0.3.0}/ifctrano/building.py +7 -4
- {ifctrano-0.2.0 → ifctrano-0.3.0}/ifctrano/exceptions.py +4 -0
- {ifctrano-0.2.0 → ifctrano-0.3.0}/ifctrano/main.py +4 -4
- {ifctrano-0.2.0 → ifctrano-0.3.0}/ifctrano/space_boundary.py +94 -26
- {ifctrano-0.2.0 → ifctrano-0.3.0}/pyproject.toml +2 -2
- {ifctrano-0.2.0 → ifctrano-0.3.0}/LICENSE +0 -0
- {ifctrano-0.2.0 → ifctrano-0.3.0}/README.md +0 -0
- {ifctrano-0.2.0 → ifctrano-0.3.0}/ifctrano/__init__.py +0 -0
- {ifctrano-0.2.0 → ifctrano-0.3.0}/ifctrano/construction.py +0 -0
- {ifctrano-0.2.0 → ifctrano-0.3.0}/ifctrano/example/verification.ifc +0 -0
- {ifctrano-0.2.0 → ifctrano-0.3.0}/ifctrano/types.py +0 -0
- {ifctrano-0.2.0 → ifctrano-0.3.0}/ifctrano/utils.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: ifctrano
|
3
|
-
Version: 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.
|
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
|
@@ -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
|
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
|
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
|
-
|
251
|
-
|
254
|
+
vectors = [
|
255
|
+
(a - b).normalize().to_array() for a, b in combinations(self.points, 2)
|
256
|
+
]
|
252
257
|
found = False
|
253
|
-
for
|
254
|
-
|
255
|
-
|
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("
|
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":
|
@@ -14,7 +14,7 @@ from pydantic import (
|
|
14
14
|
Field,
|
15
15
|
ConfigDict,
|
16
16
|
)
|
17
|
-
from
|
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.
|
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
|
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 = (
|
125
|
-
|
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(
|
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_.
|
196
|
-
|
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)
|
@@ -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
|
-
).
|
105
|
+
).is_null()
|
106
106
|
and (
|
107
107
|
boundary_.common_surface.orientation
|
108
108
|
* common_surface.orientation
|
109
|
-
).
|
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
|
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.
|
239
|
+
model_ = self.create_network(library)
|
237
240
|
Path(self.parent_folder.joinpath(f"{self.name}.mo")).write_text(model_.model())
|
@@ -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.
|
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.
|
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.
|
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
|
-
|
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
|
[tool.poetry]
|
2
2
|
name = "ifctrano"
|
3
|
-
version = "0.
|
3
|
+
version = "0.3.0"
|
4
4
|
description = "Package for generating building energy simulation model from IFC"
|
5
5
|
authors = ["Ando Andriamamonjy <andoludovic.andriamamonjy@gmail.com>"]
|
6
6
|
license = "GPL V3"
|
@@ -11,7 +11,7 @@ keywords = ["BIM","IFC","energy simulation", "modelica", "building energy simula
|
|
11
11
|
[tool.poetry.dependencies]
|
12
12
|
python = ">=3.10,<3.13"
|
13
13
|
ifcopenshell = "^0.8.1.post1"
|
14
|
-
trano = "^0.
|
14
|
+
trano = "^0.5.0"
|
15
15
|
shapely = "^2.0.7"
|
16
16
|
typer = "^0.12.5"
|
17
17
|
vedo = "^2025.5.3"
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|