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/__init__.py +3 -0
- ifctrano/base.py +256 -12
- ifctrano/bounding_box.py +97 -168
- ifctrano/building.py +104 -52
- ifctrano/construction.py +227 -0
- ifctrano/example/verification.ifc +3 -3043
- ifctrano/main.py +55 -2
- ifctrano/space_boundary.py +100 -98
- ifctrano/types.py +5 -0
- ifctrano/utils.py +29 -0
- {ifctrano-0.1.11.dist-info → ifctrano-0.2.0.dist-info}/METADATA +20 -5
- ifctrano-0.2.0.dist-info/RECORD +16 -0
- {ifctrano-0.1.11.dist-info → ifctrano-0.2.0.dist-info}/WHEEL +1 -1
- ifctrano-0.1.11.dist-info/RECORD +0 -13
- {ifctrano-0.1.11.dist-info → ifctrano-0.2.0.dist-info}/LICENSE +0 -0
- {ifctrano-0.1.11.dist-info → ifctrano-0.2.0.dist-info}/entry_points.txt +0 -0
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
|
-
|
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) ->
|
60
|
-
return
|
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(
|
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
|
-
|
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
|
-
) ->
|
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(),
|
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=
|
228
|
+
construction=default_construction,
|
177
229
|
surface=internal_element.area,
|
178
230
|
tilt=Tilt.wall,
|
179
231
|
),
|
180
232
|
)
|
181
|
-
return network
|
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())
|
ifctrano/construction.py
ADDED
@@ -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
|