ifctrano 0.7.0__tar.gz → 0.8.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.
- {ifctrano-0.7.0 → ifctrano-0.8.0}/PKG-INFO +1 -1
- {ifctrano-0.7.0 → ifctrano-0.8.0}/ifctrano/building.py +72 -2
- {ifctrano-0.7.0 → ifctrano-0.8.0}/ifctrano/construction.py +87 -3
- {ifctrano-0.7.0 → ifctrano-0.8.0}/ifctrano/main.py +32 -0
- {ifctrano-0.7.0 → ifctrano-0.8.0}/ifctrano/space_boundary.py +74 -1
- {ifctrano-0.7.0 → ifctrano-0.8.0}/pyproject.toml +2 -1
- {ifctrano-0.7.0 → ifctrano-0.8.0}/LICENSE +0 -0
- {ifctrano-0.7.0 → ifctrano-0.8.0}/README.md +0 -0
- {ifctrano-0.7.0 → ifctrano-0.8.0}/ifctrano/__init__.py +0 -0
- {ifctrano-0.7.0 → ifctrano-0.8.0}/ifctrano/base.py +0 -0
- {ifctrano-0.7.0 → ifctrano-0.8.0}/ifctrano/bounding_box.py +0 -0
- {ifctrano-0.7.0 → ifctrano-0.8.0}/ifctrano/example/verification.ifc +0 -0
- {ifctrano-0.7.0 → ifctrano-0.8.0}/ifctrano/exceptions.py +0 -0
- {ifctrano-0.7.0 → ifctrano-0.8.0}/ifctrano/types.py +0 -0
- {ifctrano-0.7.0 → ifctrano-0.8.0}/ifctrano/utils.py +0 -0
@@ -1,9 +1,10 @@
|
|
1
1
|
import logging
|
2
2
|
import re
|
3
3
|
from pathlib import Path
|
4
|
-
from typing import List, Tuple, Any, Optional, Set
|
4
|
+
from typing import List, Tuple, Any, Optional, Set, Dict
|
5
5
|
|
6
6
|
import ifcopenshell
|
7
|
+
import yaml
|
7
8
|
from ifcopenshell import file, entity_instance
|
8
9
|
from pydantic import validate_call, Field, model_validator, field_validator
|
9
10
|
from trano.elements import InternalElement # type: ignore
|
@@ -23,7 +24,11 @@ from ifctrano.space_boundary import (
|
|
23
24
|
initialize_tree,
|
24
25
|
Space,
|
25
26
|
)
|
26
|
-
from ifctrano.construction import
|
27
|
+
from ifctrano.construction import (
|
28
|
+
Constructions,
|
29
|
+
default_construction,
|
30
|
+
default_internal_construction,
|
31
|
+
)
|
27
32
|
|
28
33
|
logger = logging.getLogger(__name__)
|
29
34
|
|
@@ -215,6 +220,71 @@ class Building(BaseShow):
|
|
215
220
|
def get_adjacency(self) -> InternalElements:
|
216
221
|
return get_internal_elements(self.space_boundaries)
|
217
222
|
|
223
|
+
@validate_call
|
224
|
+
def to_config(
|
225
|
+
self,
|
226
|
+
north_axis: Optional[Vector] = None,
|
227
|
+
) -> Dict[str, Any]:
|
228
|
+
north_axis = north_axis or Vector(x=0, y=1, z=0)
|
229
|
+
spaces = [
|
230
|
+
space_boundary.to_config(
|
231
|
+
self.internal_elements.internal_element_ids(),
|
232
|
+
north_axis,
|
233
|
+
self.constructions,
|
234
|
+
)
|
235
|
+
for space_boundary in self.space_boundaries
|
236
|
+
]
|
237
|
+
internal_walls = []
|
238
|
+
for internal_element in self.internal_elements.elements:
|
239
|
+
space_1 = internal_element.spaces[0]
|
240
|
+
space_2 = internal_element.spaces[1]
|
241
|
+
construction = self.constructions.get_construction(
|
242
|
+
internal_element.element, default_internal_construction
|
243
|
+
)
|
244
|
+
if internal_element.element.is_a() in ["IfcSlab"]:
|
245
|
+
space_1_tilt = (
|
246
|
+
Tilt.floor.value
|
247
|
+
if space_1.bounding_box.centroid.z > space_2.bounding_box.centroid.z
|
248
|
+
else Tilt.ceiling.value
|
249
|
+
)
|
250
|
+
space_2_tilt = (
|
251
|
+
Tilt.floor.value
|
252
|
+
if space_2.bounding_box.centroid.z > space_1.bounding_box.centroid.z
|
253
|
+
else Tilt.ceiling.value
|
254
|
+
)
|
255
|
+
if space_1_tilt == space_2_tilt:
|
256
|
+
raise ValueError("Space tilts are not compatible.")
|
257
|
+
internal_walls.append(
|
258
|
+
{
|
259
|
+
"space_1": space_1.space_unique_name(),
|
260
|
+
"space_2": space_2.space_unique_name(),
|
261
|
+
"construction": construction.name,
|
262
|
+
"surface": internal_element.area,
|
263
|
+
"space_1_tilt": space_1_tilt,
|
264
|
+
"space_2_tilt": space_2_tilt,
|
265
|
+
}
|
266
|
+
)
|
267
|
+
else:
|
268
|
+
internal_walls.append(
|
269
|
+
{
|
270
|
+
"space_1": space_1.space_unique_name(),
|
271
|
+
"space_2": space_2.space_unique_name(),
|
272
|
+
"construction": construction.name,
|
273
|
+
"surface": internal_element.area,
|
274
|
+
}
|
275
|
+
)
|
276
|
+
construction_config = self.constructions.to_config()
|
277
|
+
return construction_config | {
|
278
|
+
"spaces": spaces,
|
279
|
+
"internal_walls": internal_walls,
|
280
|
+
}
|
281
|
+
|
282
|
+
@validate_call
|
283
|
+
def to_yaml(self, yaml_path: Path, north_axis: Optional[Vector] = None) -> None:
|
284
|
+
config = self.to_config(north_axis=north_axis)
|
285
|
+
yaml_data = yaml.dump(config)
|
286
|
+
yaml_path.write_text(yaml_data)
|
287
|
+
|
218
288
|
@validate_call
|
219
289
|
def create_network(
|
220
290
|
self,
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import logging
|
2
|
-
from typing import List, Optional
|
2
|
+
from typing import List, Optional, Dict, Any
|
3
3
|
|
4
4
|
from ifcopenshell import file, entity_instance
|
5
5
|
|
@@ -38,6 +38,12 @@ default_construction = Construction(
|
|
38
38
|
Layer(material=material_1, thickness=0.18),
|
39
39
|
],
|
40
40
|
)
|
41
|
+
default_internal_construction = Construction(
|
42
|
+
name="default_internal_construction",
|
43
|
+
layers=[
|
44
|
+
Layer(material=material_1, thickness=0.18),
|
45
|
+
],
|
46
|
+
)
|
41
47
|
id_100 = GlassMaterial(
|
42
48
|
name="id_100",
|
43
49
|
thermal_conductivity=1,
|
@@ -186,14 +192,16 @@ class Constructions(BaseModel):
|
|
186
192
|
)
|
187
193
|
return cls(constructions=constructions)
|
188
194
|
|
189
|
-
def get_construction(
|
195
|
+
def get_construction(
|
196
|
+
self, entity: entity_instance, default: Optional[Construction] = None
|
197
|
+
) -> Construction:
|
190
198
|
construction_id = self._get_construction_id(entity)
|
191
199
|
if construction_id is None:
|
192
200
|
logger.warning(
|
193
201
|
f"Construction ID not found for {entity.GlobalId} ({entity.is_a()}). "
|
194
202
|
f"Using default construction."
|
195
203
|
)
|
196
|
-
return default_construction
|
204
|
+
return default or default_construction
|
197
205
|
constructions = [
|
198
206
|
construction.to_construction()
|
199
207
|
for construction in self.constructions
|
@@ -225,3 +233,79 @@ class Constructions(BaseModel):
|
|
225
233
|
else:
|
226
234
|
logger.error("Unexpected material type found.")
|
227
235
|
return None
|
236
|
+
|
237
|
+
def to_config(self) -> Dict[str, Any]:
|
238
|
+
constructions_all = [*self.constructions, default_construction, glass]
|
239
|
+
constructions = [
|
240
|
+
{
|
241
|
+
"id": construction.name,
|
242
|
+
"layers": [
|
243
|
+
{"material": layer.material.name, "thickness": layer.thickness}
|
244
|
+
for layer in construction.layers
|
245
|
+
],
|
246
|
+
}
|
247
|
+
for construction in constructions_all
|
248
|
+
if isinstance(construction, Construction)
|
249
|
+
]
|
250
|
+
glazings = [
|
251
|
+
{
|
252
|
+
"id": construction.name,
|
253
|
+
"layers": [
|
254
|
+
{
|
255
|
+
(
|
256
|
+
"glass" if isinstance(layer, GlassLayer) else "gas"
|
257
|
+
): layer.material.name,
|
258
|
+
"thickness": layer.thickness,
|
259
|
+
}
|
260
|
+
for layer in construction.layers
|
261
|
+
],
|
262
|
+
}
|
263
|
+
for construction in constructions_all
|
264
|
+
if isinstance(construction, Glass)
|
265
|
+
]
|
266
|
+
materials = {
|
267
|
+
layer.material
|
268
|
+
for construction in constructions_all
|
269
|
+
for layer in construction.layers
|
270
|
+
if type(layer.material) is Material
|
271
|
+
}
|
272
|
+
gas = {
|
273
|
+
layer.material
|
274
|
+
for construction in constructions_all
|
275
|
+
for layer in construction.layers
|
276
|
+
if type(layer.material) is Gas
|
277
|
+
}
|
278
|
+
glass_ = {
|
279
|
+
layer.material
|
280
|
+
for construction in constructions_all
|
281
|
+
for layer in construction.layers
|
282
|
+
if type(layer.material) is GlassMaterial
|
283
|
+
}
|
284
|
+
|
285
|
+
materials_ = [
|
286
|
+
(material.model_dump(exclude={"name"}) | {"id": material.name})
|
287
|
+
for material in materials
|
288
|
+
]
|
289
|
+
gas_ = [
|
290
|
+
(material.model_dump(exclude={"name"}) | {"id": material.name})
|
291
|
+
for material in gas
|
292
|
+
]
|
293
|
+
glass_material = [
|
294
|
+
(_convert_glass(material) | {"id": material.name}) for material in glass_
|
295
|
+
]
|
296
|
+
return {
|
297
|
+
"constructions": constructions,
|
298
|
+
"material": materials_,
|
299
|
+
"glazings": glazings,
|
300
|
+
"gas": gas_,
|
301
|
+
"glass_material": glass_material,
|
302
|
+
}
|
303
|
+
|
304
|
+
|
305
|
+
def _convert_glass(glass_: Material) -> Dict[str, Any]:
|
306
|
+
return {
|
307
|
+
key: (
|
308
|
+
value if not isinstance(value, list) else f"{{{','.join(map(str, value))}}}"
|
309
|
+
)
|
310
|
+
for key, value in glass_.model_dump().items()
|
311
|
+
}
|
@@ -22,6 +22,38 @@ CHECKMARK = "[green]✔[/green]"
|
|
22
22
|
CROSS_MARK = "[red]✘[/red]"
|
23
23
|
|
24
24
|
|
25
|
+
@app.command()
|
26
|
+
def config(
|
27
|
+
model: Annotated[
|
28
|
+
str,
|
29
|
+
typer.Argument(help="Local path to the ifc file."),
|
30
|
+
],
|
31
|
+
show_space_boundaries: Annotated[
|
32
|
+
bool,
|
33
|
+
typer.Option(help="Show computed space boundaries."),
|
34
|
+
] = False,
|
35
|
+
) -> None:
|
36
|
+
working_directory = Path.cwd()
|
37
|
+
with Progress(
|
38
|
+
SpinnerColumn(),
|
39
|
+
TextColumn("[progress.description]{task.description}"),
|
40
|
+
transient=True,
|
41
|
+
) as progress:
|
42
|
+
modelica_model_path = Path(model).resolve().with_suffix(".mo")
|
43
|
+
config_path = working_directory.joinpath(f"{modelica_model_path.stem}.yaml")
|
44
|
+
task = progress.add_task(
|
45
|
+
description=f"Generating {config_path} configuration file.",
|
46
|
+
total=None,
|
47
|
+
)
|
48
|
+
building = Building.from_ifc(Path(model))
|
49
|
+
if show_space_boundaries:
|
50
|
+
print(f"{CHECKMARK} Showing space boundaries.")
|
51
|
+
building.show()
|
52
|
+
building.to_yaml(config_path)
|
53
|
+
progress.remove_task(task)
|
54
|
+
print(f"{CHECKMARK} configuration file generated: {config_path}.")
|
55
|
+
|
56
|
+
|
25
57
|
@app.command()
|
26
58
|
def create(
|
27
59
|
model: Annotated[
|
@@ -1,5 +1,7 @@
|
|
1
|
+
import logging
|
2
|
+
import math
|
1
3
|
import multiprocessing
|
2
|
-
from typing import Optional, List, Tuple, Any, Annotated
|
4
|
+
from typing import Optional, List, Tuple, Any, Annotated, Dict
|
3
5
|
|
4
6
|
import ifcopenshell
|
5
7
|
import ifcopenshell.geom
|
@@ -34,6 +36,8 @@ from ifctrano.utils import (
|
|
34
36
|
|
35
37
|
ROOF_VECTOR = Vector(x=0, y=0, z=1)
|
36
38
|
|
39
|
+
logger = logging.getLogger(__name__)
|
40
|
+
|
37
41
|
|
38
42
|
def initialize_tree(ifc_file: file) -> ifcopenshell.geom.tree:
|
39
43
|
tree = ifcopenshell.geom.tree()
|
@@ -89,6 +93,14 @@ class Space(GlobalId):
|
|
89
93
|
main_name = f"{remove_non_alphanumeric(self.name)}_" if self.name else ""
|
90
94
|
return f"space_{main_name}{remove_non_alphanumeric(self.entity.GlobalId)}"
|
91
95
|
|
96
|
+
def space_unique_name(self) -> str:
|
97
|
+
base_name = remove_non_alphanumeric(self.name) if self.name else ""
|
98
|
+
main_name = f"{base_name}_" if base_name else ""
|
99
|
+
space_name = f"{main_name}{remove_non_alphanumeric(self.entity.GlobalId)[-3:]}"
|
100
|
+
if "space" not in space_name:
|
101
|
+
return f"space_{space_name}"
|
102
|
+
return space_name
|
103
|
+
|
92
104
|
|
93
105
|
class ExternalSpaceBoundaryGroup(BaseModelConfig):
|
94
106
|
constructions: List[BaseWall]
|
@@ -147,6 +159,10 @@ class ExternalSpaceBoundaryGroups(BaseModelConfig):
|
|
147
159
|
)
|
148
160
|
|
149
161
|
|
162
|
+
def deg_to_rad(deg: float) -> float:
|
163
|
+
return deg * math.pi / 180.0
|
164
|
+
|
165
|
+
|
150
166
|
class Azimuths(BaseModel):
|
151
167
|
north: List[float] = [0.0, 360]
|
152
168
|
east: List[float] = [90.0]
|
@@ -278,6 +294,63 @@ class SpaceBoundaries(BaseShow):
|
|
278
294
|
if space_boundary in self.boundaries:
|
279
295
|
self.boundaries.remove(space_boundary)
|
280
296
|
|
297
|
+
def to_config(
|
298
|
+
self,
|
299
|
+
exclude_entities: List[str],
|
300
|
+
north_axis: Vector,
|
301
|
+
constructions: Constructions,
|
302
|
+
) -> Dict[str, Any]:
|
303
|
+
external_boundaries: Dict[str, Any] = {
|
304
|
+
"external_walls": [],
|
305
|
+
"floor_on_grounds": [],
|
306
|
+
"windows": [],
|
307
|
+
}
|
308
|
+
external_boundaries_check = []
|
309
|
+
for boundary in self.boundaries:
|
310
|
+
boundary_model = boundary.model_element(
|
311
|
+
exclude_entities, north_axis, constructions
|
312
|
+
)
|
313
|
+
if boundary_model:
|
314
|
+
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")
|
333
|
+
external_space_boundaries_group = (
|
334
|
+
ExternalSpaceBoundaryGroups.from_external_boundaries(
|
335
|
+
external_boundaries_check
|
336
|
+
)
|
337
|
+
)
|
338
|
+
if not external_space_boundaries_group.has_windows_without_wall():
|
339
|
+
logger.error(
|
340
|
+
f"Space {self.space.global_id} has a boundary that has a windows but without walls."
|
341
|
+
)
|
342
|
+
occupancy_parameters = Occupancy().parameters.model_dump(mode="json")
|
343
|
+
space_parameters = SpaceParameter(
|
344
|
+
floor_area=self.space.floor_area,
|
345
|
+
average_room_height=self.space.average_room_height,
|
346
|
+
).model_dump(mode="json")
|
347
|
+
return {
|
348
|
+
"id": self.space.space_unique_name(),
|
349
|
+
"occupancy": {"parameters": occupancy_parameters},
|
350
|
+
"parameters": space_parameters,
|
351
|
+
"external_boundaries": external_boundaries,
|
352
|
+
}
|
353
|
+
|
281
354
|
def model(
|
282
355
|
self,
|
283
356
|
exclude_entities: List[str],
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "ifctrano"
|
3
|
-
version = "0.
|
3
|
+
version = "0.8.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"
|
@@ -33,6 +33,7 @@ isort = "^5.13.2"
|
|
33
33
|
jsf = "^0.11.2"
|
34
34
|
black = "^24.10.0"
|
35
35
|
pytest = "^7.4.3"
|
36
|
+
deepdiff = "^8.6.1"
|
36
37
|
|
37
38
|
[tool.poetry.scripts]
|
38
39
|
ifctrano = "ifctrano.main:app"
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|