ifctrano 0.1.7__tar.gz → 0.1.8__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ifctrano
3
- Version: 0.1.7
3
+ Version: 0.1.8
4
4
  Summary: Package for generating building energy simulation model from IFC
5
5
  Home-page: https://github.com/andoludo/ifctrano
6
6
  License: GPL V3
@@ -8,6 +8,7 @@ from pydantic import BaseModel, BeforeValidator, ConfigDict
8
8
  settings = ifcopenshell.geom.settings() # type: ignore
9
9
  Coordinate = Literal["x", "y", "z"]
10
10
  AREA_TOLERANCE = 0.5
11
+ ROUNDING_FACTOR = 5
11
12
 
12
13
 
13
14
  class BaseModelConfig(BaseModel):
@@ -1,5 +1,5 @@
1
1
  from logging import getLogger
2
- from typing import List, Optional, Any, Tuple
2
+ from typing import List, Optional, Any, Tuple, cast
3
3
 
4
4
  import ifcopenshell
5
5
  import numpy as np
@@ -23,6 +23,7 @@ from ifctrano.base import (
23
23
  settings,
24
24
  CommonSurface,
25
25
  AREA_TOLERANCE,
26
+ ROUNDING_FACTOR,
26
27
  )
27
28
  from ifctrano.exceptions import BoundingBoxFaceError
28
29
 
@@ -77,7 +78,12 @@ class BoundingBoxFace(BaseModelConfig):
77
78
  vertices=vertices, normal=normal, coordinate_system=coordinate_system
78
79
  )
79
80
 
81
+ def get_face_area(self) -> float:
82
+ polygon_2d = self.get_2d_polygon(self.coordinate_system)
83
+ return cast(float, round(polygon_2d.polygon.area, ROUNDING_FACTOR))
84
+
80
85
  def get_2d_polygon(self, coordinate_system: CoordinateSystem) -> Polygon2D:
86
+
81
87
  projected_vertices = coordinate_system.inverse(self.vertices.to_array())
82
88
  projected_normal_index = Vector.from_array(
83
89
  coordinate_system.inverse(self.normal.to_array())
@@ -152,6 +158,8 @@ class OrientedBoundingBox(BaseModel):
152
158
  faces: BoundingBoxFaces
153
159
  centroid: Point
154
160
  area_tolerance: float = Field(default=AREA_TOLERANCE)
161
+ volume: float
162
+ height: float
155
163
 
156
164
  def intersect_faces(self, other: "OrientedBoundingBox") -> Optional[CommonSurface]:
157
165
  extend_surfaces = []
@@ -160,11 +168,9 @@ class OrientedBoundingBox(BaseModel):
160
168
  for other_face in other.faces.faces:
161
169
  vector = face.normal * other_face.normal
162
170
  if vector.is_a_zero():
163
-
164
171
  polygon_1 = other_face.get_2d_polygon(face.coordinate_system)
165
172
  polygon_2 = face.get_2d_polygon(face.coordinate_system)
166
173
  intersection = polygon_2.polygon.intersection(polygon_1.polygon)
167
-
168
174
  if intersection.area > self.area_tolerance:
169
175
  distance = abs(polygon_1.length - polygon_2.length)
170
176
  area = intersection.area
@@ -194,8 +200,8 @@ class OrientedBoundingBox(BaseModel):
194
200
  ) -> "OrientedBoundingBox":
195
201
  vertices_np = np.array(vertices)
196
202
  points = np.asarray(vertices_np)
197
- cov = np.cov(points, y=None, rowvar=0, bias=1) # type: ignore
198
- v, vect = np.linalg.eig(cov)
203
+ cov = np.cov(points, y=None, rowvar=0, bias=0) # type: ignore
204
+ v, vect = np.linalg.eig(np.round(cov, ROUNDING_FACTOR))
199
205
  tvect = np.transpose(vect)
200
206
  points_r = np.dot(points, np.linalg.inv(tvect))
201
207
 
@@ -206,9 +212,12 @@ class OrientedBoundingBox(BaseModel):
206
212
  ymin, ymax = co_min[1], co_max[1]
207
213
  zmin, zmax = co_min[2], co_max[2]
208
214
 
209
- xdif = (xmax - xmin) * 0.5
210
- ydif = (ymax - ymin) * 0.5
211
- zdif = (zmax - zmin) * 0.5
215
+ x_len = xmax - xmin
216
+ y_len = ymax - ymin
217
+ z_len = zmax - zmin
218
+ xdif = x_len * 0.5
219
+ ydif = y_len * 0.5
220
+ zdif = z_len * 0.5
212
221
 
213
222
  cx = xmin + xdif
214
223
  cy = ymin + ydif
@@ -225,7 +234,11 @@ class OrientedBoundingBox(BaseModel):
225
234
  [cx + xdif, cy - ydif, cz - zdif],
226
235
  ]
227
236
  )
228
- corners = np.dot(corners, tvect)
237
+ corners_ = np.dot(corners, tvect)
238
+ dims = np.transpose(corners_)
239
+ x_size = np.max(dims[0]) - np.min(dims[0])
240
+ y_size = np.max(dims[1]) - np.min(dims[1])
241
+ z_size = np.max(dims[2]) - np.min(dims[2])
229
242
  coordinate_system = CoordinateSystem.from_array(tvect)
230
243
  c = P(x=cx, y=cy, z=cz)
231
244
  d = P(x=xdif, y=ydif, z=zdif)
@@ -233,6 +246,8 @@ class OrientedBoundingBox(BaseModel):
233
246
  return cls(
234
247
  faces=faces,
235
248
  centroid=Point.from_array(coordinate_system.project(c.to_array())),
249
+ volume=x_size * y_size * z_size,
250
+ height=z_size,
236
251
  )
237
252
 
238
253
  @classmethod
@@ -0,0 +1,133 @@
1
+ from pathlib import Path
2
+ from typing import List
3
+
4
+ import ifcopenshell
5
+ from ifcopenshell import file, entity_instance
6
+ from pydantic import validate_call, Field, model_validator
7
+ from trano.elements import InternalElement # type: ignore
8
+ from trano.elements.library.library import Library # type: ignore
9
+ from trano.elements.types import Tilt # type: ignore
10
+ from trano.topology import Network # type: ignore
11
+
12
+ from ifctrano.base import BaseModelConfig, Libraries
13
+ from ifctrano.exceptions import IfcFileNotFoundError
14
+ from ifctrano.space_boundary import (
15
+ SpaceBoundaries,
16
+ initialize_tree,
17
+ Space,
18
+ construction,
19
+ )
20
+
21
+
22
+ def get_spaces(ifcopenshell_file: file) -> List[entity_instance]:
23
+ return ifcopenshell_file.by_type("IfcSpace")
24
+
25
+
26
+ class IfcInternalElement(BaseModelConfig):
27
+ spaces: List[Space]
28
+ element: entity_instance
29
+ area: float
30
+
31
+
32
+ class InternalElements(BaseModelConfig):
33
+ elements: List[IfcInternalElement] = Field(default_factory=list)
34
+
35
+ def internal_element_ids(self) -> List[str]:
36
+ return list({e.element.GlobalId for e in self.elements})
37
+
38
+
39
+ class Building(BaseModelConfig):
40
+ name: str
41
+ space_boundaries: List[SpaceBoundaries]
42
+ ifc_file: file
43
+ parent_folder: Path
44
+ internal_elements: InternalElements = Field(default_factory=InternalElements)
45
+
46
+ @classmethod
47
+ def from_ifc(cls, ifc_file_path: Path) -> "Building":
48
+ if not ifc_file_path.exists():
49
+ raise IfcFileNotFoundError(
50
+ f"File specified {ifc_file_path} does not exist."
51
+ )
52
+ ifc_file = ifcopenshell.open(str(ifc_file_path))
53
+ tree = initialize_tree(ifc_file)
54
+ spaces = get_spaces(ifc_file)
55
+ space_boundaries = [
56
+ SpaceBoundaries.from_space_entity(ifc_file, tree, space) for space in spaces
57
+ ]
58
+ return cls(
59
+ space_boundaries=space_boundaries,
60
+ ifc_file=ifc_file,
61
+ parent_folder=ifc_file_path.parent,
62
+ name=ifc_file_path.stem,
63
+ )
64
+
65
+ @model_validator(mode="after")
66
+ def _validator(self) -> "Building":
67
+ self.internal_elements = self.get_adjacency()
68
+ return self
69
+
70
+ def get_adjacency(self) -> InternalElements:
71
+ elements = []
72
+ for space_boundaries_ in self.space_boundaries:
73
+ for space_boundaries__ in self.space_boundaries:
74
+ space_1 = space_boundaries_.space
75
+ space_2 = space_boundaries__.space
76
+ if space_1.global_id == space_2.global_id:
77
+ continue
78
+ common_surface = space_1.bounding_box.intersect_faces(
79
+ space_2.bounding_box
80
+ )
81
+ for boundary in space_boundaries_.boundaries:
82
+ for boundary_ in space_boundaries__.boundaries:
83
+ if (
84
+ boundary.entity.GlobalId == boundary_.entity.GlobalId
85
+ and boundary.common_surface
86
+ and boundary_.common_surface
87
+ and common_surface
88
+ and (
89
+ boundary.common_surface.orientation
90
+ * common_surface.orientation
91
+ ).is_a_zero()
92
+ and (
93
+ boundary_.common_surface.orientation
94
+ * common_surface.orientation
95
+ ).is_a_zero()
96
+ ):
97
+ elements.append( # noqa: PERF401
98
+ IfcInternalElement(
99
+ spaces=[space_1, space_2],
100
+ element=boundary_.entity,
101
+ area=common_surface.area,
102
+ )
103
+ )
104
+ return InternalElements(elements=elements)
105
+
106
+ @validate_call
107
+ def create_model(self, library: Libraries = "Buildings") -> str:
108
+ network = Network(name=self.name, library=Library.from_configuration(library))
109
+ spaces = {
110
+ space_boundary.space.global_id: space_boundary.model(
111
+ self.internal_elements.internal_element_ids()
112
+ )
113
+ for space_boundary in self.space_boundaries
114
+ }
115
+ network.add_boiler_plate_spaces(list(spaces.values()))
116
+ for internal_element in self.internal_elements.elements:
117
+ space_1 = internal_element.spaces[0]
118
+ space_2 = internal_element.spaces[1]
119
+ network.connect_spaces(
120
+ spaces[space_1.global_id],
121
+ spaces[space_2.global_id],
122
+ InternalElement(
123
+ azimuth=10,
124
+ construction=construction,
125
+ surface=internal_element.area,
126
+ tilt=Tilt.wall,
127
+ ),
128
+ )
129
+ return network.model() # type: ignore
130
+
131
+ def save_model(self, library: Libraries = "Buildings") -> None:
132
+ model_ = self.create_model(library)
133
+ Path(self.parent_folder.joinpath(f"{self.name}.mo")).write_text(model_)
@@ -8,3 +8,11 @@ class BoundingBoxFaceError(Exception):
8
8
 
9
9
  class IfcFileNotFoundError(FileNotFoundError):
10
10
  pass
11
+
12
+
13
+ class SpaceSurfaceAreaNullError(Exception):
14
+ pass
15
+
16
+
17
+ class InvalidLibraryError(Exception):
18
+ pass
@@ -1,12 +1,12 @@
1
1
  from pathlib import Path
2
- from typing import Annotated
2
+ from typing import Annotated, get_args
3
3
 
4
4
  import typer
5
- from pydantic import validate_call
6
5
  from rich.progress import Progress, SpinnerColumn, TextColumn
7
6
 
8
7
  from ifctrano.base import Libraries
9
8
  from ifctrano.building import Building
9
+ from ifctrano.exceptions import InvalidLibraryError
10
10
 
11
11
  app = typer.Typer()
12
12
  CHECKMARK = "[green]✔[/green]"
@@ -14,14 +14,13 @@ CROSS_MARK = "[red]✘[/red]"
14
14
 
15
15
 
16
16
  @app.command()
17
- @validate_call
18
17
  def create(
19
18
  model: Annotated[
20
19
  str,
21
20
  typer.Argument(help="Local path to the ifc file."),
22
21
  ],
23
22
  library: Annotated[
24
- Libraries,
23
+ str,
25
24
  typer.Argument(help="Modelica library to be used for simulation."),
26
25
  ] = "Buildings",
27
26
  ) -> None:
@@ -30,6 +29,10 @@ def create(
30
29
  TextColumn("[progress.description]{task.description}"),
31
30
  transient=True,
32
31
  ) as progress:
32
+ if library not in get_args(Libraries):
33
+ raise InvalidLibraryError(
34
+ f"Invalid library {library}. Valid libraries are {get_args(Libraries)}"
35
+ )
33
36
  modelica_model_path = Path(model).resolve().with_suffix(".mo")
34
37
  task = progress.add_task(
35
38
  description=f"Generating model {modelica_model_path.name} with library {library} from {model}",
@@ -7,17 +7,23 @@ import ifcopenshell.geom
7
7
  import ifcopenshell.util.shape
8
8
  from ifcopenshell import entity_instance, file
9
9
  from pydantic import BaseModel, Field
10
+ from trano.data_models.conversion import SpaceParameter # type: ignore
10
11
  from trano.elements import Space as TranoSpace, ExternalWall, Window, BaseWall # type: ignore
11
12
  from trano.elements.construction import Construction, Layer, Material # type: ignore
12
13
  from trano.elements.system import Occupancy # type: ignore
13
14
  from trano.elements.types import Azimuth, Tilt # type: ignore
14
15
 
15
- from ifctrano.base import GlobalId, settings, BaseModelConfig, CommonSurface
16
+ from ifctrano.base import (
17
+ GlobalId,
18
+ settings,
19
+ BaseModelConfig,
20
+ CommonSurface,
21
+ ROUNDING_FACTOR,
22
+ )
16
23
  from ifctrano.bounding_box import OrientedBoundingBox
17
24
 
18
25
 
19
26
  def initialize_tree(ifc_file: file) -> ifcopenshell.geom.tree:
20
-
21
27
  tree = ifcopenshell.geom.tree()
22
28
 
23
29
  iterator = ifcopenshell.geom.iterator(
@@ -39,6 +45,37 @@ class Space(GlobalId):
39
45
  name: Optional[str] = None
40
46
  bounding_box: OrientedBoundingBox
41
47
  entity: entity_instance
48
+ average_room_height: float
49
+ floor_area: float
50
+ bounding_box_height: float
51
+ bounding_box_volume: float
52
+
53
+ @classmethod
54
+ def from_entity(cls, entity: entity_instance) -> "Space":
55
+ bounding_box = OrientedBoundingBox.from_entity(entity)
56
+ entity_shape = ifcopenshell.geom.create_shape(settings, entity)
57
+ area = ifcopenshell.util.shape.get_footprint_area(entity_shape.geometry) # type: ignore
58
+ volume = ifcopenshell.util.shape.get_volume(entity_shape.geometry) # type: ignore
59
+ if area:
60
+ average_room_height = volume / area
61
+ else:
62
+ area = bounding_box.volume / bounding_box.height
63
+ average_room_height = bounding_box.height
64
+ return cls(
65
+ global_id=entity.GlobalId,
66
+ name=entity.Name,
67
+ bounding_box=bounding_box,
68
+ entity=entity,
69
+ average_room_height=average_room_height,
70
+ floor_area=area,
71
+ bounding_box_height=bounding_box.height,
72
+ bounding_box_volume=bounding_box.volume,
73
+ )
74
+
75
+ def check_volume(self) -> bool:
76
+ return round(self.bounding_box_volume, ROUNDING_FACTOR) == round(
77
+ self.floor_area * self.average_room_height, ROUNDING_FACTOR
78
+ )
42
79
 
43
80
  def space_name(self) -> str:
44
81
  main_name = (
@@ -67,23 +104,32 @@ class SpaceBoundary(BaseModelConfig):
67
104
  bounding_box: OrientedBoundingBox
68
105
  entity: entity_instance
69
106
  common_surface: CommonSurface
70
- adjacent_space: Optional[Space] = None
107
+ adjacent_spaces: List[Space] = Field(default_factory=list)
71
108
 
72
- def model_element(self) -> Optional[BaseWall]:
109
+ def model_element(self, exclude_entities: List[str]) -> Optional[BaseWall]:
110
+ if self.entity.GlobalId in exclude_entities:
111
+ return None
73
112
  if "wall" in self.entity.is_a().lower():
74
113
  return ExternalWall(
75
- surface=6.44,
114
+ surface=self.common_surface.area,
76
115
  azimuth=Azimuth.south,
77
116
  tilt=Tilt.wall,
78
117
  construction=construction,
79
118
  )
80
119
  if "window" in self.entity.is_a().lower():
81
120
  return Window(
82
- surface=6.44,
121
+ surface=self.common_surface.area,
83
122
  azimuth=Azimuth.south,
84
123
  tilt=Tilt.wall,
85
124
  construction=construction,
86
125
  )
126
+ if "roof" in self.entity.is_a().lower():
127
+ return ExternalWall(
128
+ surface=self.common_surface.area,
129
+ azimuth=Azimuth.south,
130
+ tilt=Tilt.ceiling,
131
+ construction=construction,
132
+ )
87
133
 
88
134
  return None
89
135
 
@@ -112,14 +158,18 @@ class SpaceBoundaries(BaseModel):
112
158
  space: Space
113
159
  boundaries: List[SpaceBoundary] = Field(default_factory=list)
114
160
 
115
- def model(self) -> TranoSpace:
161
+ def model(self, exclude_entities: List[str]) -> TranoSpace:
116
162
  return TranoSpace(
117
163
  name=self.space.space_name(),
118
164
  occupancy=Occupancy(),
165
+ parameters=SpaceParameter(
166
+ floor_area=self.space.floor_area,
167
+ average_room_height=self.space.average_room_height,
168
+ ),
119
169
  external_boundaries=[
120
- boundary.model_element()
170
+ boundary.model_element(exclude_entities)
121
171
  for boundary in self.boundaries
122
- if boundary.model_element()
172
+ if boundary.model_element(exclude_entities)
123
173
  ],
124
174
  )
125
175
 
@@ -130,13 +180,7 @@ class SpaceBoundaries(BaseModel):
130
180
  tree: ifcopenshell.geom.tree,
131
181
  space: entity_instance,
132
182
  ) -> "SpaceBoundaries":
133
- bounding_box = OrientedBoundingBox.from_entity(space)
134
- space_ = Space(
135
- global_id=space.GlobalId,
136
- name=space.Name,
137
- bounding_box=bounding_box,
138
- entity=space,
139
- )
183
+ space_ = Space.from_entity(space)
140
184
  clashes = tree.clash_clearance_many(
141
185
  [space],
142
186
  ifcopenshell_file.by_type("IfcWall")
@@ -157,7 +201,7 @@ class SpaceBoundaries(BaseModel):
157
201
  if element.GlobalId == space.GlobalId:
158
202
  continue
159
203
  space_boundary = SpaceBoundary.from_space_and_element(
160
- bounding_box, element
204
+ space_.bounding_box, element
161
205
  )
162
206
  if space_boundary:
163
207
  space_boundaries.append(space_boundary)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "ifctrano"
3
- version = "0.1.7"
3
+ version = "0.1.8"
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"
@@ -34,7 +34,7 @@ black = "^24.10.0"
34
34
  pytest = "^7.4.3"
35
35
 
36
36
  [tool.poetry.scripts]
37
- trano = "ifctrano.main:app"
37
+ ifctrano = "ifctrano.main:app"
38
38
 
39
39
 
40
40
  [tool.poetry.group.docs.dependencies]
@@ -1,55 +0,0 @@
1
- from pathlib import Path
2
- from typing import List
3
-
4
- import ifcopenshell
5
- from ifcopenshell import file, entity_instance
6
- from pydantic import validate_call
7
- from trano.elements.library.library import Library # type: ignore
8
-
9
- from trano.topology import Network # type: ignore
10
-
11
- from ifctrano.base import BaseModelConfig, Libraries
12
- from ifctrano.exceptions import IfcFileNotFoundError
13
- from ifctrano.space_boundary import SpaceBoundaries, initialize_tree
14
-
15
-
16
- def get_spaces(ifcopenshell_file: file) -> List[entity_instance]:
17
- return ifcopenshell_file.by_type("IfcSpace")
18
-
19
-
20
- class Building(BaseModelConfig):
21
- name: str
22
- space_boundaries: List[SpaceBoundaries]
23
- ifc_file: file
24
- parent_folder: Path
25
-
26
- @classmethod
27
- def from_ifc(cls, ifc_file_path: Path) -> "Building":
28
- if not ifc_file_path.exists():
29
- raise IfcFileNotFoundError(
30
- f"File specified {ifc_file_path} does not exist."
31
- )
32
- ifc_file = ifcopenshell.open(str(ifc_file_path))
33
- tree = initialize_tree(ifc_file)
34
- spaces = get_spaces(ifc_file)
35
- space_boundaries = [
36
- SpaceBoundaries.from_space_entity(ifc_file, tree, space) for space in spaces
37
- ]
38
- return cls(
39
- space_boundaries=space_boundaries,
40
- ifc_file=ifc_file,
41
- parent_folder=ifc_file_path.parent,
42
- name=ifc_file_path.stem,
43
- )
44
-
45
- @validate_call
46
- def create_model(self, library: Libraries = "Buildings") -> str:
47
- network = Network(name=self.name, library=Library.from_configuration(library))
48
- network.add_boiler_plate_spaces(
49
- [space_boundary.model() for space_boundary in self.space_boundaries]
50
- )
51
- return network.model() # type: ignore
52
-
53
- def save_model(self, library: Libraries = "Buildings") -> None:
54
- model_ = self.create_model(library)
55
- Path(self.parent_folder.joinpath(f"{self.name}.mo")).write_text(model_)
File without changes
File without changes
File without changes