ifctrano 0.8.0__tar.gz → 0.10.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ifctrano
3
- Version: 0.8.0
3
+ Version: 0.10.0
4
4
  Summary: Package for generating building energy simulation model from IFC
5
5
  License: GPL V3
6
6
  License-File: LICENSE
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.12
16
16
  Requires-Dist: ifcopenshell (>=0.8.1.post1,<0.9.0)
17
17
  Requires-Dist: open3d (>=0.19.0,<0.20.0)
18
18
  Requires-Dist: shapely (>=2.0.7,<3.0.0)
19
- Requires-Dist: trano (>=0.10.0,<0.11.0)
19
+ Requires-Dist: trano (>=0.12.0,<0.13.0)
20
20
  Requires-Dist: typer (>=0.12.5,<0.13.0)
21
21
  Requires-Dist: vedo (>=2025.5.3,<2026.0.0)
22
22
  Project-URL: Repository, https://github.com/andoludo/ifctrano
@@ -8,6 +8,7 @@ import yaml
8
8
  from ifcopenshell import file, entity_instance
9
9
  from pydantic import validate_call, Field, model_validator, field_validator
10
10
  from trano.elements import InternalElement # type: ignore
11
+ from trano.elements.envelope import SpaceTilt # type: ignore
11
12
  from trano.elements.library.library import Library # type: ignore
12
13
  from trano.elements.types import Tilt # type: ignore
13
14
  from trano.topology import Network # type: ignore
@@ -234,10 +235,21 @@ class Building(BaseShow):
234
235
  )
235
236
  for space_boundary in self.space_boundaries
236
237
  ]
238
+ spaces = [sp for sp in spaces if sp is not None]
237
239
  internal_walls = []
240
+
238
241
  for internal_element in self.internal_elements.elements:
239
242
  space_1 = internal_element.spaces[0]
240
243
  space_2 = internal_element.spaces[1]
244
+ if any(
245
+ name not in [sp["id"] for sp in spaces] # type: ignore
246
+ for name in [
247
+ space_1.space_unique_name(),
248
+ space_2.space_unique_name(),
249
+ ]
250
+ ):
251
+ continue
252
+
241
253
  construction = self.constructions.get_construction(
242
254
  internal_element.element, default_internal_construction
243
255
  )
@@ -253,7 +265,8 @@ class Building(BaseShow):
253
265
  else Tilt.ceiling.value
254
266
  )
255
267
  if space_1_tilt == space_2_tilt:
256
- raise ValueError("Space tilts are not compatible.")
268
+ logger.error("Space tilts are not compatible.")
269
+ continue
257
270
  internal_walls.append(
258
271
  {
259
272
  "space_1": space_1.space_unique_name(),
@@ -306,6 +319,30 @@ class Building(BaseShow):
306
319
  for internal_element in self.internal_elements.elements:
307
320
  space_1 = internal_element.spaces[0]
308
321
  space_2 = internal_element.spaces[1]
322
+ if any(
323
+ global_id not in spaces
324
+ for global_id in [space_1.entity.GlobalId, space_2.entity.GlobalId]
325
+ ):
326
+ continue
327
+ space_tilts = []
328
+ if internal_element.element.is_a() in ["IfcSlab"]:
329
+ space_1_tilt = (
330
+ Tilt.floor
331
+ if space_1.bounding_box.centroid.z > space_2.bounding_box.centroid.z
332
+ else Tilt.ceiling
333
+ )
334
+ space_2_tilt = (
335
+ Tilt.floor
336
+ if space_2.bounding_box.centroid.z > space_1.bounding_box.centroid.z
337
+ else Tilt.ceiling
338
+ )
339
+ if space_1_tilt == space_2_tilt:
340
+ logger.error("Space tilts are not compatible.")
341
+ continue
342
+ space_tilts = [
343
+ SpaceTilt(space_name=space_1.name, tilt=space_1_tilt),
344
+ SpaceTilt(space_name=space_2.name, tilt=space_2_tilt),
345
+ ]
309
346
  network.connect_spaces(
310
347
  spaces[space_1.global_id],
311
348
  spaces[space_2.global_id],
@@ -314,6 +351,7 @@ class Building(BaseShow):
314
351
  construction=default_construction,
315
352
  surface=internal_element.area,
316
353
  tilt=Tilt.wall,
354
+ space_tilts=space_tilts,
317
355
  ),
318
356
  )
319
357
  return network
@@ -235,7 +235,12 @@ class Constructions(BaseModel):
235
235
  return None
236
236
 
237
237
  def to_config(self) -> Dict[str, Any]:
238
- constructions_all = [*self.constructions, default_construction, glass]
238
+ constructions_all = [
239
+ *self.constructions,
240
+ default_construction,
241
+ glass,
242
+ default_internal_construction,
243
+ ]
239
244
  constructions = [
240
245
  {
241
246
  "id": construction.name,
@@ -304,8 +309,7 @@ class Constructions(BaseModel):
304
309
 
305
310
  def _convert_glass(glass_: Material) -> Dict[str, Any]:
306
311
  return {
307
- key: (
308
- value if not isinstance(value, list) else f"{{{','.join(map(str, value))}}}"
309
- )
312
+ key: (value if not isinstance(value, list) else value)
310
313
  for key, value in glass_.model_dump().items()
314
+ if key not in ["name"]
311
315
  }
@@ -25,8 +25,7 @@ from ifctrano.base import (
25
25
  BaseShow,
26
26
  )
27
27
  from ifctrano.bounding_box import OrientedBoundingBox
28
- from ifctrano.construction import glass, Constructions
29
- from ifctrano.exceptions import HasWindowsWithoutWallsError
28
+ from ifctrano.construction import glass, Constructions, default_construction
30
29
  from ifctrano.utils import (
31
30
  remove_non_alphanumeric,
32
31
  _round,
@@ -121,11 +120,57 @@ class ExternalSpaceBoundaryGroup(BaseModelConfig):
121
120
  for construction in self.constructions
122
121
  )
123
122
 
123
+ def _merge(
124
+ self,
125
+ constructions: List[Window | ExternalWall],
126
+ construction_type: type[Window | ExternalWall],
127
+ ) -> List[Window | ExternalWall]:
128
+ construction_types = [
129
+ c for c in self.constructions if isinstance(c, construction_type)
130
+ ]
131
+ reference = next(iter(construction_types), None)
132
+ if not reference:
133
+ return []
134
+ surface = sum([construction.surface for construction in construction_types])
135
+ azimuth = reference.azimuth
136
+ tilt = reference.tilt
137
+ return [
138
+ construction_type(
139
+ surface=surface,
140
+ azimuth=azimuth,
141
+ tilt=tilt,
142
+ construction=reference.construction,
143
+ )
144
+ ]
145
+
146
+ def merge(self) -> None:
147
+ self.constructions = [
148
+ *self._merge(self.constructions, Window),
149
+ *self._merge(self.constructions, ExternalWall),
150
+ ]
151
+
152
+ def add_external_wall(self) -> None:
153
+ reference = next(iter(self.constructions))
154
+ surface = sum([construction.surface for construction in self.constructions]) * (
155
+ 0.7 / 0.3
156
+ )
157
+ azimuth = reference.azimuth
158
+ tilt = reference.tilt
159
+ self.constructions.append(
160
+ ExternalWall(
161
+ surface=surface,
162
+ azimuth=azimuth,
163
+ tilt=tilt,
164
+ construction=default_construction,
165
+ )
166
+ )
167
+
124
168
 
125
169
  class ExternalSpaceBoundaryGroups(BaseModelConfig):
126
170
  space_boundary_groups: List[ExternalSpaceBoundaryGroup] = Field(
127
171
  default_factory=list
128
172
  )
173
+ remaining_constructions: List[BaseWall]
129
174
 
130
175
  @classmethod
131
176
  def from_external_boundaries(
@@ -136,6 +181,11 @@ class ExternalSpaceBoundaryGroups(BaseModelConfig):
136
181
  for ex in external_boundaries
137
182
  if isinstance(ex, (ExternalWall, Window)) and ex.tilt == Tilt.wall
138
183
  ]
184
+ remaining_constructions = [
185
+ ex
186
+ for ex in external_boundaries
187
+ if not (isinstance(ex, (ExternalWall, Window)) and ex.tilt == Tilt.wall)
188
+ ]
139
189
  space_boundary_groups = list(
140
190
  {
141
191
  ExternalSpaceBoundaryGroup(
@@ -150,7 +200,12 @@ class ExternalSpaceBoundaryGroups(BaseModelConfig):
150
200
  for ex in boundary_walls
151
201
  }
152
202
  )
153
- return cls(space_boundary_groups=space_boundary_groups)
203
+ groups = cls(
204
+ space_boundary_groups=space_boundary_groups,
205
+ remaining_constructions=remaining_constructions,
206
+ )
207
+ groups.merge()
208
+ return groups
154
209
 
155
210
  def has_windows_without_wall(self) -> bool:
156
211
  return all(
@@ -158,9 +213,23 @@ class ExternalSpaceBoundaryGroups(BaseModelConfig):
158
213
  for group in self.space_boundary_groups
159
214
  )
160
215
 
216
+ def add_external_walls(self) -> None:
217
+ for group in self.space_boundary_groups:
218
+ group.add_external_wall()
219
+
220
+ def get_constructions(self) -> List[ExternalWall | Window]:
221
+ return [
222
+ *[c for group in self.space_boundary_groups for c in group.constructions],
223
+ *self.remaining_constructions,
224
+ ]
225
+
226
+ def merge(self) -> None:
227
+ for g in self.space_boundary_groups:
228
+ g.merge()
229
+
161
230
 
162
231
  def deg_to_rad(deg: float) -> float:
163
- return deg * math.pi / 180.0
232
+ return round(deg * math.pi / 180.0, 2)
164
233
 
165
234
 
166
235
  class Azimuths(BaseModel):
@@ -299,7 +368,7 @@ class SpaceBoundaries(BaseShow):
299
368
  exclude_entities: List[str],
300
369
  north_axis: Vector,
301
370
  constructions: Constructions,
302
- ) -> Dict[str, Any]:
371
+ ) -> Optional[Dict[str, Any]]:
303
372
  external_boundaries: Dict[str, Any] = {
304
373
  "external_walls": [],
305
374
  "floor_on_grounds": [],
@@ -312,38 +381,45 @@ class SpaceBoundaries(BaseShow):
312
381
  )
313
382
  if boundary_model:
314
383
  external_boundaries_check.append(boundary_model)
315
- element = {
316
- "surface": boundary_model.surface,
317
- "azimuth": deg_to_rad(boundary_model.azimuth),
318
- "tilt": boundary_model.tilt.value,
319
- "construction": boundary_model.construction.name,
320
- }
321
- if isinstance(
322
- boundary_model, (ExternalWall, ExternalDoor)
323
- ) and boundary_model.tilt in [Tilt.wall, Tilt.ceiling]:
324
- external_boundaries["external_walls"].append(element)
325
- elif isinstance(boundary_model, (Window)):
326
- external_boundaries["windows"].append(element)
327
- elif isinstance(
328
- boundary_model, (ExternalWall)
329
- ) and boundary_model.tilt in [Tilt.floor]:
330
- external_boundaries["floor_on_grounds"].append(element)
331
- else:
332
- raise ValueError("Unknown boundary type")
384
+ if not external_boundaries_check:
385
+ return None
333
386
  external_space_boundaries_group = (
334
387
  ExternalSpaceBoundaryGroups.from_external_boundaries(
335
388
  external_boundaries_check
336
389
  )
337
390
  )
338
391
  if not external_space_boundaries_group.has_windows_without_wall():
392
+ external_space_boundaries_group.add_external_walls()
339
393
  logger.error(
340
394
  f"Space {self.space.global_id} has a boundary that has a windows but without walls."
341
395
  )
396
+ for boundary_model in external_space_boundaries_group.get_constructions():
397
+ element = {
398
+ "surface": boundary_model.surface,
399
+ "azimuth": deg_to_rad(boundary_model.azimuth),
400
+ "tilt": boundary_model.tilt.value,
401
+ "construction": boundary_model.construction.name,
402
+ }
403
+ if isinstance(
404
+ boundary_model, (ExternalWall, ExternalDoor)
405
+ ) and boundary_model.tilt in [Tilt.wall, Tilt.ceiling]:
406
+ external_boundaries["external_walls"].append(element)
407
+ elif isinstance(boundary_model, (Window)):
408
+ external_boundaries["windows"].append(element)
409
+ elif isinstance(boundary_model, (ExternalWall)) and boundary_model.tilt in [
410
+ Tilt.floor
411
+ ]:
412
+ element.pop("azimuth")
413
+ element.pop("tilt")
414
+ external_boundaries["floor_on_grounds"].append(element)
415
+ else:
416
+ raise ValueError("Unknown boundary type")
417
+
342
418
  occupancy_parameters = Occupancy().parameters.model_dump(mode="json")
343
419
  space_parameters = SpaceParameter(
344
420
  floor_area=self.space.floor_area,
345
421
  average_room_height=self.space.average_room_height,
346
- ).model_dump(mode="json")
422
+ ).model_dump(mode="json", exclude={"linearize_emissive_power", "volume"})
347
423
  return {
348
424
  "id": self.space.space_unique_name(),
349
425
  "occupancy": {"parameters": occupancy_parameters},
@@ -364,12 +440,15 @@ class SpaceBoundaries(BaseShow):
364
440
  )
365
441
  if boundary_model:
366
442
  external_boundaries.append(boundary_model)
443
+ if not external_boundaries:
444
+ return None
367
445
 
368
446
  external_space_boundaries_group = (
369
447
  ExternalSpaceBoundaryGroups.from_external_boundaries(external_boundaries)
370
448
  )
371
449
  if not external_space_boundaries_group.has_windows_without_wall():
372
- raise HasWindowsWithoutWallsError(
450
+ external_space_boundaries_group.add_external_walls()
451
+ logger.error(
373
452
  f"Space {self.space.global_id} has a boundary that has a windows but without walls."
374
453
  )
375
454
  return TranoSpace(
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "ifctrano"
3
- version = "0.8.0"
3
+ version = "0.10.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.10.0"
14
+ trano = "^0.12.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