ifctrano 0.7.0__py3-none-any.whl → 0.8.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,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 Constructions, default_construction
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,
ifctrano/construction.py CHANGED
@@ -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(self, entity: entity_instance) -> 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
+ }
ifctrano/main.py CHANGED
@@ -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
  Metadata-Version: 2.4
2
2
  Name: ifctrano
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: Package for generating building energy simulation model from IFC
5
5
  License: GPL V3
6
6
  License-File: LICENSE
@@ -1,16 +1,16 @@
1
1
  ifctrano/__init__.py,sha256=6wK0Qa_5Xe0KSk5Spn98AjTNCTIXtenZ3C0k8ZaVcn8,191
2
2
  ifctrano/base.py,sha256=s8BSnQqd6v_NFvkQFRriHCine6iylUBQai2FduC4IFk,14352
3
3
  ifctrano/bounding_box.py,sha256=64jlzY60ZDUX4ipR34WkOICpPhxLP-5w-FP42yBONRs,7729
4
- ifctrano/building.py,sha256=tBctMyrsvwRiDk5bDhAdfgRlbyvnbCCeA8rcdUjMbU8,9355
5
- ifctrano/construction.py,sha256=Lyh_o37znrDyxtKgw7dOqVsQk1S3btCPDh2YTJ8h6T0,7234
4
+ ifctrano/building.py,sha256=oPRk8Mwvr51sKaWW-sFPPud23a7KAq10-RgpbfFVziw,12074
5
+ ifctrano/construction.py,sha256=5W9Vo5VyliQ26vl_NTOnR9i5wbZjz6FKL7O3Sm57uNw,9997
6
6
  ifctrano/example/verification.ifc,sha256=tQ9QcubT_wrbb-sc1WRRwYpb2cbkWm3dnRfXdP5GTTg,131
7
7
  ifctrano/exceptions.py,sha256=JDy_0HV7_FLfr92DkrN8F59zUHl9KdMa_lfIcfBJG6I,540
8
- ifctrano/main.py,sha256=YTp7RaWRMKt_UC2DRt_oXXLGTLCAc9FPg6Tsqg1zgpk,4979
9
- ifctrano/space_boundary.py,sha256=PmiKhcJo6iyxXKNab5NCphQgBQwatoNsqsi-BYHOONs,16055
8
+ ifctrano/main.py,sha256=W6Jx8ZN2Euoz44ECrU8xYetdSmdgkWBo3lngOVYXNU4,6053
9
+ ifctrano/space_boundary.py,sha256=xJc4mBxiJvZMm75APBHMnpNC7xKBZl36BoRjRhTL-xk,19046
10
10
  ifctrano/types.py,sha256=FBSWrD66EL7uHvv0GveCXmYyLtCr8gGRlT7-5j7aZD8,181
11
11
  ifctrano/utils.py,sha256=zLQUGVo15RSQQ-vfOuHsJBkHy4QhpxKSAH7Au72s0s0,873
12
- ifctrano-0.7.0.dist-info/METADATA,sha256=VAynLl3eaecRmVSgvU3XVSmO-_weipMqdfbw-pq3Blw,4884
13
- ifctrano-0.7.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
14
- ifctrano-0.7.0.dist-info/entry_points.txt,sha256=_2daDejazkphufyEu0m3lOeTio53WYmjol3KmSN0JM4,46
15
- ifctrano-0.7.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
16
- ifctrano-0.7.0.dist-info/RECORD,,
12
+ ifctrano-0.8.0.dist-info/METADATA,sha256=cFw_XDKTheAlXSmvnBd4lyhcchKvs17L2fncjFzq8Rs,4884
13
+ ifctrano-0.8.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
14
+ ifctrano-0.8.0.dist-info/entry_points.txt,sha256=_2daDejazkphufyEu0m3lOeTio53WYmjol3KmSN0JM4,46
15
+ ifctrano-0.8.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
16
+ ifctrano-0.8.0.dist-info/RECORD,,