ifctrano 0.1.7__py3-none-any.whl → 0.1.8__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
@@ -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):
ifctrano/bounding_box.py CHANGED
@@ -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
ifctrano/building.py CHANGED
@@ -3,25 +3,45 @@ from typing import List
3
3
 
4
4
  import ifcopenshell
5
5
  from ifcopenshell import file, entity_instance
6
- from pydantic import validate_call
6
+ from pydantic import validate_call, Field, model_validator
7
+ from trano.elements import InternalElement # type: ignore
7
8
  from trano.elements.library.library import Library # type: ignore
8
-
9
+ from trano.elements.types import Tilt # type: ignore
9
10
  from trano.topology import Network # type: ignore
10
11
 
11
12
  from ifctrano.base import BaseModelConfig, Libraries
12
13
  from ifctrano.exceptions import IfcFileNotFoundError
13
- from ifctrano.space_boundary import SpaceBoundaries, initialize_tree
14
+ from ifctrano.space_boundary import (
15
+ SpaceBoundaries,
16
+ initialize_tree,
17
+ Space,
18
+ construction,
19
+ )
14
20
 
15
21
 
16
22
  def get_spaces(ifcopenshell_file: file) -> List[entity_instance]:
17
23
  return ifcopenshell_file.by_type("IfcSpace")
18
24
 
19
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
+
20
39
  class Building(BaseModelConfig):
21
40
  name: str
22
41
  space_boundaries: List[SpaceBoundaries]
23
42
  ifc_file: file
24
43
  parent_folder: Path
44
+ internal_elements: InternalElements = Field(default_factory=InternalElements)
25
45
 
26
46
  @classmethod
27
47
  def from_ifc(cls, ifc_file_path: Path) -> "Building":
@@ -42,12 +62,70 @@ class Building(BaseModelConfig):
42
62
  name=ifc_file_path.stem,
43
63
  )
44
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
+
45
106
  @validate_call
46
107
  def create_model(self, library: Libraries = "Buildings") -> str:
47
108
  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
- )
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
+ )
51
129
  return network.model() # type: ignore
52
130
 
53
131
  def save_model(self, library: Libraries = "Buildings") -> None:
ifctrano/exceptions.py CHANGED
@@ -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
ifctrano/main.py CHANGED
@@ -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
  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
@@ -0,0 +1,12 @@
1
+ ifctrano/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ ifctrano/base.py,sha256=VjGWioeysspdTrgofhykw0eh6n9UkaXHp5gvOSVJ4hY,4991
3
+ ifctrano/bounding_box.py,sha256=G0FcqCNG-nBrUkA5Hw8wWqvJODxWUxOo_x8jrk63YAc,8983
4
+ ifctrano/building.py,sha256=mrqMAtPWmTerLXdtFHTvGLxXI5bg23gqEftlquSMgtw,5129
5
+ ifctrano/exceptions.py,sha256=_kdQ4BfNq6RijIeFEcDsKJliXDW_QHzfUhdhEE_I1V0,332
6
+ ifctrano/main.py,sha256=m8SpF6s7543d5tr3hkL9FXGAYnEblk7-xM8xygiwl3o,1605
7
+ ifctrano/space_boundary.py,sha256=4Mk6eEBcA6ard0YWf5uHnaHu1Ik75Ekzg106baAGzU4,7029
8
+ ifctrano-0.1.8.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
9
+ ifctrano-0.1.8.dist-info/METADATA,sha256=-wJb4lz-M2hjdzbTVrr-_Un9YHnqvIl3BiEe4IX5wGI,3000
10
+ ifctrano-0.1.8.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
11
+ ifctrano-0.1.8.dist-info/entry_points.txt,sha256=_2daDejazkphufyEu0m3lOeTio53WYmjol3KmSN0JM4,46
12
+ ifctrano-0.1.8.dist-info/RECORD,,
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ ifctrano=ifctrano.main:app
3
+
@@ -1,12 +0,0 @@
1
- ifctrano/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- ifctrano/base.py,sha256=vwWmzOTnMpE5rmnIti7QeG92Z_rpk-9nIrl7I9Or1cA,4971
3
- ifctrano/bounding_box.py,sha256=_cvS6G6RaNKCFIE3gaQO-bQTKOxIvn0xDZ4rKSCL5T4,8390
4
- ifctrano/building.py,sha256=0RCxlupyPl7c7d1F-JVL0mjDGpB7MPYVpoDuXFI0AeE,1940
5
- ifctrano/exceptions.py,sha256=_kntVIsqEvz3zxtJd8RougQRXEwjF5avarmGB-mig1w,228
6
- ifctrano/main.py,sha256=97Uh8JgjoeeeZ3VMP1_STpDH23mOlW2M9oo9gwECLPE,1411
7
- ifctrano/space_boundary.py,sha256=PkBiLPT26-Us7emyjqNUjKDL4GWMxfQQnHYOgRhelEI,5175
8
- ifctrano-0.1.7.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
9
- ifctrano-0.1.7.dist-info/METADATA,sha256=1kgy06CAiqdoqALp5VPMAO9qSULiB1ecI4Su6NFd9Dc,3000
10
- ifctrano-0.1.7.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
11
- ifctrano-0.1.7.dist-info/entry_points.txt,sha256=lHxD6pcOuPxITBoWjE35s4f6Dy7tzPa48ffuI-ehb2M,43
12
- ifctrano-0.1.7.dist-info/RECORD,,
@@ -1,3 +0,0 @@
1
- [console_scripts]
2
- trano=ifctrano.main:app
3
-