ifctrano 0.1.11__py3-none-any.whl → 0.2.0__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/building.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import re
2
2
  from pathlib import Path
3
- from typing import List, Tuple, Any, Optional
3
+ from typing import List, Tuple, Any, Optional, Set
4
4
 
5
5
  import ifcopenshell
6
6
  from ifcopenshell import file, entity_instance
@@ -9,15 +9,16 @@ from trano.elements import InternalElement # type: ignore
9
9
  from trano.elements.library.library import Library # type: ignore
10
10
  from trano.elements.types import Tilt # type: ignore
11
11
  from trano.topology import Network # type: ignore
12
+ from vedo import Line # type: ignore
12
13
 
13
- from ifctrano.base import BaseModelConfig, Libraries, Vector
14
+ from ifctrano.base import BaseModelConfig, Libraries, Vector, BaseShow, CommonSurface
14
15
  from ifctrano.exceptions import IfcFileNotFoundError, NoIfcSpaceFoundError
15
16
  from ifctrano.space_boundary import (
16
17
  SpaceBoundaries,
17
18
  initialize_tree,
18
19
  Space,
19
- construction,
20
20
  )
21
+ from ifctrano.construction import Constructions, default_construction
21
22
 
22
23
 
23
24
  def get_spaces(ifcopenshell_file: file) -> List[entity_instance]:
@@ -28,6 +29,7 @@ class IfcInternalElement(BaseModelConfig):
28
29
  spaces: List[Space]
29
30
  element: entity_instance
30
31
  area: float
32
+ common_surface: CommonSurface
31
33
 
32
34
  def __hash__(self) -> int:
33
35
  return hash(
@@ -49,23 +51,108 @@ class IfcInternalElement(BaseModelConfig):
49
51
  self.area,
50
52
  )
51
53
 
54
+ def lines(self) -> List[Line]:
55
+ lines = []
56
+ if self.common_surface:
57
+ lines += self.common_surface.lines()
58
+ return lines
52
59
 
53
- class InternalElements(BaseModelConfig):
60
+
61
+ class InternalElements(BaseShow):
54
62
  elements: List[IfcInternalElement] = Field(default_factory=list)
55
63
 
56
64
  def internal_element_ids(self) -> List[str]:
57
65
  return list({e.element.GlobalId for e in self.elements})
58
66
 
59
- def description(self) -> List[Tuple[Any, Any, str, float]]:
60
- return sorted([element.description() for element in self.elements])
67
+ def description(self) -> Set[Tuple[Any, Any, str, float]]:
68
+ return set( # noqa: C414
69
+ sorted([element.description() for element in self.elements])
70
+ )
71
+
72
+
73
+ def get_internal_elements(space1_boundaries: List[SpaceBoundaries]) -> InternalElements:
74
+ elements = []
75
+ seen = set()
76
+ common_boundaries = []
77
+ for space_boundaries_ in space1_boundaries:
78
+ for space_boundaries__ in space1_boundaries:
79
+ space_1 = space_boundaries_.space
80
+ space_2 = space_boundaries__.space
81
+
82
+ if (
83
+ space_1.global_id == space_2.global_id
84
+ and (space_1.global_id, space_2.global_id) in seen
85
+ ):
86
+ continue
87
+ seen.update(
88
+ {
89
+ (space_1.global_id, space_2.global_id),
90
+ (space_2.global_id, space_1.global_id),
91
+ }
92
+ )
93
+ common_surface = space_1.bounding_box.intersect_faces(space_2.bounding_box)
94
+
95
+ for boundary in space_boundaries_.boundaries:
96
+ for boundary_ in space_boundaries__.boundaries:
97
+ if (
98
+ boundary.entity.GlobalId == boundary_.entity.GlobalId
99
+ and boundary.common_surface
100
+ and boundary_.common_surface
101
+ and common_surface
102
+ and (
103
+ boundary.common_surface.orientation
104
+ * common_surface.orientation
105
+ ).is_a_zero()
106
+ and (
107
+ boundary_.common_surface.orientation
108
+ * common_surface.orientation
109
+ ).is_a_zero()
110
+ ) and boundary.common_surface.orientation.dot(
111
+ boundary_.common_surface.orientation
112
+ ) < 0:
113
+ common_boundaries.extend([boundary, boundary_])
114
+ common_surface = sorted(
115
+ [boundary.common_surface, boundary_.common_surface],
116
+ key=lambda s: s.area,
117
+ )[0]
118
+ common_surface.exterior = False
119
+ elements.append(
120
+ IfcInternalElement(
121
+ spaces=[space_1, space_2],
122
+ element=boundary_.entity,
123
+ area=common_surface.area,
124
+ common_surface=common_surface,
125
+ )
126
+ )
127
+ for space_boundaries_ in space1_boundaries:
128
+ space_boundaries_.remove(common_boundaries)
129
+ return InternalElements(elements=list(set(elements)))
61
130
 
62
131
 
63
- class Building(BaseModelConfig):
132
+ class Building(BaseShow):
64
133
  name: str
65
134
  space_boundaries: List[SpaceBoundaries]
66
135
  ifc_file: file
67
136
  parent_folder: Path
68
137
  internal_elements: InternalElements = Field(default_factory=InternalElements)
138
+ constructions: Constructions
139
+
140
+ def get_boundaries(self, space_id: str) -> SpaceBoundaries:
141
+ return next(
142
+ sb for sb in self.space_boundaries if sb.space.global_id == space_id
143
+ )
144
+
145
+ def description(self) -> list[list[tuple[float, tuple[float, ...], Any, str]]]:
146
+ return sorted([sorted(b.description()) for b in self.space_boundaries])
147
+
148
+ def lines(self) -> List[Line]:
149
+ lines = []
150
+ for space_boundaries_ in [
151
+ *self.space_boundaries,
152
+ *self.internal_elements.elements,
153
+ ]:
154
+ lines += space_boundaries_.lines() # type: ignore
155
+ return lines
69
156
 
70
157
  @field_validator("name")
71
158
  @classmethod
@@ -86,6 +173,7 @@ class Building(BaseModelConfig):
86
173
  ifc_file = ifcopenshell.open(str(ifc_file_path))
87
174
  tree = initialize_tree(ifc_file)
88
175
  spaces = get_spaces(ifc_file)
176
+ constructions = Constructions.from_ifc(ifc_file)
89
177
  if selected_spaces_global_id:
90
178
  spaces = [
91
179
  space for space in spaces if space.GlobalId in selected_spaces_global_id
@@ -100,6 +188,7 @@ class Building(BaseModelConfig):
100
188
  ifc_file=ifc_file,
101
189
  parent_folder=ifc_file_path.parent,
102
190
  name=ifc_file_path.stem,
191
+ constructions=constructions,
103
192
  )
104
193
 
105
194
  @model_validator(mode="after")
@@ -108,58 +197,21 @@ class Building(BaseModelConfig):
108
197
  return self
109
198
 
110
199
  def get_adjacency(self) -> InternalElements:
111
- elements = []
112
- for space_boundaries_ in self.space_boundaries:
113
- for space_boundaries__ in self.space_boundaries:
114
- space_1 = space_boundaries_.space
115
- space_2 = space_boundaries__.space
116
- if space_1.global_id == space_2.global_id:
117
- continue
118
- common_surface = space_1.bounding_box.intersect_faces(
119
- space_2.bounding_box
120
- )
121
- for boundary in space_boundaries_.boundaries:
122
- for boundary_ in space_boundaries__.boundaries:
123
- if (
124
- boundary.entity.GlobalId == boundary_.entity.GlobalId
125
- and boundary.common_surface
126
- and boundary_.common_surface
127
- and common_surface
128
- and (
129
- boundary.common_surface.orientation
130
- * common_surface.orientation
131
- ).is_a_zero()
132
- and (
133
- boundary_.common_surface.orientation
134
- * common_surface.orientation
135
- ).is_a_zero()
136
- ) and boundary.common_surface.orientation.dot(
137
- boundary_.common_surface.orientation
138
- ) < 0:
139
- elements.append( # noqa: PERF401
140
- IfcInternalElement(
141
- spaces=[space_1, space_2],
142
- element=boundary_.entity,
143
- area=min(
144
- common_surface.area,
145
- boundary.common_surface.area,
146
- boundary_.common_surface.area,
147
- ),
148
- )
149
- )
150
- return InternalElements(elements=list(set(elements)))
200
+ return get_internal_elements(self.space_boundaries)
151
201
 
152
202
  @validate_call
153
203
  def create_model(
154
204
  self,
155
205
  library: Libraries = "Buildings",
156
206
  north_axis: Optional[Vector] = None,
157
- ) -> str:
207
+ ) -> Network:
158
208
  north_axis = north_axis or Vector(x=0, y=1, z=0)
159
209
  network = Network(name=self.name, library=Library.from_configuration(library))
160
210
  spaces = {
161
211
  space_boundary.space.global_id: space_boundary.model(
162
- self.internal_elements.internal_element_ids(), north_axis
212
+ self.internal_elements.internal_element_ids(),
213
+ north_axis,
214
+ self.constructions,
163
215
  )
164
216
  for space_boundary in self.space_boundaries
165
217
  }
@@ -173,13 +225,13 @@ class Building(BaseModelConfig):
173
225
  spaces[space_2.global_id],
174
226
  InternalElement(
175
227
  azimuth=10,
176
- construction=construction,
228
+ construction=default_construction,
177
229
  surface=internal_element.area,
178
230
  tilt=Tilt.wall,
179
231
  ),
180
232
  )
181
- return network.model() # type: ignore
233
+ return network
182
234
 
183
235
  def save_model(self, library: Libraries = "Buildings") -> None:
184
236
  model_ = self.create_model(library)
185
- Path(self.parent_folder.joinpath(f"{self.name}.mo")).write_text(model_)
237
+ Path(self.parent_folder.joinpath(f"{self.name}.mo")).write_text(model_.model())
@@ -0,0 +1,227 @@
1
+ import logging
2
+ from typing import List, Optional
3
+
4
+ from ifcopenshell import file, entity_instance
5
+
6
+ from pydantic import BaseModel
7
+ from trano.elements.construction import ( # type: ignore
8
+ Material,
9
+ Layer,
10
+ Construction,
11
+ GlassMaterial,
12
+ Gas,
13
+ Glass,
14
+ GlassLayer,
15
+ GasLayer,
16
+ )
17
+ from ifctrano.utils import remove_non_alphanumeric, generate_alphanumeric_uuid
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ DEFAULT_MATERIAL = {
23
+ "thermal_conductivity": 0.046,
24
+ "specific_heat_capacity": 940,
25
+ "density": 80,
26
+ }
27
+
28
+
29
+ material_1 = Material(
30
+ name="material_1",
31
+ thermal_conductivity=0.046,
32
+ specific_heat_capacity=940,
33
+ density=80,
34
+ )
35
+ default_construction = Construction(
36
+ name="default_construction",
37
+ layers=[
38
+ Layer(material=material_1, thickness=0.18),
39
+ ],
40
+ )
41
+ id_100 = GlassMaterial(
42
+ name="id_100",
43
+ thermal_conductivity=1,
44
+ density=2500,
45
+ specific_heat_capacity=840,
46
+ solar_transmittance=[0.646],
47
+ solar_reflectance_outside_facing=[0.062],
48
+ solar_reflectance_room_facing=[0.063],
49
+ infrared_transmissivity=0,
50
+ infrared_absorptivity_outside_facing=0.84,
51
+ infrared_absorptivity_room_facing=0.84,
52
+ )
53
+ air = Gas(
54
+ name="Air",
55
+ thermal_conductivity=0.025,
56
+ density=1.2,
57
+ specific_heat_capacity=1005,
58
+ )
59
+ glass = Glass(
60
+ name="double_glazing",
61
+ u_value_frame=1.4,
62
+ layers=[
63
+ GlassLayer(thickness=0.003, material=id_100),
64
+ GasLayer(thickness=0.0127, material=air),
65
+ GlassLayer(thickness=0.003, material=id_100),
66
+ ],
67
+ )
68
+
69
+
70
+ class MaterialId(Material): # type: ignore
71
+ id: int
72
+
73
+ def to_material(self) -> Material:
74
+ return Material.model_validate(self.model_dump(exclude={"id"}))
75
+
76
+
77
+ class LayerId(Layer): # type: ignore
78
+ id: int
79
+
80
+ def to_layer(self) -> Layer:
81
+ return Layer.model_validate(self.model_dump(exclude={"id"}))
82
+
83
+
84
+ class ConstructionId(Construction): # type: ignore
85
+ id: int
86
+
87
+ def to_construction(self) -> Construction:
88
+ return Construction.model_validate(self.model_dump(exclude={"id"}))
89
+
90
+
91
+ class Materials(BaseModel):
92
+ materials: List[MaterialId]
93
+
94
+ @classmethod
95
+ def from_ifc(cls, ifc_file: file) -> "Materials":
96
+ materials = ifc_file.by_type("IfcMaterial")
97
+ return cls.from_ifc_materials(materials)
98
+
99
+ @classmethod
100
+ def from_ifc_materials(cls, ifc_materials: List[entity_instance]) -> "Materials":
101
+ materials = []
102
+ for material in ifc_materials:
103
+ material_name = remove_non_alphanumeric(material.Name)
104
+ materials.append(
105
+ MaterialId.model_validate(
106
+ {"name": material_name, "id": material.id(), **DEFAULT_MATERIAL}
107
+ )
108
+ )
109
+ return cls(materials=materials)
110
+
111
+ def get_material(self, id: int) -> Material:
112
+ for material in self.materials:
113
+ if material.id == id:
114
+ return material.to_material()
115
+ raise ValueError(f"Material {id} not found in materials list.")
116
+
117
+
118
+ def _get_unit_factor(ifc_file: file) -> float:
119
+ length_unit = next(
120
+ unit for unit in ifc_file.by_type("IfcSiUnit") if unit.UnitType == "LENGTHUNIT"
121
+ )
122
+ if length_unit.Prefix == "MILLI" and length_unit.Name == "METRE":
123
+ return 0.001
124
+ return 1
125
+
126
+
127
+ class Layers(BaseModel):
128
+ layers: List[LayerId]
129
+
130
+ @classmethod
131
+ def from_ifc(cls, ifc_file: file, materials: Materials) -> "Layers":
132
+ material_layers = ifc_file.by_type("IfcMaterialLayer")
133
+ unit_factor = _get_unit_factor(ifc_file)
134
+ return cls.from_ifc_material_layers(
135
+ material_layers, materials, unit_factor=unit_factor
136
+ )
137
+
138
+ @classmethod
139
+ def from_ifc_material_layers(
140
+ cls,
141
+ ifc_material_layers: List[entity_instance],
142
+ materials: Materials,
143
+ unit_factor: float = 1,
144
+ ) -> "Layers":
145
+ layers = []
146
+ for layer in ifc_material_layers:
147
+ thickness = layer.LayerThickness * unit_factor
148
+ layers.append(
149
+ LayerId(
150
+ id=layer.id(),
151
+ thickness=thickness,
152
+ material=materials.get_material(layer.Material.id()),
153
+ )
154
+ )
155
+ return cls(layers=layers)
156
+
157
+ def from_ids(self, ids: List[int]) -> List[Layer]:
158
+ return [layer.to_layer() for layer in self.layers if layer.id in ids]
159
+
160
+
161
+ class Constructions(BaseModel):
162
+ constructions: List[ConstructionId]
163
+
164
+ @classmethod
165
+ def from_ifc(cls, ifc_file: file) -> "Constructions":
166
+ materials = Materials.from_ifc(ifc_file)
167
+ layers = Layers.from_ifc(ifc_file, materials)
168
+ material_layers_sets = ifc_file.by_type("IfcMaterialLayerSet")
169
+ return cls.from_ifc_material_layer_sets(material_layers_sets, layers)
170
+
171
+ @classmethod
172
+ def from_ifc_material_layer_sets(
173
+ cls, ifc_material_layer_sets: List[entity_instance], layers: Layers
174
+ ) -> "Constructions":
175
+ constructions = []
176
+ for layer_set in ifc_material_layer_sets:
177
+ name_ = layer_set.LayerSetName or generate_alphanumeric_uuid()
178
+ name = remove_non_alphanumeric(name_)
179
+ layer_ids = [
180
+ int(material_layer.id()) for material_layer in layer_set.MaterialLayers
181
+ ]
182
+ constructions.append(
183
+ ConstructionId(
184
+ id=layer_set.id(), name=name, layers=layers.from_ids(layer_ids)
185
+ )
186
+ )
187
+ return cls(constructions=constructions)
188
+
189
+ def get_construction(self, entity: entity_instance) -> Construction:
190
+ construction_id = self._get_construction_id(entity)
191
+ if construction_id is None:
192
+ logger.error(
193
+ f"Construction ID not found for {entity.GlobalId} ({entity.is_a()}) "
194
+ f"({entity.Name}). Using default construction."
195
+ )
196
+ return default_construction
197
+ constructions = [
198
+ construction.to_construction()
199
+ for construction in self.constructions
200
+ if construction.id == construction_id
201
+ ]
202
+ if not constructions:
203
+ raise ValueError(f"No construction found for {entity.GlobalId}")
204
+ return constructions[0]
205
+
206
+ def _get_construction_id(self, entity: entity_instance) -> Optional[int]:
207
+ associates_materials = [
208
+ association
209
+ for association in entity.HasAssociations
210
+ if association.is_a() == "IfcRelAssociatesMaterial"
211
+ ]
212
+ if not associates_materials:
213
+ logger.error(f"Associate materials not found for {entity.GlobalId}.")
214
+ return None
215
+ relating_material = associates_materials[0].RelatingMaterial
216
+ if relating_material.is_a() == "IfcMaterialList":
217
+ logger.error(
218
+ f"Material list found for {entity.GlobalId}, but no construction ID available."
219
+ )
220
+ return None
221
+ elif relating_material.is_a() == "IfcMaterialLayerSetUsage":
222
+ return int(associates_materials[0].RelatingMaterial.ForLayerSet.id())
223
+ elif relating_material.is_a() == "IfcMaterialLayerSet":
224
+ return int(relating_material.id())
225
+ else:
226
+ logger.error("Unexpected material type found.")
227
+ return None