ifctrano 0.2.0__tar.gz → 0.4.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.2.0 → ifctrano-0.4.0}/PKG-INFO +101 -29
- {ifctrano-0.2.0 → ifctrano-0.4.0}/README.md +99 -27
- ifctrano-0.4.0/ifctrano/__init__.py +8 -0
- {ifctrano-0.2.0 → ifctrano-0.4.0}/ifctrano/base.py +24 -11
- {ifctrano-0.2.0 → ifctrano-0.4.0}/ifctrano/bounding_box.py +22 -15
- {ifctrano-0.2.0 → ifctrano-0.4.0}/ifctrano/building.py +7 -4
- {ifctrano-0.2.0 → ifctrano-0.4.0}/ifctrano/construction.py +5 -5
- {ifctrano-0.2.0 → ifctrano-0.4.0}/ifctrano/exceptions.py +4 -0
- {ifctrano-0.2.0 → ifctrano-0.4.0}/ifctrano/main.py +16 -11
- {ifctrano-0.2.0 → ifctrano-0.4.0}/ifctrano/space_boundary.py +104 -28
- {ifctrano-0.2.0 → ifctrano-0.4.0}/ifctrano/utils.py +8 -0
- {ifctrano-0.2.0 → ifctrano-0.4.0}/pyproject.toml +2 -2
- ifctrano-0.2.0/ifctrano/__init__.py +0 -3
- {ifctrano-0.2.0 → ifctrano-0.4.0}/LICENSE +0 -0
- {ifctrano-0.2.0 → ifctrano-0.4.0}/ifctrano/example/verification.ifc +0 -0
- {ifctrano-0.2.0 → ifctrano-0.4.0}/ifctrano/types.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: ifctrano
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.4.0
|
4
4
|
Summary: Package for generating building energy simulation model from IFC
|
5
5
|
License: GPL V3
|
6
6
|
Keywords: BIM,IFC,energy simulation,modelica,building energy simulation,buildings,ideas
|
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
15
15
|
Requires-Dist: ifcopenshell (>=0.8.1.post1,<0.9.0)
|
16
16
|
Requires-Dist: open3d (>=0.19.0,<0.20.0)
|
17
17
|
Requires-Dist: shapely (>=2.0.7,<3.0.0)
|
18
|
-
Requires-Dist: trano (>=0.
|
18
|
+
Requires-Dist: trano (>=0.6.0,<0.7.0)
|
19
19
|
Requires-Dist: typer (>=0.12.5,<0.13.0)
|
20
20
|
Requires-Dist: vedo (>=2025.5.3,<2026.0.0)
|
21
21
|
Project-URL: Repository, https://github.com/andoludo/ifctrano
|
@@ -23,33 +23,11 @@ Description-Content-Type: text/markdown
|
|
23
23
|
|
24
24
|
# ifctrano - IFC to Energy Simulation Tool
|
25
25
|
|
26
|
+
---
|
26
27
|
📖 **Full Documentation:** 👉 [ifctrano Docs](https://andoludo.github.io/ifctrano/)
|
28
|
+
---
|
27
29
|
|
28
|
-
|
29
|
-
pip install ifctrano
|
30
|
-
```
|
31
|
-
|
32
|
-
To check the installation, run the following commands:
|
33
|
-
|
34
|
-
```bash
|
35
|
-
ifctrano --help
|
36
|
-
|
37
|
-
ifctrano verify
|
38
|
-
```
|
39
|
-
|
40
|
-
# ⚠️ WARNING ⚠️
|
41
|
-
|
42
|
-
**This package is still under construction and is largely a work in progress.**
|
43
|
-
There are still several aspects that need further development, including:
|
44
|
-
|
45
|
-
- Material and construction extraction
|
46
|
-
- Slab and roof boundaries
|
47
|
-
- Systems integration
|
48
|
-
- Additional validation
|
49
|
-
- Bug fixes
|
50
|
-
- ...
|
51
|
-
-
|
52
|
-
Help and contribution are more than appreciated! 🚧
|
30
|
+
Generate Modelica building models directly from IFC files — with support for simulation, visualization, and multiple libraries.
|
53
31
|
|
54
32
|
## Overview
|
55
33
|
ifctrano is yet another **IFC to energy simulation** tool designed to translate **Industry Foundation Classes (IFC)** models into energy simulation models in **Modelica**.
|
@@ -79,10 +57,104 @@ ifctrano has been tested using open-source IFC files from various repositories:
|
|
79
57
|
- 🕸️ [Ifc2Graph Test Files](https://github.com/JBjoernskov/Ifc2Graph/tree/main/test_ifc_files)
|
80
58
|
- 🔓 [Open Source BIM](https://github.com/opensourceBIM)
|
81
59
|
|
82
|
-
## Installation
|
83
|
-
|
60
|
+
## 🚀 Installation
|
61
|
+
|
62
|
+
### 📦 Install `ifctrano`
|
63
|
+
|
64
|
+
!!! warning
|
65
|
+
Trano requires python 3.9 or higher and docker to be installed on the system.
|
66
|
+
|
67
|
+
|
68
|
+
ifctrano is a Python package that can be installed via pip.
|
69
|
+
|
70
|
+
```bash
|
71
|
+
pip install ifctrano
|
72
|
+
```
|
73
|
+
|
74
|
+
### ✅ Verify Installation
|
75
|
+
|
76
|
+
Run the following commands to ensure everything is working:
|
77
|
+
|
78
|
+
```bash
|
79
|
+
ifctrano --help
|
80
|
+
ifctrano verify
|
81
|
+
```
|
82
|
+
|
83
|
+
---
|
84
|
+
|
85
|
+
## 🔧 Optional Dependencies
|
86
|
+
|
87
|
+
### 🐳 Docker (for simulation)
|
88
|
+
|
89
|
+
To enable model simulation using the official OpenModelica Docker image, install Docker Desktop:
|
90
|
+
|
91
|
+
👉 [https://docs.docker.com/desktop/](https://docs.docker.com/desktop/)
|
92
|
+
|
93
|
+
Required for using the `--simulate-model` flag.
|
94
|
+
|
95
|
+
---
|
84
96
|
|
97
|
+
### 🧠 Graphviz (for layout visualization)
|
98
|
+
|
99
|
+
`ifctrano` leverages Graphviz to optimize component layout in generated Modelica models. It is optional, but **recommended**.
|
100
|
+
|
101
|
+
#### 📥 Install on Windows
|
102
|
+
|
103
|
+
- Download and install from: [https://graphviz.org/download/](https://graphviz.org/download/)
|
104
|
+
- Add the Graphviz `bin` folder to your **system `PATH`**.
|
105
|
+
|
106
|
+
#### 🐧 Install on Linux
|
107
|
+
|
108
|
+
```bash
|
109
|
+
sudo apt update
|
110
|
+
sudo apt install graphviz
|
111
|
+
```
|
112
|
+
|
113
|
+
---
|
114
|
+
|
115
|
+
## ⚙️ Usage
|
116
|
+
|
117
|
+
### 📁 Generate Modelica models from IFC
|
118
|
+
|
119
|
+
#### 🏢 Using the **Buildings** library
|
120
|
+
|
121
|
+
```bash
|
122
|
+
ifctrano create /path/to/your.ifc
|
123
|
+
```
|
124
|
+
|
125
|
+
#### 🏫 Using the **IDEAS** library
|
126
|
+
|
127
|
+
```bash
|
128
|
+
ifctrano create /path/to/your.ifc IDEAS
|
129
|
+
```
|
130
|
+
|
131
|
+
#### 🧮 Using the **Reduced Order** library
|
132
|
+
|
133
|
+
```bash
|
134
|
+
ifctrano create /path/to/your.ifc reduced_order
|
135
|
+
```
|
136
|
+
|
137
|
+
---
|
138
|
+
|
139
|
+
### 🧱 Show Space Boundaries
|
140
|
+
|
141
|
+
To visualize the computed space boundaries:
|
142
|
+
|
143
|
+
```bash
|
144
|
+
ifctrano create /path/to/your.ifc --show-space-boundaries
|
145
|
+
```
|
146
|
+
|
147
|
+
---
|
148
|
+
|
149
|
+
### 🔁 Simulate the Model
|
150
|
+
|
151
|
+
Run a full simulation after model generation:
|
152
|
+
|
153
|
+
```bash
|
154
|
+
ifctrano create /path/to/your.ifc --simulate-model
|
155
|
+
```
|
85
156
|
|
157
|
+
Make sure Docker is installed and running before simulating.
|
86
158
|
|
87
159
|
---
|
88
160
|
💡 **ifctrano** aims to make energy simulation model generation from IFC files **simpler, more accessible, and less reliant on incomplete IFC attributes**. 🚀
|
@@ -1,32 +1,10 @@
|
|
1
1
|
# ifctrano - IFC to Energy Simulation Tool
|
2
2
|
|
3
|
+
---
|
3
4
|
📖 **Full Documentation:** 👉 [ifctrano Docs](https://andoludo.github.io/ifctrano/)
|
5
|
+
---
|
4
6
|
|
5
|
-
|
6
|
-
pip install ifctrano
|
7
|
-
```
|
8
|
-
|
9
|
-
To check the installation, run the following commands:
|
10
|
-
|
11
|
-
```bash
|
12
|
-
ifctrano --help
|
13
|
-
|
14
|
-
ifctrano verify
|
15
|
-
```
|
16
|
-
|
17
|
-
# ⚠️ WARNING ⚠️
|
18
|
-
|
19
|
-
**This package is still under construction and is largely a work in progress.**
|
20
|
-
There are still several aspects that need further development, including:
|
21
|
-
|
22
|
-
- Material and construction extraction
|
23
|
-
- Slab and roof boundaries
|
24
|
-
- Systems integration
|
25
|
-
- Additional validation
|
26
|
-
- Bug fixes
|
27
|
-
- ...
|
28
|
-
-
|
29
|
-
Help and contribution are more than appreciated! 🚧
|
7
|
+
Generate Modelica building models directly from IFC files — with support for simulation, visualization, and multiple libraries.
|
30
8
|
|
31
9
|
## Overview
|
32
10
|
ifctrano is yet another **IFC to energy simulation** tool designed to translate **Industry Foundation Classes (IFC)** models into energy simulation models in **Modelica**.
|
@@ -56,10 +34,104 @@ ifctrano has been tested using open-source IFC files from various repositories:
|
|
56
34
|
- 🕸️ [Ifc2Graph Test Files](https://github.com/JBjoernskov/Ifc2Graph/tree/main/test_ifc_files)
|
57
35
|
- 🔓 [Open Source BIM](https://github.com/opensourceBIM)
|
58
36
|
|
59
|
-
## Installation
|
60
|
-
|
37
|
+
## 🚀 Installation
|
38
|
+
|
39
|
+
### 📦 Install `ifctrano`
|
40
|
+
|
41
|
+
!!! warning
|
42
|
+
Trano requires python 3.9 or higher and docker to be installed on the system.
|
43
|
+
|
44
|
+
|
45
|
+
ifctrano is a Python package that can be installed via pip.
|
46
|
+
|
47
|
+
```bash
|
48
|
+
pip install ifctrano
|
49
|
+
```
|
50
|
+
|
51
|
+
### ✅ Verify Installation
|
52
|
+
|
53
|
+
Run the following commands to ensure everything is working:
|
54
|
+
|
55
|
+
```bash
|
56
|
+
ifctrano --help
|
57
|
+
ifctrano verify
|
58
|
+
```
|
59
|
+
|
60
|
+
---
|
61
|
+
|
62
|
+
## 🔧 Optional Dependencies
|
63
|
+
|
64
|
+
### 🐳 Docker (for simulation)
|
65
|
+
|
66
|
+
To enable model simulation using the official OpenModelica Docker image, install Docker Desktop:
|
67
|
+
|
68
|
+
👉 [https://docs.docker.com/desktop/](https://docs.docker.com/desktop/)
|
69
|
+
|
70
|
+
Required for using the `--simulate-model` flag.
|
71
|
+
|
72
|
+
---
|
61
73
|
|
74
|
+
### 🧠 Graphviz (for layout visualization)
|
75
|
+
|
76
|
+
`ifctrano` leverages Graphviz to optimize component layout in generated Modelica models. It is optional, but **recommended**.
|
77
|
+
|
78
|
+
#### 📥 Install on Windows
|
79
|
+
|
80
|
+
- Download and install from: [https://graphviz.org/download/](https://graphviz.org/download/)
|
81
|
+
- Add the Graphviz `bin` folder to your **system `PATH`**.
|
82
|
+
|
83
|
+
#### 🐧 Install on Linux
|
84
|
+
|
85
|
+
```bash
|
86
|
+
sudo apt update
|
87
|
+
sudo apt install graphviz
|
88
|
+
```
|
89
|
+
|
90
|
+
---
|
91
|
+
|
92
|
+
## ⚙️ Usage
|
93
|
+
|
94
|
+
### 📁 Generate Modelica models from IFC
|
95
|
+
|
96
|
+
#### 🏢 Using the **Buildings** library
|
97
|
+
|
98
|
+
```bash
|
99
|
+
ifctrano create /path/to/your.ifc
|
100
|
+
```
|
101
|
+
|
102
|
+
#### 🏫 Using the **IDEAS** library
|
103
|
+
|
104
|
+
```bash
|
105
|
+
ifctrano create /path/to/your.ifc IDEAS
|
106
|
+
```
|
107
|
+
|
108
|
+
#### 🧮 Using the **Reduced Order** library
|
109
|
+
|
110
|
+
```bash
|
111
|
+
ifctrano create /path/to/your.ifc reduced_order
|
112
|
+
```
|
113
|
+
|
114
|
+
---
|
115
|
+
|
116
|
+
### 🧱 Show Space Boundaries
|
117
|
+
|
118
|
+
To visualize the computed space boundaries:
|
119
|
+
|
120
|
+
```bash
|
121
|
+
ifctrano create /path/to/your.ifc --show-space-boundaries
|
122
|
+
```
|
123
|
+
|
124
|
+
---
|
125
|
+
|
126
|
+
### 🔁 Simulate the Model
|
127
|
+
|
128
|
+
Run a full simulation after model generation:
|
129
|
+
|
130
|
+
```bash
|
131
|
+
ifctrano create /path/to/your.ifc --simulate-model
|
132
|
+
```
|
62
133
|
|
134
|
+
Make sure Docker is installed and running before simulating.
|
63
135
|
|
64
136
|
---
|
65
137
|
💡 **ifctrano** aims to make energy simulation model generation from IFC files **simpler, more accessible, and less reliant on incomplete IFC attributes**. 🚀
|
@@ -1,5 +1,8 @@
|
|
1
1
|
import json
|
2
2
|
import math
|
3
|
+
import sys
|
4
|
+
from itertools import combinations
|
5
|
+
from multiprocessing import Process
|
3
6
|
from pathlib import Path
|
4
7
|
from typing import Tuple, Literal, List, Annotated, Any, Dict, cast
|
5
8
|
|
@@ -18,7 +21,6 @@ from shapely.geometry.polygon import Polygon # type: ignore
|
|
18
21
|
from vedo import Line, Arrow, Mesh, show, write # type: ignore
|
19
22
|
|
20
23
|
from ifctrano.exceptions import VectorWithNansError
|
21
|
-
from multiprocessing import Process
|
22
24
|
|
23
25
|
settings = ifcopenshell.geom.settings() # type: ignore
|
24
26
|
Coordinate = Literal["x", "y", "z"]
|
@@ -53,6 +55,9 @@ class BaseShow(BaseModel):
|
|
53
55
|
def description(self) -> Any: ... # noqa: ANN401
|
54
56
|
|
55
57
|
def show(self, interactive: bool = True) -> None:
|
58
|
+
if sys.platform == "win32":
|
59
|
+
_show(self.lines(), interactive)
|
60
|
+
return
|
56
61
|
p = Process(target=_show, args=(self.lines(), interactive))
|
57
62
|
p.start()
|
58
63
|
|
@@ -147,12 +152,15 @@ class Vector(BasePoint):
|
|
147
152
|
a = self.dot(other) / other.dot(other)
|
148
153
|
return Vector(x=a * other.x, y=a * other.y, z=a * other.z)
|
149
154
|
|
150
|
-
def
|
155
|
+
def normalize(self) -> "Vector":
|
151
156
|
normalized_vector = self.to_array() / np.linalg.norm(self.to_array())
|
152
157
|
return Vector(
|
153
158
|
x=normalized_vector[0], y=normalized_vector[1], z=normalized_vector[2]
|
154
159
|
)
|
155
160
|
|
161
|
+
def norm(self) -> float:
|
162
|
+
return float(np.linalg.norm(self.to_array()))
|
163
|
+
|
156
164
|
def to_array(self) -> np.ndarray: # type: ignore
|
157
165
|
return np.array([self.x, self.y, self.z])
|
158
166
|
|
@@ -163,7 +171,7 @@ class Vector(BasePoint):
|
|
163
171
|
normal_index_list = [abs(v) for v in self.to_list()]
|
164
172
|
return normal_index_list.index(max(normal_index_list))
|
165
173
|
|
166
|
-
def
|
174
|
+
def is_null(self, tolerance: float = 0.1) -> bool:
|
167
175
|
return all(abs(value) < tolerance for value in self.to_list())
|
168
176
|
|
169
177
|
@classmethod
|
@@ -247,18 +255,23 @@ class Vertices(BaseModel):
|
|
247
255
|
return FaceVertices(points=self.points)
|
248
256
|
|
249
257
|
def get_local_coordinate_system(self) -> CoordinateSystem:
|
250
|
-
|
251
|
-
|
258
|
+
vectors = [
|
259
|
+
(a - b).normalize().to_array() for a, b in combinations(self.points, 2)
|
260
|
+
]
|
252
261
|
found = False
|
253
|
-
for
|
254
|
-
|
255
|
-
|
262
|
+
for v1, v2, v3 in combinations(vectors, 3):
|
263
|
+
if (
|
264
|
+
np.isclose(abs(np.dot(v1, v2)), 0)
|
265
|
+
and np.isclose(abs(np.dot(v1, v3)), 0)
|
266
|
+
and np.isclose(abs(np.dot(v2, v3)), 0)
|
267
|
+
):
|
256
268
|
found = True
|
269
|
+
x = Vector.from_array(v1)
|
270
|
+
y = Vector.from_array(v2)
|
271
|
+
z = Vector.from_array(v3)
|
257
272
|
break
|
258
273
|
if not found:
|
259
|
-
raise ValueError("
|
260
|
-
|
261
|
-
z = x * y
|
274
|
+
raise ValueError("Cannot find coordinate system")
|
262
275
|
return CoordinateSystem(x=x, y=y, z=z)
|
263
276
|
|
264
277
|
def get_bounding_box(self) -> "Vertices":
|
@@ -14,7 +14,7 @@ from pydantic import (
|
|
14
14
|
Field,
|
15
15
|
ConfigDict,
|
16
16
|
)
|
17
|
-
from
|
17
|
+
from scipy.spatial import ConvexHull, QhullError # type: ignore
|
18
18
|
from vedo import Line # type: ignore
|
19
19
|
|
20
20
|
from ifctrano.base import (
|
@@ -98,31 +98,28 @@ class OrientedBoundingBox(BaseShow):
|
|
98
98
|
lines.append(Line(a, b))
|
99
99
|
return lines
|
100
100
|
|
101
|
-
def contained(self, poly_1: Polygon, poly_2: Polygon) -> bool:
|
102
|
-
include_1_in_2 = poly_1.contains(poly_2)
|
103
|
-
include_2_in_1 = poly_2.contains(poly_1)
|
104
|
-
return bool(include_2_in_1 or include_1_in_2)
|
105
|
-
|
106
101
|
def intersect_faces(self, other: "OrientedBoundingBox") -> Optional[CommonSurface]:
|
107
102
|
extend_surfaces = []
|
108
103
|
for face in self.faces.faces:
|
109
104
|
|
110
105
|
for other_face in other.faces.faces:
|
111
106
|
vector = face.normal * other_face.normal
|
112
|
-
if vector.
|
107
|
+
if vector.is_null():
|
113
108
|
projected_face_1 = face.vertices.project(face.vertices)
|
114
109
|
projected_face_2 = face.vertices.project(other_face.vertices)
|
115
110
|
polygon_1 = projected_face_1.to_polygon()
|
116
111
|
polygon_2 = projected_face_2.to_polygon()
|
117
112
|
intersection = polygon_2.intersection(polygon_1)
|
118
|
-
if intersection.area > self.area_tolerance
|
119
|
-
polygon_1, polygon_2
|
120
|
-
):
|
113
|
+
if intersection.area > self.area_tolerance:
|
121
114
|
distance = projected_face_1.get_distance(projected_face_2)
|
122
115
|
area = intersection.area
|
123
116
|
try:
|
124
|
-
direction_vector = (
|
125
|
-
|
117
|
+
direction_vector = (
|
118
|
+
other.centroid - self.centroid
|
119
|
+
).normalize()
|
120
|
+
orientation = direction_vector.project(
|
121
|
+
face.normal
|
122
|
+
).normalize()
|
126
123
|
except VectorWithNansError as e:
|
127
124
|
logger.warning(
|
128
125
|
"Orientation vector was not properly computed when computing the intersection between "
|
@@ -170,7 +167,9 @@ class OrientedBoundingBox(BaseShow):
|
|
170
167
|
entity: Optional[entity_instance] = None,
|
171
168
|
) -> "OrientedBoundingBox":
|
172
169
|
points_ = open3d.utility.Vector3dVector(vertices)
|
173
|
-
mobb = open3d.geometry.OrientedBoundingBox.create_from_points_minimal(
|
170
|
+
mobb = open3d.geometry.OrientedBoundingBox.create_from_points_minimal(
|
171
|
+
points_, robust=True
|
172
|
+
)
|
174
173
|
height = (mobb.get_max_bound() - mobb.get_min_bound())[
|
175
174
|
2
|
176
175
|
] # assuming that height is the z axis
|
@@ -191,6 +190,14 @@ class OrientedBoundingBox(BaseShow):
|
|
191
190
|
entity_shape, entity_shape.geometry # type: ignore
|
192
191
|
)
|
193
192
|
vertices_ = Vertices.from_arrays(np.asarray(vertices))
|
193
|
+
try:
|
194
|
+
hull = ConvexHull(vertices_.to_array())
|
195
|
+
vertices_ = Vertices.from_arrays(vertices_.to_array()[hull.vertices])
|
194
196
|
|
195
|
-
|
196
|
-
|
197
|
+
except QhullError:
|
198
|
+
logger.error(
|
199
|
+
f"Convex hull failed for {entity.GlobalId} ({entity.is_a()}).... Continuing without it."
|
200
|
+
)
|
201
|
+
points_ = open3d.utility.Vector3dVector(vertices_.to_array())
|
202
|
+
aab = open3d.geometry.AxisAlignedBoundingBox.create_from_points(points_)
|
203
|
+
return cls.from_vertices(aab.get_box_points(), entity)
|
@@ -102,11 +102,11 @@ def get_internal_elements(space1_boundaries: List[SpaceBoundaries]) -> InternalE
|
|
102
102
|
and (
|
103
103
|
boundary.common_surface.orientation
|
104
104
|
* common_surface.orientation
|
105
|
-
).
|
105
|
+
).is_null()
|
106
106
|
and (
|
107
107
|
boundary_.common_surface.orientation
|
108
108
|
* common_surface.orientation
|
109
|
-
).
|
109
|
+
).is_null()
|
110
110
|
) and boundary.common_surface.orientation.dot(
|
111
111
|
boundary_.common_surface.orientation
|
112
112
|
) < 0:
|
@@ -200,7 +200,7 @@ class Building(BaseShow):
|
|
200
200
|
return get_internal_elements(self.space_boundaries)
|
201
201
|
|
202
202
|
@validate_call
|
203
|
-
def
|
203
|
+
def create_network(
|
204
204
|
self,
|
205
205
|
library: Libraries = "Buildings",
|
206
206
|
north_axis: Optional[Vector] = None,
|
@@ -232,6 +232,9 @@ class Building(BaseShow):
|
|
232
232
|
)
|
233
233
|
return network
|
234
234
|
|
235
|
+
def get_model(self) -> str:
|
236
|
+
return str(self.create_network().model())
|
237
|
+
|
235
238
|
def save_model(self, library: Libraries = "Buildings") -> None:
|
236
|
-
model_ = self.
|
239
|
+
model_ = self.create_network(library)
|
237
240
|
Path(self.parent_folder.joinpath(f"{self.name}.mo")).write_text(model_.model())
|
@@ -189,9 +189,9 @@ class Constructions(BaseModel):
|
|
189
189
|
def get_construction(self, entity: entity_instance) -> Construction:
|
190
190
|
construction_id = self._get_construction_id(entity)
|
191
191
|
if construction_id is None:
|
192
|
-
logger.
|
193
|
-
f"Construction ID not found for {entity.GlobalId} ({entity.is_a()}) "
|
194
|
-
f"
|
192
|
+
logger.warning(
|
193
|
+
f"Construction ID not found for {entity.GlobalId} ({entity.is_a()}). "
|
194
|
+
f"Using default construction."
|
195
195
|
)
|
196
196
|
return default_construction
|
197
197
|
constructions = [
|
@@ -210,11 +210,11 @@ class Constructions(BaseModel):
|
|
210
210
|
if association.is_a() == "IfcRelAssociatesMaterial"
|
211
211
|
]
|
212
212
|
if not associates_materials:
|
213
|
-
logger.
|
213
|
+
logger.warning(f"Associate materials not found for {entity.GlobalId}.")
|
214
214
|
return None
|
215
215
|
relating_material = associates_materials[0].RelatingMaterial
|
216
216
|
if relating_material.is_a() == "IfcMaterialList":
|
217
|
-
logger.
|
217
|
+
logger.warning(
|
218
218
|
f"Material list found for {entity.GlobalId}, but no construction ID available."
|
219
219
|
)
|
220
220
|
return None
|
@@ -15,6 +15,7 @@ from trano.utils.utils import is_success # type: ignore
|
|
15
15
|
from ifctrano.base import Libraries
|
16
16
|
from ifctrano.building import Building
|
17
17
|
from ifctrano.exceptions import InvalidLibraryError
|
18
|
+
from rich import print
|
18
19
|
|
19
20
|
app = typer.Typer()
|
20
21
|
CHECKMARK = "[green]✔[/green]"
|
@@ -34,11 +35,11 @@ def create(
|
|
34
35
|
show_space_boundaries: Annotated[
|
35
36
|
bool,
|
36
37
|
typer.Option(help="Show computed space boundaries."),
|
37
|
-
] =
|
38
|
+
] = False,
|
38
39
|
simulate_model: Annotated[
|
39
40
|
bool,
|
40
41
|
typer.Option(help="Simulate the generated model."),
|
41
|
-
] =
|
42
|
+
] = False,
|
42
43
|
) -> None:
|
43
44
|
with Progress(
|
44
45
|
SpinnerColumn(),
|
@@ -58,7 +59,7 @@ def create(
|
|
58
59
|
if show_space_boundaries:
|
59
60
|
print(f"{CHECKMARK} Showing space boundaries.")
|
60
61
|
building.show()
|
61
|
-
modelica_network = building.
|
62
|
+
modelica_network = building.create_network(library=library) # type: ignore
|
62
63
|
progress.update(task, completed=True)
|
63
64
|
task = progress.add_task(description="Writing model to file...", total=None)
|
64
65
|
modelica_model_path.write_text(modelica_network.model())
|
@@ -66,12 +67,16 @@ def create(
|
|
66
67
|
print(f"{CHECKMARK} Model generated at {modelica_model_path}")
|
67
68
|
if simulate_model:
|
68
69
|
print("Simulating...")
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
70
|
+
try:
|
71
|
+
results = simulate(
|
72
|
+
modelica_model_path.parent,
|
73
|
+
building.create_network(
|
74
|
+
library=library # type: ignore
|
75
|
+
), # TODO: cannot use the network after creating he model
|
76
|
+
)
|
77
|
+
except Exception as e:
|
78
|
+
print(f"{CROSS_MARK} Simulation failed: {e}")
|
79
|
+
return
|
75
80
|
if not is_success(results):
|
76
81
|
print(f"{CROSS_MARK} Simulation failed. See logs for more information.")
|
77
82
|
return
|
@@ -79,7 +84,7 @@ def create(
|
|
79
84
|
result_path = (
|
80
85
|
Path(modelica_model_path.parent)
|
81
86
|
/ "results"
|
82
|
-
/ f"{modelica_model_path.stem}.building_res.mat"
|
87
|
+
/ f"{modelica_model_path.stem.lower()}.building_res.mat"
|
83
88
|
)
|
84
89
|
if not result_path.exists():
|
85
90
|
print(
|
@@ -87,7 +92,7 @@ def create(
|
|
87
92
|
)
|
88
93
|
return
|
89
94
|
reporting = ModelDocumentation.from_network(
|
90
|
-
building.
|
95
|
+
building.create_network(library=library), # type: ignore
|
91
96
|
result=ResultFile(path=result_path),
|
92
97
|
)
|
93
98
|
html = to_html_reporting(reporting)
|
@@ -5,7 +5,7 @@ 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
|
8
|
+
from pydantic import Field, BeforeValidator, BaseModel
|
9
9
|
from trano.data_models.conversion import SpaceParameter # type: ignore
|
10
10
|
from trano.elements import Space as TranoSpace, ExternalWall, Window, BaseWall, ExternalDoor # type: ignore
|
11
11
|
from trano.elements.system import Occupancy # type: ignore
|
@@ -23,7 +23,13 @@ from ifctrano.base import (
|
|
23
23
|
)
|
24
24
|
from ifctrano.bounding_box import OrientedBoundingBox
|
25
25
|
from ifctrano.construction import glass, Constructions
|
26
|
-
from ifctrano.
|
26
|
+
from ifctrano.exceptions import HasWindowsWithoutWallsError
|
27
|
+
from ifctrano.utils import (
|
28
|
+
remove_non_alphanumeric,
|
29
|
+
_round,
|
30
|
+
get_building_elements,
|
31
|
+
short_uuid,
|
32
|
+
)
|
27
33
|
|
28
34
|
ROOF_VECTOR = Vector(x=0, y=0, z=1)
|
29
35
|
|
@@ -83,6 +89,87 @@ class Space(GlobalId):
|
|
83
89
|
return f"space_{main_name}{remove_non_alphanumeric(self.entity.GlobalId)}"
|
84
90
|
|
85
91
|
|
92
|
+
class ExternalSpaceBoundaryGroup(BaseModelConfig):
|
93
|
+
constructions: List[BaseWall]
|
94
|
+
azimuth: float
|
95
|
+
tilt: Tilt
|
96
|
+
|
97
|
+
def __hash__(self) -> int:
|
98
|
+
return hash((self.azimuth, self.tilt.value))
|
99
|
+
|
100
|
+
def has_window(self) -> bool:
|
101
|
+
return any(
|
102
|
+
isinstance(construction, Window) for construction in self.constructions
|
103
|
+
)
|
104
|
+
|
105
|
+
def has_external_wall(self) -> bool:
|
106
|
+
return any(
|
107
|
+
isinstance(construction, ExternalWall)
|
108
|
+
for construction in self.constructions
|
109
|
+
)
|
110
|
+
|
111
|
+
|
112
|
+
class ExternalSpaceBoundaryGroups(BaseModelConfig):
|
113
|
+
space_boundary_groups: List[ExternalSpaceBoundaryGroup] = Field(
|
114
|
+
default_factory=list
|
115
|
+
)
|
116
|
+
|
117
|
+
@classmethod
|
118
|
+
def from_external_boundaries(
|
119
|
+
cls, external_boundaries: List[BaseWall]
|
120
|
+
) -> "ExternalSpaceBoundaryGroups":
|
121
|
+
boundary_walls = [
|
122
|
+
ex
|
123
|
+
for ex in external_boundaries
|
124
|
+
if isinstance(ex, (ExternalWall, Window)) and ex.tilt == Tilt.wall
|
125
|
+
]
|
126
|
+
space_boundary_groups = list(
|
127
|
+
{
|
128
|
+
ExternalSpaceBoundaryGroup(
|
129
|
+
constructions=[
|
130
|
+
ex_
|
131
|
+
for ex_ in boundary_walls
|
132
|
+
if ex_.azimuth == ex.azimuth and ex_.tilt == ex.tilt
|
133
|
+
],
|
134
|
+
azimuth=ex.azimuth,
|
135
|
+
tilt=ex.tilt,
|
136
|
+
)
|
137
|
+
for ex in boundary_walls
|
138
|
+
}
|
139
|
+
)
|
140
|
+
return cls(space_boundary_groups=space_boundary_groups)
|
141
|
+
|
142
|
+
def has_windows_without_wall(self) -> bool:
|
143
|
+
return all(
|
144
|
+
not (group.has_window() and not group.has_external_wall())
|
145
|
+
for group in self.space_boundary_groups
|
146
|
+
)
|
147
|
+
|
148
|
+
|
149
|
+
class Azimuths(BaseModel):
|
150
|
+
north: List[float] = [0.0, 360]
|
151
|
+
east: List[float] = [90.0]
|
152
|
+
south: List[float] = [180.0]
|
153
|
+
west: List[float] = [270.0]
|
154
|
+
northeast: List[float] = [45.0]
|
155
|
+
southeast: List[float] = [135.0]
|
156
|
+
southwest: List[float] = [225.0]
|
157
|
+
northwest: List[float] = [315.0]
|
158
|
+
tolerance: float = 22.5
|
159
|
+
|
160
|
+
def get_azimuth(self, value: float) -> float:
|
161
|
+
fields = [field for field in self.model_fields if field not in ["tolerance"]]
|
162
|
+
for field in fields:
|
163
|
+
possibilities = getattr(self, field)
|
164
|
+
for possibility in possibilities:
|
165
|
+
if (
|
166
|
+
value >= possibility - self.tolerance
|
167
|
+
and value <= possibility + self.tolerance
|
168
|
+
):
|
169
|
+
return float(possibilities[0])
|
170
|
+
raise ValueError(f"Value {value} is not within tolerance of any azimuths.")
|
171
|
+
|
172
|
+
|
86
173
|
class SpaceBoundary(BaseModelConfig):
|
87
174
|
bounding_box: OrientedBoundingBox
|
88
175
|
entity: entity_instance
|
@@ -93,7 +180,10 @@ class SpaceBoundary(BaseModelConfig):
|
|
93
180
|
return hash(self.common_surface)
|
94
181
|
|
95
182
|
def boundary_name(self) -> str:
|
96
|
-
return
|
183
|
+
return (
|
184
|
+
f"{remove_non_alphanumeric(self.entity.Name) or self.entity.is_a().lower()}_"
|
185
|
+
f"__{remove_non_alphanumeric(self.entity.GlobalId)}{short_uuid()}"
|
186
|
+
)
|
97
187
|
|
98
188
|
def model_element( # noqa: PLR0911
|
99
189
|
self,
|
@@ -108,7 +198,7 @@ class SpaceBoundary(BaseModelConfig):
|
|
108
198
|
return ExternalWall(
|
109
199
|
name=self.boundary_name(),
|
110
200
|
surface=self.common_surface.area,
|
111
|
-
azimuth=azimuth,
|
201
|
+
azimuth=Azimuths().get_azimuth(azimuth),
|
112
202
|
tilt=Tilt.wall,
|
113
203
|
construction=constructions.get_construction(self.entity),
|
114
204
|
)
|
@@ -116,7 +206,7 @@ class SpaceBoundary(BaseModelConfig):
|
|
116
206
|
return ExternalDoor(
|
117
207
|
name=self.boundary_name(),
|
118
208
|
surface=self.common_surface.area,
|
119
|
-
azimuth=azimuth,
|
209
|
+
azimuth=Azimuths().get_azimuth(azimuth),
|
120
210
|
tilt=Tilt.wall,
|
121
211
|
construction=constructions.get_construction(self.entity),
|
122
212
|
)
|
@@ -124,7 +214,7 @@ class SpaceBoundary(BaseModelConfig):
|
|
124
214
|
return Window(
|
125
215
|
name=self.boundary_name(),
|
126
216
|
surface=self.common_surface.area,
|
127
|
-
azimuth=azimuth,
|
217
|
+
azimuth=Azimuths().get_azimuth(azimuth),
|
128
218
|
tilt=Tilt.wall,
|
129
219
|
construction=glass,
|
130
220
|
)
|
@@ -169,25 +259,6 @@ class SpaceBoundary(BaseModelConfig):
|
|
169
259
|
)
|
170
260
|
|
171
261
|
|
172
|
-
def _reassign_constructions(external_boundaries: List[BaseWall]) -> None:
|
173
|
-
results = {
|
174
|
-
tuple(sorted([ex.name, ex_.name])): (ex, ex_)
|
175
|
-
for ex in external_boundaries
|
176
|
-
for ex_ in external_boundaries
|
177
|
-
if ex.construction.name != ex_.construction.name
|
178
|
-
and ex.azimuth == ex_.azimuth
|
179
|
-
and isinstance(ex, ExternalWall)
|
180
|
-
and isinstance(ex_, ExternalWall)
|
181
|
-
and ex.tilt == Tilt.wall
|
182
|
-
and ex_.tilt == Tilt.wall
|
183
|
-
}
|
184
|
-
if results:
|
185
|
-
for walls in results.values():
|
186
|
-
construction = next(w.construction for w in walls)
|
187
|
-
for w in walls:
|
188
|
-
w.construction = construction.model_copy(deep=True)
|
189
|
-
|
190
|
-
|
191
262
|
class SpaceBoundaries(BaseShow):
|
192
263
|
space: Space
|
193
264
|
boundaries: List[SpaceBoundary] = Field(default_factory=list)
|
@@ -220,8 +291,13 @@ class SpaceBoundaries(BaseShow):
|
|
220
291
|
if boundary_model:
|
221
292
|
external_boundaries.append(boundary_model)
|
222
293
|
|
223
|
-
|
224
|
-
|
294
|
+
external_space_boundaries_group = (
|
295
|
+
ExternalSpaceBoundaryGroups.from_external_boundaries(external_boundaries)
|
296
|
+
)
|
297
|
+
if not external_space_boundaries_group.has_windows_without_wall():
|
298
|
+
raise HasWindowsWithoutWallsError(
|
299
|
+
f"Space {self.space.global_id} has a boundary that has a windows but without walls."
|
300
|
+
)
|
225
301
|
return TranoSpace(
|
226
302
|
name=self.space.space_name(),
|
227
303
|
occupancy=Occupancy(),
|
@@ -258,7 +334,7 @@ class SpaceBoundaries(BaseShow):
|
|
258
334
|
if entity.is_a() not in ["IfcSpace"]
|
259
335
|
}
|
260
336
|
|
261
|
-
for element in elements_:
|
337
|
+
for element in list(elements_):
|
262
338
|
space_boundary = SpaceBoundary.from_space_and_element(
|
263
339
|
space_.bounding_box, element
|
264
340
|
)
|
@@ -1,4 +1,6 @@
|
|
1
|
+
import random
|
1
2
|
import re
|
3
|
+
import string
|
2
4
|
import uuid
|
3
5
|
from typing import get_args
|
4
6
|
|
@@ -13,6 +15,12 @@ def remove_non_alphanumeric(text: str) -> str:
|
|
13
15
|
return re.sub(r"[^a-zA-Z0-9_]", "", text).lower()
|
14
16
|
|
15
17
|
|
18
|
+
def short_uuid() -> str:
|
19
|
+
return "".join(
|
20
|
+
random.choices(string.ascii_letters + string.digits, k=3) # noqa: S311
|
21
|
+
)
|
22
|
+
|
23
|
+
|
16
24
|
def generate_alphanumeric_uuid() -> str:
|
17
25
|
return str(uuid.uuid4().hex).lower()
|
18
26
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "ifctrano"
|
3
|
-
version = "0.
|
3
|
+
version = "0.4.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.
|
14
|
+
trano = "^0.6.0"
|
15
15
|
shapely = "^2.0.7"
|
16
16
|
typer = "^0.12.5"
|
17
17
|
vedo = "^2025.5.3"
|
File without changes
|
File without changes
|
File without changes
|