Spaces:
Sleeping
Sleeping
Update app/construction.py
Browse files- app/construction.py +474 -477
app/construction.py
CHANGED
@@ -1,10 +1,10 @@
|
|
1 |
"""
|
2 |
-
BuildSustain - Construction Module
|
3 |
|
4 |
This module handles the construction assembly functionality of the BuildSustain application,
|
5 |
allowing users to create and manage multi-layer constructions for walls, roofs, and floors.
|
6 |
It integrates with the material library to select materials for each layer and calculates
|
7 |
-
overall thermal properties.
|
8 |
|
9 |
Developed by: Dr Majed Abuseif, Deakin University
|
10 |
漏 2025
|
@@ -18,8 +18,9 @@ import logging
|
|
18 |
import uuid
|
19 |
from typing import Dict, List, Any, Optional, Tuple, Union
|
20 |
|
21 |
-
# Import the centralized data module
|
22 |
from app.m_c_data import get_default_constructions
|
|
|
23 |
|
24 |
# Configure logging
|
25 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
@@ -28,522 +29,520 @@ logger = logging.getLogger(__name__)
|
|
28 |
# Define constants
|
29 |
CONSTRUCTION_TYPES = ["Wall", "Roof", "Floor"]
|
30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
def display_construction_page():
|
32 |
"""
|
33 |
-
Display the construction page.
|
34 |
-
This is the main function called by main.py when the Construction page is selected.
|
35 |
"""
|
36 |
st.title("Construction Library")
|
37 |
st.write("Define multi-layer constructions for walls, roofs, and floors based on ASHRAE 2005 Handbook of Fundamentals.")
|
38 |
-
|
39 |
-
# Display help information
|
40 |
with st.expander("Help & Information"):
|
41 |
display_construction_help()
|
42 |
-
|
43 |
-
#
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
51 |
st.session_state.construction_rerun_pending = False
|
52 |
st.rerun()
|
53 |
-
|
54 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
55 |
col1, col2 = st.columns([3, 2])
|
56 |
-
|
57 |
with col1:
|
58 |
-
|
59 |
-
display_constructions_table()
|
60 |
-
|
61 |
with col2:
|
62 |
-
|
63 |
-
|
64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
65 |
# Navigation buttons
|
66 |
col1, col2 = st.columns(2)
|
67 |
-
|
68 |
with col1:
|
69 |
if st.button("Back to Material Library", key="back_to_materials"):
|
70 |
st.session_state.current_page = "Material Library"
|
71 |
st.rerun()
|
72 |
-
|
73 |
with col2:
|
74 |
if st.button("Continue to Building Components", key="continue_to_components"):
|
75 |
st.session_state.current_page = "Building Components"
|
76 |
st.rerun()
|
77 |
|
78 |
-
def
|
79 |
"""Initialize constructions in session state if not present."""
|
|
|
|
|
80 |
if "constructions" not in st.session_state.project_data:
|
81 |
st.session_state.project_data["constructions"] = {
|
82 |
-
"library":
|
83 |
"project": {}
|
84 |
}
|
85 |
-
|
86 |
-
|
87 |
-
if not st.session_state.project_data["constructions"]["library"]:
|
88 |
-
# Get default constructions from the centralized data module
|
89 |
-
default_constructions = get_default_constructions()
|
90 |
-
st.session_state.project_data["constructions"]["library"] = default_constructions.copy()
|
91 |
-
|
92 |
-
# Initialize construction editor state
|
93 |
-
if "construction_editor" not in st.session_state:
|
94 |
-
st.session_state.construction_editor = {}
|
95 |
-
|
96 |
-
# Initialize construction action state
|
97 |
-
if "construction_action" not in st.session_state:
|
98 |
-
st.session_state.construction_action = {"action": None, "id": None}
|
99 |
|
100 |
-
def
|
101 |
-
"""Display
|
102 |
-
#
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
with col2:
|
113 |
-
type_filter = st.selectbox(
|
114 |
-
"Type",
|
115 |
-
["All"] + CONSTRUCTION_TYPES,
|
116 |
-
key="construction_type_filter"
|
117 |
-
)
|
118 |
-
|
119 |
-
# Get constructions based on filters
|
120 |
-
constructions = {}
|
121 |
-
|
122 |
-
if source_filter in ["All", "Library"]:
|
123 |
-
for name, props in st.session_state.project_data["constructions"]["library"].items():
|
124 |
-
if type_filter == "All" or props["type"] == type_filter:
|
125 |
-
constructions[name] = {**props, "source": "Library"}
|
126 |
-
|
127 |
-
if source_filter in ["All", "Project"]:
|
128 |
-
for name, props in st.session_state.project_data["constructions"]["project"].items():
|
129 |
-
if type_filter == "All" or props["type"] == type_filter:
|
130 |
-
constructions[name] = {**props, "source": "Project"}
|
131 |
-
|
132 |
-
# Display constructions in a table
|
133 |
-
if constructions:
|
134 |
-
# Create column headers
|
135 |
-
cols = st.columns([3, 1, 1, 1, 1, 1, 1])
|
136 |
cols[0].write("**Name**")
|
137 |
-
cols[1].write("**
|
138 |
-
cols[2].write("**U-Value**")
|
139 |
-
cols[3].write("**
|
140 |
-
cols[4].write("**
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
cols
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
159 |
st.session_state.construction_editor = {
|
160 |
-
"name": name,
|
161 |
-
"
|
162 |
-
"layers":
|
163 |
-
"is_edit": True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
164 |
}
|
165 |
-
st.session_state.construction_action = {"action": "edit", "id": str(uuid.uuid4())}
|
166 |
st.session_state.construction_rerun_pending = True
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
delete_key = f"delete_construction_{name}_{idx}"
|
174 |
-
if st.button("Delete", key=delete_key):
|
175 |
-
# Check if construction is in use
|
176 |
-
is_in_use = check_construction_in_use(name)
|
177 |
-
|
178 |
-
if is_in_use:
|
179 |
-
st.error(f"Cannot delete construction '{name}' because it is in use in building components.")
|
180 |
-
else:
|
181 |
-
# Delete construction
|
182 |
-
del st.session_state.project_data["constructions"]["project"][name]
|
183 |
-
st.success(f"Construction '{name}' deleted from your project.")
|
184 |
-
logger.info(f"Deleted construction '{name}' from project")
|
185 |
-
st.session_state.construction_action = {"action": "delete", "id": str(uuid.uuid4())}
|
186 |
-
st.session_state.construction_rerun_pending = True
|
187 |
-
else:
|
188 |
-
st.write("N/A")
|
189 |
-
|
190 |
-
# Display details of selected construction
|
191 |
-
st.subheader("Construction Details")
|
192 |
-
selected_construction = st.selectbox(
|
193 |
-
"Select Construction to View Details",
|
194 |
-
list(constructions.keys()),
|
195 |
-
key="construction_details_selector"
|
196 |
-
)
|
197 |
-
|
198 |
-
if selected_construction:
|
199 |
-
display_construction_details(constructions[selected_construction])
|
200 |
-
|
201 |
-
# Add to project button for library constructions
|
202 |
-
if constructions[selected_construction]["source"] == "Library":
|
203 |
-
if st.button("Add to Project", key="add_to_project"):
|
204 |
-
# Check if construction already exists in project
|
205 |
-
if selected_construction in st.session_state.project_data["constructions"]["project"]:
|
206 |
-
st.warning(f"Construction '{selected_construction}' already exists in your project.")
|
207 |
-
else:
|
208 |
-
# Add to project constructions
|
209 |
-
construction_data = st.session_state.project_data["constructions"]["library"][selected_construction].copy()
|
210 |
-
st.session_state.project_data["constructions"]["project"][selected_construction] = construction_data
|
211 |
-
st.success(f"Construction '{selected_construction}' added to your project.")
|
212 |
-
logger.info(f"Added library construction '{selected_construction}' to project")
|
213 |
-
st.session_state.construction_action = {"action": "add", "id": str(uuid.uuid4())}
|
214 |
st.session_state.construction_rerun_pending = True
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
"""
|
220 |
-
Display detailed information about a construction.
|
221 |
-
|
222 |
-
Args:
|
223 |
-
construction: Construction data dictionary
|
224 |
-
"""
|
225 |
-
# Display basic properties
|
226 |
-
col1, col2, col3 = st.columns(3)
|
227 |
-
|
228 |
-
with col1:
|
229 |
-
st.write(f"**Type:** {construction['type']}")
|
230 |
-
st.write(f"**U-Value:** {construction.get('u_value', 0.0):.3f} W/m虏路K")
|
231 |
-
|
232 |
-
with col2:
|
233 |
-
st.write(f"**R-Value:** {construction.get('r_value', 0.0):.3f} m虏路K/W")
|
234 |
-
st.write(f"**Thermal Mass:** {construction.get('thermal_mass', 0.0):.1f} kJ/m虏路K")
|
235 |
-
|
236 |
-
with col3:
|
237 |
-
st.write(f"**Embodied Carbon:** {construction.get('embodied_carbon', 0.0):.1f} kg CO鈧俥/m虏")
|
238 |
-
st.write(f"**Cost:** ${construction.get('cost', 0.0):.2f}/m虏")
|
239 |
-
|
240 |
-
# Display layers
|
241 |
-
st.write("**Layers (from outside to inside):**")
|
242 |
-
|
243 |
-
# Create a DataFrame for layers
|
244 |
-
layers_data = []
|
245 |
-
for i, layer in enumerate(construction.get("layers", [])):
|
246 |
-
layers_data.append({
|
247 |
-
"Layer": i + 1,
|
248 |
-
"Material": layer.get("material", ""),
|
249 |
-
"Thickness (m)": layer.get("thickness", 0.0)
|
250 |
-
})
|
251 |
-
|
252 |
-
if layers_data:
|
253 |
-
layers_df = pd.DataFrame(layers_data)
|
254 |
-
st.dataframe(layers_df, use_container_width=True, hide_index=True)
|
255 |
-
else:
|
256 |
-
st.info("No layers defined for this construction.")
|
257 |
|
258 |
-
def display_construction_editor():
|
259 |
"""Display the construction editor form."""
|
260 |
-
|
261 |
-
|
262 |
-
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
if layers:
|
291 |
-
for i, layer in enumerate(layers):
|
292 |
-
col1, col2, col3 = st.columns([3, 2, 1])
|
293 |
-
|
294 |
-
with col1:
|
295 |
-
st.write(f"**Layer {i+1}:** {layer.get('material', '')}")
|
296 |
-
|
297 |
-
with col2:
|
298 |
-
st.write(f"**Thickness:** {layer.get('thickness', 0.0):.3f} m")
|
299 |
-
|
300 |
-
with col3:
|
301 |
-
if st.form_submit_button(f"Remove Layer {i+1}"):
|
302 |
-
layers.pop(i)
|
303 |
-
st.session_state.construction_editor["layers"] = layers
|
304 |
-
st.session_state.construction_action = {"action": "update_layers", "id": str(uuid.uuid4())}
|
305 |
-
st.session_state.construction_rerun_pending = True
|
306 |
-
else:
|
307 |
-
st.info("No layers defined. Add layers below.")
|
308 |
-
|
309 |
-
# Add new layer
|
310 |
-
st.subheader("Add New Layer")
|
311 |
-
|
312 |
-
col1, col2 = st.columns(2)
|
313 |
-
|
314 |
-
with col1:
|
315 |
-
new_material = st.selectbox(
|
316 |
-
"Material",
|
317 |
-
list(available_materials.keys()),
|
318 |
-
help="Select a material for the new layer."
|
319 |
)
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
default_thickness = thickness_range.get("default", 0.1)
|
327 |
-
|
328 |
-
new_thickness = st.number_input(
|
329 |
-
"Thickness (m)",
|
330 |
-
min_value=min_thickness,
|
331 |
-
max_value=max_thickness,
|
332 |
-
value=default_thickness,
|
333 |
-
format="%.3f",
|
334 |
-
help=f"Enter the thickness of the layer (between {min_thickness} and {max_thickness} m)."
|
335 |
)
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
|
351 |
-
|
352 |
-
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
|
357 |
-
|
358 |
-
|
359 |
-
|
360 |
-
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
|
367 |
-
|
368 |
-
|
369 |
-
|
370 |
-
|
371 |
-
|
372 |
-
st.session_state.construction_rerun_pending = True
|
373 |
-
|
374 |
-
elif calculate:
|
375 |
-
# Calculate construction properties
|
376 |
-
if not layers:
|
377 |
-
st.error("Cannot calculate properties for a construction with no layers.")
|
378 |
-
else:
|
379 |
-
properties = calculate_construction_properties(layers, available_materials)
|
380 |
-
|
381 |
-
# Update editor state with calculated properties
|
382 |
-
st.session_state.construction_editor = {
|
383 |
-
**editor_state,
|
384 |
-
"u_value": properties["u_value"],
|
385 |
-
"r_value": properties["r_value"],
|
386 |
-
"thermal_mass": properties["thermal_mass"],
|
387 |
-
"embodied_carbon": properties["embodied_carbon"],
|
388 |
-
"cost": properties["cost"]
|
389 |
-
}
|
390 |
-
|
391 |
-
# Display calculated properties
|
392 |
-
st.success("Properties calculated successfully.")
|
393 |
-
st.write(f"**U-Value:** {properties['u_value']:.3f} W/m虏路K")
|
394 |
-
st.write(f"**R-Value:** {properties['r_value']:.3f} m虏路K/W")
|
395 |
-
st.write(f"**Thermal Mass:** {properties['thermal_mass']:.1f} kJ/m虏路K")
|
396 |
-
st.write(f"**Embodied Carbon:** {properties['embodied_carbon']:.1f} kg CO鈧俥/m虏")
|
397 |
-
st.write(f"**Cost:** ${properties['cost']:.2f}/m虏")
|
398 |
-
|
399 |
-
st.session_state.construction_action = {"action": "calculate", "id": str(uuid.uuid4())}
|
400 |
-
st.session_state.construction_rerun_pending = True
|
401 |
-
|
402 |
-
elif submit:
|
403 |
-
# Validate inputs
|
404 |
-
if not name:
|
405 |
-
st.error("Construction name is required.")
|
406 |
-
elif not layers:
|
407 |
-
st.error("At least one layer is required.")
|
408 |
-
else:
|
409 |
-
# Calculate properties if not already calculated
|
410 |
-
if "u_value" not in editor_state:
|
411 |
-
properties = calculate_construction_properties(layers, available_materials)
|
412 |
-
else:
|
413 |
-
properties = {
|
414 |
-
"u_value": editor_state.get("u_value", 0.0),
|
415 |
-
"r_value": editor_state.get("r_value", 0.0),
|
416 |
-
"thermal_mass": editor_state.get("thermal_mass", 0.0),
|
417 |
-
"embodied_carbon": editor_state.get("embodied_carbon", 0.0),
|
418 |
-
"cost": editor_state.get("cost", 0.0)
|
419 |
-
}
|
420 |
-
|
421 |
-
# Create construction data
|
422 |
-
construction_data = {
|
423 |
-
"type": construction_type,
|
424 |
-
"layers": layers,
|
425 |
-
"u_value": properties["u_value"],
|
426 |
-
"r_value": properties["r_value"],
|
427 |
-
"thermal_mass": properties["thermal_mass"],
|
428 |
-
"embodied_carbon": properties["embodied_carbon"],
|
429 |
-
"cost": properties["cost"]
|
430 |
-
}
|
431 |
-
|
432 |
-
# Handle edit mode
|
433 |
-
if is_edit:
|
434 |
-
# Check if name has changed
|
435 |
-
original_name = editor_state.get("name", "")
|
436 |
-
|
437 |
-
if name != original_name and name in st.session_state.project_data["constructions"]["project"]:
|
438 |
-
st.error(f"Construction name '{name}' already exists in your project. Please choose a different name.")
|
439 |
-
else:
|
440 |
-
# If name changed, delete old entry and add new one
|
441 |
-
if name != original_name:
|
442 |
-
del st.session_state.project_data["constructions"]["project"][original_name]
|
443 |
-
|
444 |
-
# Update construction
|
445 |
-
st.session_state.project_data["constructions"]["project"][name] = construction_data
|
446 |
-
st.success(f"Construction '{name}' updated successfully.")
|
447 |
-
logger.info(f"Updated construction '{name}' in project")
|
448 |
-
|
449 |
-
# Clear editor state
|
450 |
-
st.session_state.construction_editor = {}
|
451 |
-
st.session_state.construction_action = {"action": "update", "id": str(uuid.uuid4())}
|
452 |
-
st.session_state.construction_rerun_pending = True
|
453 |
-
else:
|
454 |
-
# Check if name already exists
|
455 |
-
if name in st.session_state.project_data["constructions"]["project"]:
|
456 |
-
st.error(f"Construction name '{name}' already exists in your project. Please choose a different name.")
|
457 |
-
else:
|
458 |
-
# Add new construction
|
459 |
-
st.session_state.project_data["constructions"]["project"][name] = construction_data
|
460 |
-
st.success(f"Construction '{name}' created successfully.")
|
461 |
-
logger.info(f"Created new construction '{name}' in project")
|
462 |
-
|
463 |
-
# Clear editor state
|
464 |
-
st.session_state.construction_editor = {}
|
465 |
-
st.session_state.construction_action = {"action": "create", "id": str(uuid.uuid4())}
|
466 |
-
st.session_state.construction_rerun_pending = True
|
467 |
-
|
468 |
-
elif cancel:
|
469 |
-
# Clear editor state
|
470 |
-
st.session_state.construction_editor = {}
|
471 |
-
st.session_state.construction_action = {"action": "cancel", "id": str(uuid.uuid4())}
|
472 |
-
st.session_state.construction_rerun_pending = True
|
473 |
|
474 |
-
|
475 |
-
|
476 |
-
|
477 |
-
|
478 |
-
|
479 |
-
|
480 |
-
|
481 |
-
|
482 |
-
|
483 |
-
|
484 |
-
|
485 |
-
|
486 |
-
|
487 |
-
|
488 |
-
|
489 |
-
|
490 |
-
|
491 |
-
|
492 |
-
|
493 |
-
|
494 |
-
|
495 |
-
|
496 |
-
|
497 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
498 |
|
499 |
def calculate_construction_properties(layers: List[Dict[str, Any]], materials: Dict[str, Any]) -> Dict[str, float]:
|
500 |
"""
|
501 |
Calculate the thermal properties of a construction based on its layers.
|
502 |
-
|
503 |
-
Args:
|
504 |
-
layers: List of layer dictionaries with material and thickness
|
505 |
-
materials: Dictionary of available materials
|
506 |
-
|
507 |
-
Returns:
|
508 |
-
Dictionary of calculated properties
|
509 |
"""
|
510 |
-
#
|
511 |
-
r_value = 0.0 # m虏路K/W
|
512 |
thermal_mass = 0.0 # kJ/m虏路K
|
513 |
embodied_carbon = 0.0 # kg CO鈧俥/m虏
|
514 |
cost = 0.0 # $/m虏
|
515 |
-
|
516 |
-
# Add surface resistances (inside and outside)
|
517 |
-
r_value += 0.17 # m虏路K/W (typical combined surface resistance)
|
518 |
-
|
519 |
-
# Calculate properties for each layer
|
520 |
for layer in layers:
|
521 |
material_name = layer.get("material", "")
|
522 |
thickness = layer.get("thickness", 0.0)
|
523 |
-
|
524 |
if material_name in materials:
|
525 |
material = materials[material_name]
|
526 |
-
|
527 |
# Thermal resistance
|
528 |
-
if material.
|
529 |
-
r_value += thickness / material.
|
530 |
-
|
531 |
# Thermal mass
|
532 |
-
thermal_mass += material.
|
533 |
-
|
534 |
# Embodied carbon
|
535 |
-
embodied_carbon += material.
|
536 |
-
|
537 |
# Cost
|
538 |
-
|
539 |
-
|
540 |
-
|
541 |
-
else:
|
542 |
-
cost += material_cost * thickness
|
543 |
-
|
544 |
-
# Calculate U-value
|
545 |
u_value = 1.0 / r_value if r_value > 0 else 0.0
|
546 |
-
|
547 |
return {
|
548 |
"u_value": u_value,
|
549 |
"r_value": r_value,
|
@@ -552,27 +551,15 @@ def calculate_construction_properties(layers: List[Dict[str, Any]], materials: D
|
|
552 |
"cost": cost
|
553 |
}
|
554 |
|
555 |
-
def check_construction_in_use(construction_name: str) -> bool:
|
556 |
"""
|
557 |
Check if a construction is in use in any building components.
|
558 |
-
|
559 |
-
Args:
|
560 |
-
construction_name: Name of the construction to check
|
561 |
-
|
562 |
-
Returns:
|
563 |
-
True if the construction is in use, False otherwise
|
564 |
"""
|
565 |
-
# Check if components exist in session state
|
566 |
-
if "components" not in st.session_state.project_data:
|
567 |
-
return False
|
568 |
-
|
569 |
-
# Check walls, roofs, and floors
|
570 |
for comp_type in ["walls", "roofs", "floors"]:
|
571 |
-
if comp_type in
|
572 |
-
for component in
|
573 |
if component.get("construction") == construction_name:
|
574 |
return True
|
575 |
-
|
576 |
return False
|
577 |
|
578 |
def display_construction_help():
|
@@ -606,3 +593,13 @@ def display_construction_help():
|
|
606 |
* Use the Calculate Properties button to evaluate your construction before saving.
|
607 |
* Library constructions cannot be modified, but you can add them to your project and then edit them.
|
608 |
""")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
"""
|
2 |
+
BuildSustain - Construction Module (Updated)
|
3 |
|
4 |
This module handles the construction assembly functionality of the BuildSustain application,
|
5 |
allowing users to create and manage multi-layer constructions for walls, roofs, and floors.
|
6 |
It integrates with the material library to select materials for each layer and calculates
|
7 |
+
overall thermal properties. The structure is adapted from old.txt's Constructions tab.
|
8 |
|
9 |
Developed by: Dr Majed Abuseif, Deakin University
|
10 |
漏 2025
|
|
|
18 |
import uuid
|
19 |
from typing import Dict, List, Any, Optional, Tuple, Union
|
20 |
|
21 |
+
# Import the centralized data module and materials library
|
22 |
from app.m_c_data import get_default_constructions
|
23 |
+
from app.materials_library import get_available_materials, Material
|
24 |
|
25 |
# Configure logging
|
26 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
|
29 |
# Define constants
|
30 |
CONSTRUCTION_TYPES = ["Wall", "Roof", "Floor"]
|
31 |
|
32 |
+
# Surface resistances (EN ISO 6946 or ASHRAE)
|
33 |
+
R_SI = 0.12 # Internal surface resistance (m虏路K/W)
|
34 |
+
R_SE = 0.04 # External surface resistance (m虏路K/W)
|
35 |
+
|
36 |
+
class ConstructionLibrary:
|
37 |
+
def __init__(self):
|
38 |
+
self.library_constructions = {}
|
39 |
+
|
40 |
+
def to_dataframe(self, project_constructions: Dict, only_project: bool = True) -> pd.DataFrame:
|
41 |
+
"""Convert project constructions to a DataFrame."""
|
42 |
+
data = [
|
43 |
+
{
|
44 |
+
"Name": name,
|
45 |
+
"Component Type": props["type"],
|
46 |
+
"U-Value (W/m虏路K)": props.get("u_value", 0.0),
|
47 |
+
"R-Value (m虏路K/W)": props.get("r_value", 0.0),
|
48 |
+
"Thermal Mass (kJ/m虏路K)": props.get("thermal_mass", 0.0),
|
49 |
+
"Embodied Carbon (kgCO鈧俥/m虏)": props.get("embodied_carbon", 0.0),
|
50 |
+
"Cost (USD/m虏)": props.get("cost", 0.0),
|
51 |
+
"Layers": len(props.get("layers", []))
|
52 |
+
}
|
53 |
+
for name, props in project_constructions.items()
|
54 |
+
]
|
55 |
+
return pd.DataFrame(data)
|
56 |
+
|
57 |
+
def add_project_construction(self, construction: Dict, project_constructions: Dict) -> Tuple[bool, str]:
|
58 |
+
"""Add a new construction to the project."""
|
59 |
+
try:
|
60 |
+
if construction["name"] in project_constructions or construction["name"] in self.library_constructions:
|
61 |
+
return False, f"Construction '{construction['name']}' already exists."
|
62 |
+
project_constructions[construction["name"]] = construction
|
63 |
+
return True, f"Construction '{construction['name']}' added successfully!"
|
64 |
+
except Exception as e:
|
65 |
+
return False, f"Error adding construction: {str(e)}"
|
66 |
+
|
67 |
+
def edit_project_construction(self, original_name: str, new_construction: Dict, project_constructions: Dict, components: Dict) -> Tuple[bool, str]:
|
68 |
+
"""Edit an existing project construction."""
|
69 |
+
try:
|
70 |
+
if original_name not in project_constructions:
|
71 |
+
return False, f"Construction '{original_name}' not found."
|
72 |
+
if new_construction["name"] != original_name and (new_construction["name"] in project_constructions or new_construction["name"] in self.library_constructions):
|
73 |
+
return False, f"Construction '{new_construction['name']}' already exists."
|
74 |
+
project_constructions[new_construction["name"]] = new_construction
|
75 |
+
if new_construction["name"] != original_name:
|
76 |
+
del project_constructions[original_name]
|
77 |
+
return True, f"Construction '{new_construction['name']}' updated successfully!"
|
78 |
+
except Exception as e:
|
79 |
+
return False, f"Error editing construction: {str(e)}"
|
80 |
+
|
81 |
+
def delete_project_construction(self, name: str, project_constructions: Dict, components: Dict) -> Tuple[bool, str]:
|
82 |
+
"""Delete a project construction."""
|
83 |
+
try:
|
84 |
+
if name not in project_constructions:
|
85 |
+
return False, f"Construction '{name}' not found."
|
86 |
+
if check_construction_in_use(name, components):
|
87 |
+
return False, f"Construction '{name}' is used in components and cannot be deleted."
|
88 |
+
del project_constructions[name]
|
89 |
+
return True, f"Construction '{name}' deleted successfully!"
|
90 |
+
except Exception as e:
|
91 |
+
return False, f"Error deleting construction: {str(e)}"
|
92 |
+
|
93 |
def display_construction_page():
|
94 |
"""
|
95 |
+
Display the construction page, adapted from old.txt's Constructions tab.
|
|
|
96 |
"""
|
97 |
st.title("Construction Library")
|
98 |
st.write("Define multi-layer constructions for walls, roofs, and floors based on ASHRAE 2005 Handbook of Fundamentals.")
|
99 |
+
|
100 |
+
# Display help information
|
101 |
with st.expander("Help & Information"):
|
102 |
display_construction_help()
|
103 |
+
|
104 |
+
# CSS for box heights, scrolling, and visual appeal
|
105 |
+
st.markdown("""
|
106 |
+
<style>
|
107 |
+
.box-container {
|
108 |
+
border: 1px solid #e0e0e0;
|
109 |
+
border-radius: 8px;
|
110 |
+
padding: 10px;
|
111 |
+
background-color: #f9f9f9;
|
112 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
113 |
+
overflow-y: auto;
|
114 |
+
scrollbar-width: thin;
|
115 |
+
}
|
116 |
+
.library-box {
|
117 |
+
max-height: 250px;
|
118 |
+
}
|
119 |
+
.project-box {
|
120 |
+
max-height: 250px;
|
121 |
+
}
|
122 |
+
.editor-box {
|
123 |
+
min-height: 540px;
|
124 |
+
}
|
125 |
+
.stButton>button {
|
126 |
+
width: 100%;
|
127 |
+
font-size: 12px;
|
128 |
+
padding: 5px;
|
129 |
+
}
|
130 |
+
</style>
|
131 |
+
""", unsafe_allow_html=True)
|
132 |
+
|
133 |
+
# Initialize constructions in session state
|
134 |
+
initialize_construction()
|
135 |
+
|
136 |
+
# Check for rerun trigger
|
137 |
+
if st.session_state.get("construction_rerun_pending", False):
|
138 |
st.session_state.construction_rerun_pending = False
|
139 |
st.rerun()
|
140 |
+
|
141 |
+
# Initialize session state
|
142 |
+
if 'construction_action' not in st.session_state:
|
143 |
+
st.session_state.construction_action = {"action": None, "id": None}
|
144 |
+
if 'construction_rerun_pending' not in st.session_state:
|
145 |
+
st.session_state.construction_rerun_pending = False
|
146 |
+
if 'construction_form_state' not in st.session_state:
|
147 |
+
st.session_state.construction_form_state = {}
|
148 |
+
if 'construction_editor' not in st.session_state:
|
149 |
+
st.session_state.construction_editor = {}
|
150 |
+
|
151 |
+
# Initialize construction library
|
152 |
+
construction_library = ConstructionLibrary()
|
153 |
+
construction_library.library_constructions = st.session_state.project_data["constructions"]["library"]
|
154 |
+
|
155 |
+
# Display constructions content
|
156 |
col1, col2 = st.columns([3, 2])
|
|
|
157 |
with col1:
|
158 |
+
display_constructions_tables(construction_library)
|
|
|
|
|
159 |
with col2:
|
160 |
+
display_construction_editor(construction_library)
|
161 |
+
|
162 |
+
# Display project constructions DataFrame
|
163 |
+
st.subheader("Project Constructions")
|
164 |
+
try:
|
165 |
+
construction_filter = st.session_state.get("construction_filter", "All")
|
166 |
+
construction_df = construction_library.to_dataframe(st.session_state.project_data["constructions"]["project"])
|
167 |
+
if construction_filter != "All":
|
168 |
+
construction_df = construction_df[construction_df["Component Type"] == construction_filter]
|
169 |
+
if not construction_df.empty:
|
170 |
+
st.dataframe(construction_df, use_container_width=True)
|
171 |
+
else:
|
172 |
+
st.write("No project constructions to display.")
|
173 |
+
except Exception as e:
|
174 |
+
st.error(f"Error displaying project constructions: {str(e)}")
|
175 |
+
st.write("No project constructions to display.")
|
176 |
+
|
177 |
# Navigation buttons
|
178 |
col1, col2 = st.columns(2)
|
|
|
179 |
with col1:
|
180 |
if st.button("Back to Material Library", key="back_to_materials"):
|
181 |
st.session_state.current_page = "Material Library"
|
182 |
st.rerun()
|
|
|
183 |
with col2:
|
184 |
if st.button("Continue to Building Components", key="continue_to_components"):
|
185 |
st.session_state.current_page = "Building Components"
|
186 |
st.rerun()
|
187 |
|
188 |
+
def initialize_construction():
|
189 |
"""Initialize constructions in session state if not present."""
|
190 |
+
if "project_data" not in st.session_state:
|
191 |
+
st.session_state.project_data = {}
|
192 |
if "constructions" not in st.session_state.project_data:
|
193 |
st.session_state.project_data["constructions"] = {
|
194 |
+
"library": get_default_constructions(),
|
195 |
"project": {}
|
196 |
}
|
197 |
+
if "components" not in st.session_state.project_data:
|
198 |
+
st.session_state.project_data["components"] = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
199 |
|
200 |
+
def display_constructions_tables(construction_library: ConstructionLibrary):
|
201 |
+
"""Display library and project constructions tables."""
|
202 |
+
# Component type filter
|
203 |
+
filter_options = ["All"] + CONSTRUCTION_TYPES
|
204 |
+
construction_filter = st.selectbox("Filter by Component Type", filter_options, key="construction_filter")
|
205 |
+
|
206 |
+
st.subheader("Library Constructions")
|
207 |
+
with st.container():
|
208 |
+
library_constructions = list(construction_library.library_constructions.values())
|
209 |
+
if construction_filter != "All":
|
210 |
+
library_constructions = [c for c in library_constructions if c["type"] == construction_filter]
|
211 |
+
cols = st.columns([2, 1, 1, 1, 1])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
212 |
cols[0].write("**Name**")
|
213 |
+
cols[1].write("**Thermal Mass**")
|
214 |
+
cols[2].write("**U-Value (W/m虏路K)**")
|
215 |
+
cols[3].write("**Preview**")
|
216 |
+
cols[4].write("**Copy**")
|
217 |
+
for construction in library_constructions:
|
218 |
+
cols = st.columns([2, 1, 1, 1, 1])
|
219 |
+
cols[0].write(construction["name"])
|
220 |
+
cols[1].write(f"{construction.get('thermal_mass', 0.0):.1f}")
|
221 |
+
cols[2].write(f"{construction.get('u_value', 0.0):.3f}")
|
222 |
+
if cols[3].button("Preview", key=f"preview_lib_cons_{construction['name']}"):
|
223 |
+
if st.session_state.get("rerun_trigger") != f"preview_cons_{construction['name']}":
|
224 |
+
st.session_state.rerun_trigger = f"preview_cons_{construction['name']}"
|
225 |
+
st.session_state.construction_editor = {
|
226 |
+
"name": construction["name"],
|
227 |
+
"component_type": construction["type"],
|
228 |
+
"layers": [{"material_name": layer["material"], "thickness": layer["thickness"]} for layer in construction["layers"]],
|
229 |
+
"is_edit": False
|
230 |
+
}
|
231 |
+
st.session_state.construction_form_state = {
|
232 |
+
"name": construction["name"],
|
233 |
+
"component_type": construction["type"],
|
234 |
+
"num_layers": len(construction["layers"]),
|
235 |
+
"layers": [{"material_name": layer["material"], "thickness": layer["thickness"]} for layer in construction["layers"]]
|
236 |
+
}
|
237 |
+
st.session_state.construction_rerun_pending = True
|
238 |
+
if cols[4].button("Copy", key=f"copy_lib_cons_{construction['name']}"):
|
239 |
+
new_name = f"{construction['name']}_Project"
|
240 |
+
counter = 1
|
241 |
+
while new_name in st.session_state.project_data["constructions"]["project"] or new_name in construction_library.library_constructions:
|
242 |
+
new_name = f"{construction['name']}_Project_{counter}"
|
243 |
+
counter += 1
|
244 |
+
new_layers = []
|
245 |
+
material_library = st.session_state.get("material_library", None)
|
246 |
+
if not material_library:
|
247 |
+
from app.materials_library import MaterialLibrary
|
248 |
+
material_library = MaterialLibrary()
|
249 |
+
st.session_state.material_library = material_library
|
250 |
+
for layer in construction["layers"]:
|
251 |
+
material_name = layer["material"]
|
252 |
+
if material_name in st.session_state.project_data["materials"]["library"]:
|
253 |
+
# Copy library material to project
|
254 |
+
library_material = st.session_state.project_data["materials"]["library"][material_name]
|
255 |
+
new_mat_name = f"{material_name}_Project"
|
256 |
+
counter = 1
|
257 |
+
while new_mat_name in st.session_state.project_data["materials"]["project"] or new_mat_name in st.session_state.project_data["materials"]["library"]:
|
258 |
+
new_mat_name = f"{material_name}_Project_{counter}"
|
259 |
+
counter += 1
|
260 |
+
new_material = Material(
|
261 |
+
name=new_mat_name,
|
262 |
+
category=MaterialCategory(library_material["category"]),
|
263 |
+
conductivity=library_material["thermal_conductivity"],
|
264 |
+
density=library_material["density"],
|
265 |
+
specific_heat=library_material["specific_heat"],
|
266 |
+
default_thickness=library_material["thickness_range"]["default"],
|
267 |
+
embodied_carbon=library_material["embodied_carbon"],
|
268 |
+
solar_absorption=library_material.get("solar_absorption", 0.6),
|
269 |
+
price=library_material["cost"]["material"],
|
270 |
+
emissivity=library_material.get("emissivity", 0.9),
|
271 |
+
is_library=False
|
272 |
+
)
|
273 |
+
success, message = material_library.add_project_material(new_material, st.session_state.project_data["materials"]["project"])
|
274 |
+
if not success:
|
275 |
+
st.error(f"Failed to copy material '{material_name}': {message}")
|
276 |
+
break
|
277 |
+
material_name = new_mat_name
|
278 |
+
new_layers.append({"material": material_name, "thickness": layer["thickness"]})
|
279 |
+
else:
|
280 |
+
properties = calculate_construction_properties(new_layers, get_available_materials())
|
281 |
+
new_construction = {
|
282 |
+
"name": new_name,
|
283 |
+
"type": construction["type"],
|
284 |
+
"layers": new_layers,
|
285 |
+
"u_value": properties["u_value"],
|
286 |
+
"r_value": properties["r_value"],
|
287 |
+
"thermal_mass": properties["thermal_mass"],
|
288 |
+
"embodied_carbon": properties["embodied_carbon"],
|
289 |
+
"cost": properties["cost"],
|
290 |
+
"is_library": False
|
291 |
+
}
|
292 |
+
success, message = construction_library.add_project_construction(new_construction, st.session_state.project_data["constructions"]["project"])
|
293 |
+
if success:
|
294 |
+
st.success(message)
|
295 |
+
st.session_state.construction_rerun_pending = True
|
296 |
+
else:
|
297 |
+
st.error(message)
|
298 |
+
|
299 |
+
st.subheader("Project Constructions")
|
300 |
+
with st.container():
|
301 |
+
project_constructions = list(st.session_state.project_data["constructions"]["project"].values())
|
302 |
+
if construction_filter != "All":
|
303 |
+
project_constructions = [c for c in project_constructions if c["type"] == construction_filter]
|
304 |
+
if project_constructions:
|
305 |
+
cols = st.columns([2, 1, 1, 1, 1])
|
306 |
+
cols[0].write("**Name**")
|
307 |
+
cols[1].write("**Thermal Mass**")
|
308 |
+
cols[2].write("**U-Value (W/m虏路K)**")
|
309 |
+
cols[3].write("**Edit**")
|
310 |
+
cols[4].write("**Delete**")
|
311 |
+
for construction in project_constructions:
|
312 |
+
cols = st.columns([2, 1, 1, 1, 1])
|
313 |
+
cols[0].write(construction["name"])
|
314 |
+
cols[1].write(f"{construction.get('thermal_mass', 0.0):.1f}")
|
315 |
+
cols[2].write(f"{construction.get('u_value', 0.0):.3f}")
|
316 |
+
if cols[3].button("Edit", key=f"edit_proj_cons_{construction['name']}"):
|
317 |
+
if st.session_state.get("rerun_trigger") != f"edit_cons_{construction['name']}":
|
318 |
+
st.session_state.rerun_trigger = f"edit_cons_{construction['name']}"
|
319 |
st.session_state.construction_editor = {
|
320 |
+
"name": construction["name"],
|
321 |
+
"component_type": construction["type"],
|
322 |
+
"layers": [{"material_name": layer["material"], "thickness": layer["thickness"]} for layer in construction["layers"]],
|
323 |
+
"is_edit": True,
|
324 |
+
"original_name": construction["name"]
|
325 |
+
}
|
326 |
+
st.session_state.construction_form_state = {
|
327 |
+
"name": construction["name"],
|
328 |
+
"component_type": construction["type"],
|
329 |
+
"num_layers": len(construction["layers"]),
|
330 |
+
"layers": [{"material_name": layer["material"], "thickness": layer["thickness"]} for layer in construction["layers"]]
|
331 |
}
|
|
|
332 |
st.session_state.construction_rerun_pending = True
|
333 |
+
if cols[4].button("Delete", key=f"delete_proj_cons_{construction['name']}"):
|
334 |
+
success, message = construction_library.delete_project_construction(
|
335 |
+
construction["name"], st.session_state.project_data["constructions"]["project"], st.session_state.project_data.get("components", {})
|
336 |
+
)
|
337 |
+
if success:
|
338 |
+
st.success(message)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
339 |
st.session_state.construction_rerun_pending = True
|
340 |
+
else:
|
341 |
+
st.error(message)
|
342 |
+
else:
|
343 |
+
st.write("No project constructions added.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
344 |
|
345 |
+
def display_construction_editor(construction_library: ConstructionLibrary):
|
346 |
"""Display the construction editor form."""
|
347 |
+
is_preview = st.session_state.get("rerun_trigger", "").startswith("preview_cons_")
|
348 |
+
materials = get_available_materials()
|
349 |
+
material_objects = {name: m for name, m in materials.items() if isinstance(m, Material)}
|
350 |
+
if is_preview:
|
351 |
+
material_objects.update(st.session_state.project_data["materials"]["library"])
|
352 |
+
material_names = list(material_objects.keys())
|
353 |
+
|
354 |
+
if not material_names and not is_preview:
|
355 |
+
st.error("No project materials available. Please create materials in the Materials tab first.")
|
356 |
+
if st.button("Go to Materials", key="go_to_materials"):
|
357 |
+
st.session_state.current_page = "Material Library"
|
358 |
+
st.rerun()
|
359 |
+
return
|
360 |
+
|
361 |
+
with st.container():
|
362 |
+
with st.form("construction_editor_form", clear_on_submit=False):
|
363 |
+
editor_state = st.session_state.get("construction_editor", {})
|
364 |
+
form_state = st.session_state.get("construction_form_state", {
|
365 |
+
"name": "",
|
366 |
+
"component_type": "Wall",
|
367 |
+
"num_layers": 1,
|
368 |
+
"layers": [{"material_name": material_names[0] if material_names else "", "thickness": 0.1}]
|
369 |
+
})
|
370 |
+
is_edit = editor_state.get("is_edit", False)
|
371 |
+
original_name = editor_state.get("original_name", "")
|
372 |
+
name = st.text_input(
|
373 |
+
"Construction Name",
|
374 |
+
value=form_state.get("name", editor_state.get("name", "")),
|
375 |
+
help="Unique construction identifier",
|
376 |
+
key="construction_name_input"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
377 |
)
|
378 |
+
component_type = st.selectbox(
|
379 |
+
"Component Type",
|
380 |
+
CONSTRUCTION_TYPES,
|
381 |
+
index=CONSTRUCTION_TYPES.index(form_state.get("component_type", editor_state.get("component_type", "Wall"))),
|
382 |
+
help="Building component type",
|
383 |
+
key="construction_component_type_input"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
384 |
)
|
385 |
+
num_layers = st.number_input(
|
386 |
+
"Number of Layers",
|
387 |
+
min_value=1,
|
388 |
+
max_value=10,
|
389 |
+
value=form_state.get("num_layers", 1),
|
390 |
+
help="Material layer count",
|
391 |
+
key="construction_num_layers_input"
|
392 |
+
)
|
393 |
+
default_material = material_names[0] if material_names else ""
|
394 |
+
layers = form_state.get("layers", [{"material_name": default_material, "thickness": 0.1}])
|
395 |
+
if len(layers) < num_layers:
|
396 |
+
layers.extend([{"material_name": default_material, "thickness": 0.1} for _ in range(num_layers - len(layers))])
|
397 |
+
elif len(layers) > num_layers:
|
398 |
+
layers = layers[:num_layers]
|
399 |
+
form_state["layers"] = layers
|
400 |
+
form_state["num_layers"] = num_layers
|
401 |
+
for i in range(num_layers):
|
402 |
+
st.write(f"Layer {i + 1} (Exterior to Interior)")
|
403 |
+
layer = layers[i]
|
404 |
+
material_name = st.selectbox(
|
405 |
+
f"Material {i+1}",
|
406 |
+
material_names,
|
407 |
+
index=material_names.index(layer.get("material_name", default_material)) if layer.get("material_name", default_material) in material_names else 0,
|
408 |
+
key=f"cons_mat_{i}_{num_layers}",
|
409 |
+
help="Layer material"
|
410 |
+
)
|
411 |
+
thickness = st.number_input(
|
412 |
+
f"Thickness {i+1} (m)",
|
413 |
+
min_value=0.001,
|
414 |
+
value=layer.get("thickness", 0.1),
|
415 |
+
key=f"cons_thick_{i}_{num_layers}",
|
416 |
+
help="Layer thickness"
|
417 |
+
)
|
418 |
+
layers[i] = {"material_name": material_name, "thickness": thickness}
|
419 |
+
form_state["layers"] = layers
|
420 |
+
st.session_state.construction_form_state = form_state
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
421 |
|
422 |
+
col1, col2, col3 = st.columns(3)
|
423 |
+
with col1:
|
424 |
+
if st.form_submit_button("Update Layers"):
|
425 |
+
action_id = str(uuid.uuid4())
|
426 |
+
if st.session_state.construction_action.get("id") != action_id:
|
427 |
+
st.session_state.construction_action = {"action": "update_layers", "id": action_id}
|
428 |
+
new_layers = []
|
429 |
+
for i in range(num_layers):
|
430 |
+
if i < len(layers) and layers[i].get("material_name") in material_names:
|
431 |
+
new_layers.append(layers[i])
|
432 |
+
else:
|
433 |
+
new_layers.append({"material_name": default_material, "thickness": 0.1})
|
434 |
+
form_state["layers"] = new_layers
|
435 |
+
form_state["num_layers"] = num_layers
|
436 |
+
st.session_state.construction_form_state = form_state
|
437 |
+
st.session_state.construction_action = {"action": None, "id": None}
|
438 |
+
st.session_state.construction_rerun_pending = True
|
439 |
+
with col2:
|
440 |
+
if st.form_submit_button("Preview U-Value"):
|
441 |
+
action_id = str(uuid.uuid4())
|
442 |
+
if st.session_state.construction_action.get("id") != action_id:
|
443 |
+
st.session_state.construction_action = {"action": "preview_uvalue", "id": action_id}
|
444 |
+
if layers:
|
445 |
+
valid_layers = []
|
446 |
+
for layer in layers:
|
447 |
+
material_name = layer.get("material_name")
|
448 |
+
thickness = layer.get("thickness", 0.1)
|
449 |
+
if material_name in material_objects:
|
450 |
+
material = material_objects[material_name]
|
451 |
+
valid_layers.append({"material": material, "thickness": thickness})
|
452 |
+
if valid_layers:
|
453 |
+
r_total = R_SI + sum(layer["thickness"] / layer["material"].conductivity for layer in valid_layers) + R_SE
|
454 |
+
u_value = 1.0 / r_total if r_total > 0 else 0.1
|
455 |
+
st.write(f"Calculated U-Value: {u_value:.3f} W/m虏路K")
|
456 |
+
else:
|
457 |
+
st.warning("No valid materials selected for U-Value calculation.")
|
458 |
+
else:
|
459 |
+
st.warning("No layers defined to calculate U-Value.")
|
460 |
+
st.session_state.construction_action = {"action": None, "id": None}
|
461 |
+
with col3:
|
462 |
+
if st.form_submit_button("Save Construction"):
|
463 |
+
action_id = str(uuid.uuid4())
|
464 |
+
if st.session_state.construction_action.get("id") != action_id:
|
465 |
+
st.session_state.construction_action = {"action": "save", "id": action_id}
|
466 |
+
if not name or not name.strip():
|
467 |
+
st.error("Construction name cannot be empty.")
|
468 |
+
elif (name in st.session_state.project_data["constructions"]["project"] or name in construction_library.library_constructions) and (not is_edit or name != original_name):
|
469 |
+
st.error(f"Construction '{name}' already exists.")
|
470 |
+
elif not layers or any(not layer.get("material_name") for layer in layers):
|
471 |
+
st.error("All layers must have a valid material.")
|
472 |
+
else:
|
473 |
+
try:
|
474 |
+
valid_layers = []
|
475 |
+
for layer in layers:
|
476 |
+
material_name = layer.get("material_name")
|
477 |
+
thickness = layer.get("thickness", 0.1)
|
478 |
+
if material_name in material_names:
|
479 |
+
valid_layers.append({"material": material_name, "thickness": thickness})
|
480 |
+
else:
|
481 |
+
st.error(f"Material '{material_name}' not found.")
|
482 |
+
break
|
483 |
+
else:
|
484 |
+
properties = calculate_construction_properties(valid_layers, material_objects)
|
485 |
+
new_construction = {
|
486 |
+
"name": name,
|
487 |
+
"type": component_type,
|
488 |
+
"layers": valid_layers,
|
489 |
+
"u_value": properties["u_value"],
|
490 |
+
"r_value": properties["r_value"],
|
491 |
+
"thermal_mass": properties["thermal_mass"],
|
492 |
+
"embodied_carbon": properties["embodied_carbon"],
|
493 |
+
"cost": properties["cost"],
|
494 |
+
"is_library": False
|
495 |
+
}
|
496 |
+
if is_edit and name == original_name:
|
497 |
+
success, message = construction_library.edit_project_construction(
|
498 |
+
original_name, new_construction, st.session_state.project_data["constructions"]["project"], st.session_state.project_data.get("components", {})
|
499 |
+
)
|
500 |
+
else:
|
501 |
+
success, message = construction_library.add_project_construction(new_construction, st.session_state.project_data["constructions"]["project"])
|
502 |
+
if success:
|
503 |
+
st.success(message)
|
504 |
+
st.session_state.construction_editor = {}
|
505 |
+
st.session_state.construction_form_state = {
|
506 |
+
"name": "",
|
507 |
+
"component_type": "Wall",
|
508 |
+
"num_layers": 1,
|
509 |
+
"layers": [{"material_name": default_material, "thickness": 0.1}]
|
510 |
+
}
|
511 |
+
st.session_state.construction_action = {"action": None, "id": None}
|
512 |
+
st.session_state.rerun_trigger = None
|
513 |
+
st.session_state.construction_rerun_pending = True
|
514 |
+
else:
|
515 |
+
st.error(f"Failed to save construction: {message}")
|
516 |
+
except Exception as e:
|
517 |
+
st.error(f"Error saving construction: {str(e)}")
|
518 |
|
519 |
def calculate_construction_properties(layers: List[Dict[str, Any]], materials: Dict[str, Any]) -> Dict[str, float]:
|
520 |
"""
|
521 |
Calculate the thermal properties of a construction based on its layers.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
522 |
"""
|
523 |
+
r_value = R_SI # Internal surface resistance
|
|
|
524 |
thermal_mass = 0.0 # kJ/m虏路K
|
525 |
embodied_carbon = 0.0 # kg CO鈧俥/m虏
|
526 |
cost = 0.0 # $/m虏
|
527 |
+
|
|
|
|
|
|
|
|
|
528 |
for layer in layers:
|
529 |
material_name = layer.get("material", "")
|
530 |
thickness = layer.get("thickness", 0.0)
|
|
|
531 |
if material_name in materials:
|
532 |
material = materials[material_name]
|
|
|
533 |
# Thermal resistance
|
534 |
+
if material.conductivity > 0:
|
535 |
+
r_value += thickness / material.conductivity
|
|
|
536 |
# Thermal mass
|
537 |
+
thermal_mass += material.density * material.specific_heat * thickness / 1000.0
|
|
|
538 |
# Embodied carbon
|
539 |
+
embodied_carbon += material.embodied_carbon * material.density * thickness
|
|
|
540 |
# Cost
|
541 |
+
cost += material.price * thickness
|
542 |
+
|
543 |
+
r_value += R_SE # External surface resistance
|
|
|
|
|
|
|
|
|
544 |
u_value = 1.0 / r_value if r_value > 0 else 0.0
|
545 |
+
|
546 |
return {
|
547 |
"u_value": u_value,
|
548 |
"r_value": r_value,
|
|
|
551 |
"cost": cost
|
552 |
}
|
553 |
|
554 |
+
def check_construction_in_use(construction_name: str, components: Dict) -> bool:
|
555 |
"""
|
556 |
Check if a construction is in use in any building components.
|
|
|
|
|
|
|
|
|
|
|
|
|
557 |
"""
|
|
|
|
|
|
|
|
|
|
|
558 |
for comp_type in ["walls", "roofs", "floors"]:
|
559 |
+
if comp_type in components:
|
560 |
+
for component in components[comp_type]:
|
561 |
if component.get("construction") == construction_name:
|
562 |
return True
|
|
|
563 |
return False
|
564 |
|
565 |
def display_construction_help():
|
|
|
593 |
* Use the Calculate Properties button to evaluate your construction before saving.
|
594 |
* Library constructions cannot be modified, but you can add them to your project and then edit them.
|
595 |
""")
|
596 |
+
|
597 |
+
def get_available_constructions() -> Dict[str, Any]:
|
598 |
+
"""
|
599 |
+
Get all available constructions (library + project) for use in other modules.
|
600 |
+
"""
|
601 |
+
constructions = {}
|
602 |
+
if "constructions" in st.session_state.project_data:
|
603 |
+
constructions.update(st.session_state.project_data["constructions"]["library"])
|
604 |
+
constructions.update(st.session_state.project_data["constructions"]["project"])
|
605 |
+
return constructions
|