ifctrano 0.4.0__tar.gz → 0.7.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,8 +1,9 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: ifctrano
3
- Version: 0.4.0
3
+ Version: 0.7.0
4
4
  Summary: Package for generating building energy simulation model from IFC
5
5
  License: GPL V3
6
+ License-File: LICENSE
6
7
  Keywords: BIM,IFC,energy simulation,modelica,building energy simulation,buildings,ideas
7
8
  Author: Ando Andriamamonjy
8
9
  Author-email: andoludovic.andriamamonjy@gmail.com
@@ -15,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.12
15
16
  Requires-Dist: ifcopenshell (>=0.8.1.post1,<0.9.0)
16
17
  Requires-Dist: open3d (>=0.19.0,<0.20.0)
17
18
  Requires-Dist: shapely (>=2.0.7,<3.0.0)
18
- Requires-Dist: trano (>=0.6.0,<0.7.0)
19
+ Requires-Dist: trano (>=0.10.0,<0.11.0)
19
20
  Requires-Dist: typer (>=0.12.5,<0.13.0)
20
21
  Requires-Dist: vedo (>=2025.5.3,<2026.0.0)
21
22
  Project-URL: Repository, https://github.com/andoludo/ifctrano
@@ -398,6 +398,7 @@ class CommonSurface(BaseShow):
398
398
  main_vertices: FaceVertices
399
399
  common_vertices: FaceVertices
400
400
  exterior: bool = True
401
+ polygon: str
401
402
 
402
403
  def __hash__(self) -> int:
403
404
  return hash(
@@ -78,6 +78,7 @@ class ExtendCommonSurface(CommonSurface):
78
78
  orientation=self.orientation,
79
79
  main_vertices=self.main_vertices,
80
80
  common_vertices=self.common_vertices,
81
+ polygon=self.polygon,
81
82
  )
82
83
 
83
84
 
@@ -137,6 +138,7 @@ class OrientedBoundingBox(BaseShow):
137
138
  common_vertices=projected_face_1.common_vertices(
138
139
  intersection
139
140
  ),
141
+ polygon=intersection.wkt,
140
142
  )
141
143
  )
142
144
 
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  import re
2
3
  from pathlib import Path
3
4
  from typing import List, Tuple, Any, Optional, Set
@@ -12,7 +13,11 @@ from trano.topology import Network # type: ignore
12
13
  from vedo import Line # type: ignore
13
14
 
14
15
  from ifctrano.base import BaseModelConfig, Libraries, Vector, BaseShow, CommonSurface
15
- from ifctrano.exceptions import IfcFileNotFoundError, NoIfcSpaceFoundError
16
+ from ifctrano.exceptions import (
17
+ IfcFileNotFoundError,
18
+ NoIfcSpaceFoundError,
19
+ NoSpaceBoundariesError,
20
+ )
16
21
  from ifctrano.space_boundary import (
17
22
  SpaceBoundaries,
18
23
  initialize_tree,
@@ -20,6 +25,8 @@ from ifctrano.space_boundary import (
20
25
  )
21
26
  from ifctrano.construction import Constructions, default_construction
22
27
 
28
+ logger = logging.getLogger(__name__)
29
+
23
30
 
24
31
  def get_spaces(ifcopenshell_file: file) -> List[entity_instance]:
25
32
  return ifcopenshell_file.by_type("IfcSpace")
@@ -180,9 +187,18 @@ class Building(BaseShow):
180
187
  ]
181
188
  if not spaces:
182
189
  raise NoIfcSpaceFoundError("No IfcSpace found in the file.")
183
- space_boundaries = [
184
- SpaceBoundaries.from_space_entity(ifc_file, tree, space) for space in spaces
185
- ]
190
+ space_boundaries = []
191
+ for space in spaces:
192
+ try:
193
+ space_boundaries.append(
194
+ SpaceBoundaries.from_space_entity(ifc_file, tree, space)
195
+ )
196
+ except Exception as e: # noqa: PERF203
197
+ logger.error(f"Cannot process space {space.id()}. Reason {e}")
198
+ continue
199
+ if not space_boundaries:
200
+ raise NoSpaceBoundariesError("No valid space boundaries found.")
201
+
186
202
  return cls(
187
203
  space_boundaries=space_boundaries,
188
204
  ifc_file=ifc_file,
@@ -18,6 +18,10 @@ class NoIfcSpaceFoundError(Exception):
18
18
  pass
19
19
 
20
20
 
21
+ class NoSpaceBoundariesError(Exception):
22
+ pass
23
+
24
+
21
25
  class InvalidLibraryError(Exception):
22
26
  pass
23
27
 
@@ -5,7 +5,8 @@ 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, BaseModel
8
+ from pydantic import Field, BeforeValidator, BaseModel, ConfigDict
9
+ from shapely import wkt # type: ignore
9
10
  from trano.data_models.conversion import SpaceParameter # type: ignore
10
11
  from trano.elements import Space as TranoSpace, ExternalWall, Window, BaseWall, ExternalDoor # type: ignore
11
12
  from trano.elements.system import Occupancy # type: ignore
@@ -340,4 +341,109 @@ class SpaceBoundaries(BaseShow):
340
341
  )
341
342
  if space_boundary:
342
343
  space_boundaries.append(space_boundary)
343
- return cls(space=space_, boundaries=space_boundaries)
344
+ merged_boundaries = MergedSpaceBoundaries.from_boundaries(space_boundaries)
345
+ space_boundaries_ = merged_boundaries.merge_boundaries_from_part()
346
+ space_boundaries__ = remove_duplicate_boundaries(space_boundaries_)
347
+ return cls(space=space_, boundaries=space_boundaries__)
348
+
349
+
350
+ def remove_duplicate_boundaries(
351
+ boundaries: List[SpaceBoundary],
352
+ ) -> List[SpaceBoundary]:
353
+ types = ["IfcRoof", "IfcSlab"]
354
+ boundaries = sorted(boundaries, key=lambda b: b.entity.GlobalId)
355
+ boundaries_without_types = [
356
+ sp for sp in boundaries if sp.entity.is_a() not in types
357
+ ]
358
+ new_boundaries = []
359
+ for type_ in types:
360
+ references = [sp for sp in boundaries if sp.entity.is_a() == type_]
361
+ while True:
362
+ reference = next(iter(references), None)
363
+ if not reference:
364
+ break
365
+ others = [p_ for p_ in references if p_ != reference]
366
+ intersecting = [
367
+ o
368
+ for o in others
369
+ if (
370
+ wkt.loads(o.common_surface.polygon).intersects(
371
+ wkt.loads(reference.common_surface.polygon)
372
+ )
373
+ and o.common_surface.orientation
374
+ == reference.common_surface.orientation
375
+ )
376
+ and (
377
+ wkt.loads(o.common_surface.polygon).intersection(
378
+ wkt.loads(reference.common_surface.polygon)
379
+ )
380
+ ).area
381
+ > 0
382
+ ]
383
+ current_group = sorted(
384
+ [*intersecting, reference], key=lambda p: p.entity.GlobalId
385
+ )
386
+ new_boundaries.append(next(iter(current_group)))
387
+ references = [p_ for p_ in references if p_ not in current_group]
388
+ return [*boundaries_without_types, *new_boundaries]
389
+
390
+
391
+ class MergedSpaceBoundary(BaseModel):
392
+ model_config = ConfigDict(arbitrary_types_allowed=True)
393
+ parent: entity_instance
394
+ related_boundaries: List[SpaceBoundary]
395
+
396
+ def get_new_boundary(self) -> Optional[SpaceBoundary]:
397
+ related_boundaries = sorted(
398
+ self.related_boundaries, key=lambda b: b.entity.GlobalId
399
+ )
400
+ boundary = next(iter(related_boundaries), None)
401
+ if boundary:
402
+ return SpaceBoundary.model_validate(
403
+ boundary.model_dump() | {"entity": self.parent}
404
+ )
405
+ return None
406
+
407
+
408
+ class MergedSpaceBoundaries(BaseModel):
409
+ part_boundaries: List[MergedSpaceBoundary]
410
+ original_boundaries: List[SpaceBoundary]
411
+
412
+ @classmethod
413
+ def from_boundaries(
414
+ cls, space_boundaries: List[SpaceBoundary]
415
+ ) -> "MergedSpaceBoundaries":
416
+ building_element_part_boundaries = [
417
+ boundary
418
+ for boundary in space_boundaries
419
+ if boundary.entity.is_a() in ["IfcBuildingElementPart"]
420
+ ]
421
+ existing_parent_entities = {
422
+ decompose.RelatingObject
423
+ for b in building_element_part_boundaries
424
+ for decompose in b.entity.Decomposes
425
+ }
426
+ part_boundaries = [
427
+ MergedSpaceBoundary(
428
+ parent=parent,
429
+ related_boundaries=[
430
+ b
431
+ for b in building_element_part_boundaries
432
+ for decompose in b.entity.Decomposes
433
+ if decompose.RelatingObject == parent
434
+ ],
435
+ )
436
+ for parent in existing_parent_entities
437
+ ]
438
+ return cls(
439
+ part_boundaries=part_boundaries, original_boundaries=space_boundaries
440
+ )
441
+
442
+ def merge_boundaries_from_part(self) -> List[SpaceBoundary]:
443
+ new_boundaries = [b.get_new_boundary() for b in self.part_boundaries]
444
+ new_boundaries_ = [nb for nb in new_boundaries if nb is not None]
445
+ return [
446
+ b
447
+ for b in self.original_boundaries
448
+ if b.entity.is_a() not in ["IfcBuildingElementPart"]
449
+ ] + new_boundaries_
@@ -0,0 +1,11 @@
1
+ from typing import Literal
2
+
3
+ BuildingElements = Literal[
4
+ "IfcWall",
5
+ "IfcSlab",
6
+ "IfcRoof",
7
+ "IfcDoor",
8
+ "IfcWindow",
9
+ "IfcPlate",
10
+ "IfcBuildingElementPart",
11
+ ]
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "ifctrano"
3
- version = "0.4.0"
3
+ version = "0.7.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.6.0"
14
+ trano = "^0.10.0"
15
15
  shapely = "^2.0.7"
16
16
  typer = "^0.12.5"
17
17
  vedo = "^2025.5.3"
@@ -1,5 +0,0 @@
1
- from typing import Literal
2
-
3
- BuildingElements = Literal[
4
- "IfcWall", "IfcSlab", "IfcRoof", "IfcDoor", "IfcWindow", "IfcPlate"
5
- ]
File without changes
File without changes
File without changes
File without changes
File without changes