Spaces:
Sleeping
Sleeping
| """ | |
| BuildSustain - Construction Module | |
| This module handles the construction assembly functionality of the BuildSustain application, | |
| allowing users to create and manage multi-layer constructions for walls, roofs, and floors. | |
| It integrates with the material library to select materials for each layer and calculates | |
| overall thermal properties. | |
| Developed by: Dr Majed Abuseif, Deakin University | |
| © 2025 | |
| """ | |
| import streamlit as st | |
| import pandas as pd | |
| import numpy as np | |
| import json | |
| import logging | |
| import uuid | |
| from typing import Dict, List, Any, Optional, Tuple, Union | |
| # Import the centralized data module and materials library | |
| from app.m_c_data import get_default_constructions | |
| from app.materials_library import get_available_materials, Material, MaterialCategory | |
| from app.m_c_data import SAMPLE_MATERIALS, SAMPLE_FENESTRATIONS, DEFAULT_MATERIAL_PROPERTIES, DEFAULT_WINDOW_PROPERTIES | |
| # Configure logging | |
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') | |
| logger = logging.getLogger(__name__) | |
| # Surface resistances (EN ISO 6946 or ASHRAE) | |
| R_SI = 0.12 # Internal surface resistance (m²·K/W) | |
| R_SE = 0.04 # External surface resistance (m²·K/W) | |
| class ConstructionLibrary: | |
| def __init__(self): | |
| self.library_constructions = {} | |
| def to_dataframe(self, project_constructions: Dict, only_project: bool = True) -> pd.DataFrame: | |
| """Convert project constructions to a DataFrame.""" | |
| data = [ | |
| { | |
| "Name": name, | |
| "U-Value (W/m²·K)": props.get("u_value", 0.0), | |
| "R-Value (m²·K/W)": props.get("r_value", 0.0), | |
| "Thermal Mass Category": props.get("thermal_mass_category", "Low"), | |
| "Embodied Carbon (kgCO₂e/m²)": props.get("embodied_carbon", 0.0), | |
| "Cost (USD/m²)": props.get("cost", 0.0), | |
| "Layers": len(props.get("layers", [])) | |
| } | |
| for name, props in project_constructions.items() | |
| ] | |
| return pd.DataFrame(data) | |
| def add_project_construction(self, construction: Dict, project_constructions: Dict) -> Tuple[bool, str]: | |
| """Add a new construction to the project.""" | |
| try: | |
| if construction["name"] in project_constructions or construction["name"] in self.library_constructions: | |
| return False, f"Construction '{construction['name']}' already exists." | |
| project_constructions[construction["name"]] = construction | |
| return True, f"Construction '{construction['name']}' added successfully!" | |
| except Exception as e: | |
| return False, f"Error adding construction: {str(e)}" | |
| def edit_project_construction(self, original_name: str, new_construction: Dict, project_constructions: Dict, components: Dict) -> Tuple[bool, str]: | |
| """Edit an existing project construction.""" | |
| try: | |
| if original_name not in project_constructions: | |
| return False, f"Construction '{original_name}' not found." | |
| if new_construction["name"] != original_name and (new_construction["name"] in project_constructions or new_construction["name"] in self.library_constructions): | |
| return False, f"Construction '{new_construction['name']}' already exists." | |
| project_constructions[new_construction["name"]] = new_construction | |
| if new_construction["name"] != original_name: | |
| del project_constructions[original_name] | |
| return True, f"Construction '{new_construction['name']}' updated successfully!" | |
| except Exception as e: | |
| return False, f"Error editing construction: {str(e)}" | |
| def delete_project_construction(self, name: str, project_constructions: Dict, components: Dict) -> Tuple[bool, str]: | |
| """Delete a project construction.""" | |
| try: | |
| if name not in project_constructions: | |
| return False, f"Construction '{name}' not found." | |
| if check_construction_in_use(name, components): | |
| return False, f"Construction '{name}' is used in components and cannot be deleted." | |
| del project_constructions[name] | |
| return True, f"Construction '{name}' deleted successfully!" | |
| except Exception as e: | |
| return False, f"Error deleting construction: {str(e)}" | |
| def display_construction_page(): | |
| """ | |
| Display the construction page, adapted from old.txt's Constructions tab. | |
| """ | |
| st.title("Construction Library") | |
| st.write("Define multi-layer constructions for walls, roofs, and floors based on ASHRAE 2005 Handbook of Fundamentals.") | |
| # Display help information | |
| with st.expander("Help & Information"): | |
| display_construction_help() | |
| # CSS for box heights, scrolling, and visual appeal | |
| st.markdown(""" | |
| <style> | |
| .box-container { | |
| border: 1px solid #e0e0e0; | |
| border-radius: 8px; | |
| padding: 10px; | |
| background-color: #f9f9f9; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| overflow-y: auto; | |
| scrollbar-width: thin; | |
| } | |
| .library-box { | |
| max-height: 250px; | |
| } | |
| .project-box { | |
| max-height: 250px; | |
| } | |
| .editor-box { | |
| min-height: 540px; | |
| } | |
| .stButton>button { | |
| width: 100%; | |
| font-size: 12px; | |
| padding: 5px; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # Initialize constructions in session state | |
| initialize_construction() | |
| # Check for rerun trigger | |
| if st.session_state.get("construction_rerun_pending", False): | |
| st.session_state.construction_rerun_pending = False | |
| st.rerun() | |
| # Initialize session state | |
| if 'construction_action' not in st.session_state: | |
| st.session_state.construction_action = {"action": None, "id": None} | |
| if 'construction_rerun_pending' not in st.session_state: | |
| st.session_state.construction_rerun_pending = False | |
| if 'construction_form_state' not in st.session_state: | |
| st.session_state.construction_form_state = {} | |
| if 'construction_editor' not in st.session_state: | |
| st.session_state.construction_editor = {} | |
| # Initialize construction library | |
| construction_library = ConstructionLibrary() | |
| construction_library.library_constructions = st.session_state.project_data["constructions"]["library"] | |
| # Display constructions content | |
| col1, col2 = st.columns([3, 2]) | |
| with col1: | |
| display_constructions_tables(construction_library) | |
| with col2: | |
| display_construction_editor(construction_library) | |
| # Display project constructions DataFrame | |
| st.subheader("Project Constructions") | |
| try: | |
| construction_df = construction_library.to_dataframe(st.session_state.project_data["constructions"]["project"]) | |
| if not construction_df.empty: | |
| st.dataframe(construction_df, use_container_width=True) | |
| else: | |
| st.write("No project constructions to display.") | |
| except Exception as e: | |
| st.error(f"Error displaying project constructions: {str(e)}") | |
| st.write("No project constructions to display.") | |
| # Navigation buttons | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| if st.button("Back to Material Library", key="back_to_materials"): | |
| st.session_state.current_page = "Material Library" | |
| st.rerun() | |
| with col2: | |
| if st.button("Continue to Building Components", key="continue_to_components"): | |
| st.session_state.current_page = "Building Components" | |
| st.rerun() | |
| def initialize_construction(): | |
| """Initialize constructions in session state if not present.""" | |
| if "project_data" not in st.session_state: | |
| st.session_state.project_data = {} | |
| if "constructions" not in st.session_state.project_data: | |
| st.session_state.project_data["constructions"] = { | |
| "library": get_default_constructions(), | |
| "project": {} | |
| } | |
| if "components" not in st.session_state.project_data: | |
| st.session_state.project_data["components"] = {} | |
| if "rerun_trigger" not in st.session_state: | |
| st.session_state.rerun_trigger = "" | |
| def display_constructions_tables(construction_library: ConstructionLibrary): | |
| """Display library and project constructions tables.""" | |
| st.subheader("Library Constructions") | |
| with st.container(): | |
| # Prepare material objects for library materials | |
| material_objects = {name: m for name, m in get_available_materials().items() if isinstance(m, Material)} | |
| for name, mat_dict in st.session_state.project_data["materials"]["library"].items(): | |
| if name not in material_objects: | |
| if "thermal_properties" not in mat_dict: | |
| st.error(f"Material '{name}' is missing thermal properties") | |
| logger.error(f"Missing thermal properties for material: {name}") | |
| continue | |
| category_str = mat_dict["category"].upper().replace("-", "_") | |
| if category_str not in MaterialCategory.__members__: | |
| st.error(f"Invalid category for material {name}: {mat_dict['category']}") | |
| logger.error(f"Invalid category for material {name}: {mat_dict['category']}") | |
| continue | |
| try: | |
| material_objects[name] = Material( | |
| name=name, | |
| category=MaterialCategory[category_str], | |
| conductivity=mat_dict["thermal_properties"]["conductivity"], | |
| density=mat_dict["thermal_properties"]["density"], | |
| specific_heat=mat_dict["thermal_properties"]["specific_heat"], | |
| default_thickness=mat_dict["thickness_range"]["default"], | |
| embodied_carbon=mat_dict["embodied_carbon"], | |
| absorptivity=mat_dict.get("absorptivity", 0.6), | |
| price=mat_dict["cost"]["material"], | |
| emissivity=mat_dict.get("emissivity", 0.9), | |
| is_library=True | |
| ) | |
| except Exception as e: | |
| st.error(f"Failed to convert material '{name}' to Material object: {str(e)}") | |
| logger.error(f"Error converting material {name}: {str(e)}") | |
| continue | |
| # Recalculate properties for library constructions | |
| display_constructions = {} | |
| for name, construction in construction_library.library_constructions.items(): | |
| try: | |
| layers = [{"material": layer["material"], "thickness": layer["thickness"]} for layer in construction["layers"]] | |
| properties = calculate_construction_properties(layers, material_objects) | |
| display_constructions[name] = { | |
| **construction, | |
| "u_value": properties["u_value"], | |
| "r_value": properties["r_value"], | |
| "thermal_mass": properties["thermal_mass"], | |
| "thermal_mass_category": properties["thermal_mass_category"], | |
| "embodied_carbon": properties["embodied_carbon"], | |
| "cost": properties["cost"] | |
| } | |
| logger.debug( | |
| f"Library construction {name}: Static U={construction.get('u_value', 0.0):.3f}, " | |
| f"Calculated U={properties['u_value']:.3f}, " | |
| f"Static Thermal Mass Category={construction.get('thermal_mass_category', 'Low')}, " | |
| f"Calculated Thermal Mass={properties['thermal_mass']:.1f} ({properties['thermal_mass_category']})" | |
| ) | |
| except Exception as e: | |
| st.error(f"Error calculating properties for library construction '{name}': {str(e)}") | |
| logger.error(f"Error calculating properties for {name}: {str(e)}") | |
| display_constructions[name] = construction # Fallback to original data | |
| # Display library constructions | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write("**Name**") | |
| cols[1].write("**Thermal Mass Category**") | |
| cols[2].write("**U-Value (W/m²·K)**") | |
| cols[3].write("**Copy**") | |
| cols[4].write("**Preview**") | |
| for name, construction in display_constructions.items(): | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write(name) | |
| cols[1].write(construction.get("thermal_mass_category", "Low")) | |
| cols[2].write(f"{construction.get('u_value', 0.0):.3f}") | |
| if cols[3].button("Copy", key=f"copy_lib_cons_{name}"): | |
| new_name = f"{name}_Project" | |
| counter = 1 | |
| while new_name in st.session_state.project_data["constructions"]["project"] or new_name in construction_library.library_constructions: | |
| new_name = f"{name}_Project_{counter}" | |
| counter += 1 | |
| new_layers = [] | |
| material_library = st.session_state.get("material_library", None) | |
| if not material_library: | |
| from app.materials_library import MaterialLibrary | |
| material_library = MaterialLibrary() | |
| st.session_state.material_library = material_library | |
| for layer in construction["layers"]: | |
| material_name = layer["material"] | |
| if material_name in st.session_state.project_data["materials"]["library"]: | |
| # Copy library material to project | |
| library_material = st.session_state.project_data["materials"]["library"][material_name] | |
| if "thermal_properties" not in library_material: | |
| st.error(f"Material '{material_name}' is missing thermal properties") | |
| logger.error(f"Missing thermal properties for material: {material_name}") | |
| break | |
| new_mat_name = f"{material_name}_Project" | |
| counter = 1 | |
| while new_mat_name in st.session_state.project_data["materials"]["project"] or new_mat_name in st.session_state.project_data["materials"]["library"]: | |
| new_mat_name = f"{material_name}_Project_{counter}" | |
| counter += 1 | |
| category_str = library_material["category"].upper().replace("-", "_") | |
| if category_str not in MaterialCategory.__members__: | |
| st.error(f"Invalid category for material {material_name}: {library_material['category']}") | |
| break | |
| try: | |
| new_material = Material( | |
| name=new_mat_name, | |
| category=MaterialCategory[category_str], | |
| conductivity=library_material["thermal_properties"]["conductivity"], | |
| density=library_material["thermal_properties"]["density"], | |
| specific_heat=library_material["thermal_properties"]["specific_heat"], | |
| default_thickness=library_material["thickness_range"]["default"], | |
| embodied_carbon=library_material["embodied_carbon"], | |
| absorptivity=library_material.get("absorptivity", 0.6), | |
| price=library_material["cost"]["material"], | |
| emissivity=library_material.get("emissivity", 0.9), | |
| is_library=False | |
| ) | |
| except Exception as e: | |
| st.error(f"Failed to create material '{new_mat_name}': {str(e)}") | |
| logger.error(f"Error creating material {new_mat_name}: {str(e)}") | |
| break | |
| success, message = material_library.add_project_material(new_material, st.session_state.project_data["materials"]["project"]) | |
| if not success: | |
| st.error(f"Failed to copy material '{material_name}': {message}") | |
| break | |
| material_name = new_mat_name | |
| new_layers.append({"material": material_name, "thickness": layer["thickness"]}) | |
| else: | |
| properties = calculate_construction_properties(new_layers, get_available_materials()) | |
| new_construction = { | |
| "name": new_name, | |
| "layers": new_layers, | |
| "u_value": properties["u_value"], | |
| "r_value": properties["r_value"], | |
| "thermal_mass": properties["thermal_mass"], | |
| "thermal_mass_category": properties["thermal_mass_category"], | |
| "embodied_carbon": properties["embodied_carbon"], | |
| "cost": properties["cost"], | |
| "is_library": False | |
| } | |
| success, message = construction_library.add_project_construction(new_construction, st.session_state.project_data["constructions"]["project"]) | |
| if success: | |
| st.success(message) | |
| st.session_state.construction_rerun_pending = True | |
| else: | |
| st.error(message) | |
| if cols[4].button("Preview", key=f"preview_lib_cons_{name}"): | |
| if st.session_state.get("rerun_trigger") != f"preview_cons_{name}": | |
| st.session_state.rerun_trigger = f"preview_cons_{name}" | |
| st.session_state.construction_editor = { | |
| "name": name, | |
| "layers": [{"material_name": layer["material"], "thickness": layer["thickness"]} for layer in construction["layers"]], | |
| "is_edit": False | |
| } | |
| st.session_state.construction_form_state = { | |
| "name": name, | |
| "num_layers": len(construction["layers"]), | |
| "layers": [{"material_name": layer["material"], "thickness": layer["thickness"]} for layer in construction["layers"]] | |
| } | |
| st.session_state.construction_rerun_pending = True | |
| st.subheader("Project Constructions") | |
| with st.container(): | |
| project_constructions = list(st.session_state.project_data["constructions"]["project"].items()) | |
| if project_constructions: | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write("**Name**") | |
| cols[1].write("**Thermal Mass Category**") | |
| cols[2].write("**U-Value (W/m²·K)**") | |
| cols[3].write("**Edit**") | |
| cols[4].write("**Delete**") | |
| for name, construction in project_constructions: | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write(name) | |
| cols[1].write(construction.get("thermal_mass_category", "Low")) | |
| cols[2].write(f"{construction.get('u_value', 0.0):.3f}") | |
| if cols[3].button("Edit", key=f"edit_proj_cons_{name}"): | |
| if st.session_state.get("rerun_trigger") != f"edit_cons_{name}": | |
| st.session_state.rerun_trigger = f"edit_cons_{name}" | |
| st.session_state.construction_editor = { | |
| "name": name, | |
| "layers": [{"material_name": layer["material"], "thickness": layer["thickness"]} for layer in construction["layers"]], | |
| "is_edit": True, | |
| "original_name": name | |
| } | |
| st.session_state.construction_form_state = { | |
| "name": name, | |
| "num_layers": len(construction["layers"]), | |
| "layers": [{"material_name": layer["material"], "thickness": layer["thickness"]} for layer in construction["layers"]] | |
| } | |
| st.session_state.construction_rerun_pending = True | |
| if cols[4].button("Delete", key=f"delete_proj_cons_{name}"): | |
| success, message = construction_library.delete_project_construction( | |
| name, st.session_state.project_data["constructions"]["project"], st.session_state.project_data.get("components", {}) | |
| ) | |
| if success: | |
| st.success(message) | |
| st.session_state.construction_rerun_pending = True | |
| else: | |
| st.error(message) | |
| else: | |
| st.write("No project constructions added.") | |
| def display_construction_editor(construction_library: ConstructionLibrary): | |
| """Display the construction editor form.""" | |
| is_preview = st.session_state.get("rerun_trigger", "") and st.session_state.get("rerun_trigger", "").startswith("preview_cons_") | |
| materials = get_available_materials() | |
| material_objects = {name: m for name, m in materials.items() if isinstance(m, Material)} | |
| if is_preview: | |
| for name, mat_dict in st.session_state.project_data["materials"]["library"].items(): | |
| if name not in material_objects: | |
| if "thermal_properties" not in mat_dict: | |
| st.error(f"Material '{name}' is missing thermal properties") | |
| logger.error(f"Missing thermal properties for material: {name}") | |
| continue | |
| category_str = mat_dict["category"].upper().replace("-", "_") | |
| if category_str not in MaterialCategory.__members__: | |
| st.error(f"Invalid category for material {name}: {mat_dict['category']}") | |
| logger.error(f"Invalid category for material {name}: {mat_dict['category']}") | |
| continue | |
| try: | |
| material_objects[name] = Material( | |
| name=name, | |
| category=MaterialCategory[category_str], | |
| conductivity=mat_dict["thermal_properties"]["conductivity"], | |
| density=mat_dict["thermal_properties"]["density"], | |
| specific_heat=mat_dict["thermal_properties"]["specific_heat"], | |
| default_thickness=mat_dict["thickness_range"]["default"], | |
| embodied_carbon=mat_dict["embodied_carbon"], | |
| absorptivity=mat_dict.get("absorptivity", 0.6), | |
| price=mat_dict["cost"]["material"], | |
| emissivity=mat_dict.get("emissivity", 0.9), | |
| is_library=True | |
| ) | |
| except Exception as e: | |
| st.error(f"Failed to convert material '{name}' to Material object: {str(e)}") | |
| logger.error(f"Error converting material {name}: {str(e)}") | |
| continue | |
| material_names = list(material_objects.keys()) | |
| if not material_names and not is_preview: | |
| st.error("No project materials available. Please create materials in the Materials tab first.") | |
| if st.button("Go to Materials", key="go_to_materials"): | |
| st.session_state.current_page = "Material Library" | |
| st.rerun() | |
| return | |
| with st.container(): | |
| with st.form("construction_editor_form", clear_on_submit=False): | |
| editor_state = st.session_state.get("construction_editor", {}) | |
| form_state = st.session_state.get("construction_form_state", {}) | |
| if not form_state or not isinstance(form_state, dict) or not form_state.get("layers"): | |
| form_state = { | |
| "name": "", | |
| "num_layers": 1, | |
| "layers": [{"material_name": material_names[0] if material_names else "", "thickness": 0.1}] | |
| } | |
| st.session_state.construction_form_state = form_state | |
| logger.debug(f"Form state: {form_state}") | |
| is_edit = editor_state.get("is_edit", False) | |
| original_name = editor_state.get("original_name", "") | |
| name = st.text_input( | |
| "Construction Name", | |
| value=form_state.get("name", editor_state.get("name", "")), | |
| help="Unique construction identifier", | |
| key="construction_name_input" | |
| ) | |
| num_layers = st.number_input( | |
| "Number of Layers", | |
| min_value=1, | |
| max_value=10, | |
| value=form_state.get("num_layers", 1), | |
| help="Material layer count", | |
| key="construction_num_layers_input" | |
| ) | |
| default_material = material_names[0] if material_names else "" | |
| layers = form_state.get("layers", [{"material_name": default_material, "thickness": 0.1}]) | |
| if len(layers) < num_layers: | |
| layers.extend([{"material_name": default_material, "thickness": 0.1} for _ in range(num_layers - len(layers))]) | |
| elif len(layers) > num_layers: | |
| layers = layers[:num_layers] | |
| form_state["layers"] = layers | |
| form_state["num_layers"] = num_layers | |
| for i in range(num_layers): | |
| st.write(f"Layer {i + 1} (Exterior to Interior)") | |
| layer = layers[i] | |
| material_name = st.selectbox( | |
| f"Material {i+1}", | |
| material_names, | |
| index=material_names.index(layer.get("material_name", default_material)) if layer.get("material_name", default_material) in material_names else 0, | |
| key=f"cons_mat_{i}_{num_layers}", | |
| help="Layer material" | |
| ) | |
| thickness = st.number_input( | |
| f"Thickness {i+1} (m)", | |
| min_value=0.001, | |
| value=layer.get("thickness", 0.1), | |
| key=f"cons_thick_{i}_{num_layers}", | |
| help="Layer thickness" | |
| ) | |
| layers[i] = {"material_name": material_name, "thickness": thickness} | |
| form_state["layers"] = layers | |
| st.session_state.construction_form_state = form_state | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| if st.form_submit_button("Update Layers"): | |
| action_id = str(uuid.uuid4()) | |
| if st.session_state.construction_action.get("id") != action_id: | |
| st.session_state.construction_action = {"action": "update_layers", "id": action_id} | |
| new_layers = [] | |
| for i in range(num_layers): | |
| if i < len(layers) and layers[i].get("material_name") in material_names: | |
| new_layers.append(layers[i]) | |
| else: | |
| new_layers.append({"material_name": default_material, "thickness": 0.1}) | |
| form_state["layers"] = new_layers | |
| form_state["num_layers"] = num_layers | |
| st.session_state.construction_form_state = form_state | |
| st.session_state.construction_action = {"action": None, "id": None} | |
| st.session_state.construction_rerun_pending = True | |
| with col2: | |
| if st.form_submit_button("Preview U-Value"): | |
| action_id = str(uuid.uuid4()) | |
| if st.session_state.construction_action.get("id") != action_id: | |
| st.session_state.construction_action = {"action": "preview_uvalue", "id": action_id} | |
| if layers: | |
| valid_layers = [] | |
| for layer in layers: | |
| material_name = layer.get("material_name") | |
| thickness = layer.get("thickness", 0.1) | |
| if material_name in material_objects: | |
| valid_layers.append({"material": material_objects[material_name], "thickness": thickness}) | |
| else: | |
| st.warning(f"Material '{material_name}' not found in material objects.") | |
| if valid_layers: | |
| try: | |
| r_total = R_SI | |
| for layer in valid_layers: | |
| if layer["material"].conductivity > 0: | |
| r_total += layer["thickness"] / layer["material"].conductivity | |
| else: | |
| logger.warning(f"Invalid conductivity for material {layer['material'].name}: {layer['material'].conductivity}") | |
| r_total += R_SE | |
| u_value = 1.0 / r_total if r_total > 0 else 0.1 | |
| st.write(f"Calculated U-Value: {u_value:.3f} W/m²·K") | |
| except Exception as e: | |
| st.error(f"Error calculating U-Value: {str(e)}") | |
| logger.error(f"U-Value calculation error: {str(e)}") | |
| else: | |
| st.warning("No valid materials selected for U-Value calculation.") | |
| else: | |
| st.warning("No layers defined to calculate U-Value.") | |
| st.session_state.construction_action = {"action": None, "id": None} | |
| with col3: | |
| if st.form_submit_button("Save Construction"): | |
| action_id = str(uuid.uuid4()) | |
| if st.session_state.construction_action.get("id") != action_id: | |
| st.session_state.construction_action = {"action": "save", "id": action_id} | |
| if not name or not name.strip(): | |
| st.error("Construction name cannot be empty.") | |
| 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): | |
| st.error(f"Construction '{name}' already exists.") | |
| elif not layers or any(not layer.get("material_name") for layer in layers): | |
| st.error("All layers must have a valid material.") | |
| else: | |
| try: | |
| valid_layers = [] | |
| for layer in layers: | |
| material_name = layer.get("material_name") | |
| thickness = layer.get("thickness", 0.1) | |
| if material_name in material_names: | |
| valid_layers.append({"material": material_name, "thickness": thickness}) | |
| else: | |
| st.error(f"Material '{material_name}' not found.") | |
| break | |
| else: | |
| properties = calculate_construction_properties(valid_layers, material_objects) | |
| new_construction = { | |
| "name": name, | |
| "layers": valid_layers, | |
| "u_value": properties["u_value"], | |
| "r_value": properties["r_value"], | |
| "thermal_mass": properties["thermal_mass"], | |
| "thermal_mass_category": properties["thermal_mass_category"], | |
| "embodied_carbon": properties["embodied_carbon"], | |
| "cost": properties["cost"], | |
| "is_library": False | |
| } | |
| if is_edit and name == original_name: | |
| success, message = construction_library.edit_project_construction( | |
| original_name, new_construction, st.session_state.project_data["constructions"]["project"], st.session_state.project_data.get("components", {}) | |
| ) | |
| else: | |
| success, message = construction_library.add_project_construction(new_construction, st.session_state.project_data["constructions"]["project"]) | |
| if success: | |
| st.success(message) | |
| st.session_state.construction_editor = {} | |
| st.session_state.construction_form_state = { | |
| "name": "", | |
| "num_layers": 1, | |
| "layers": [{"material_name": default_material, "thickness": 0.1}] | |
| } | |
| st.session_state.construction_action = {"action": None, "id": None} | |
| st.session_state.rerun_trigger = "" | |
| st.session_state.construction_rerun_pending = True | |
| else: | |
| st.error(f"Failed to save construction: {message}") | |
| except Exception as e: | |
| st.error(f"Error saving construction: {str(e)}") | |
| logger.error(f"Construction save error: {str(e)}") | |
| def calculate_construction_properties(layers: List[Dict[str, Any]], materials: Dict[str, Any]) -> Dict[str, float]: | |
| """ | |
| Calculate the thermal properties of a construction based on its layers. | |
| """ | |
| r_value = R_SI # Internal surface resistance | |
| thermal_mass = 0.0 # J/m²·K | |
| embodied_carbon = 0.0 # kg CO₂e/m² | |
| cost = 0.0 # $/m² | |
| for layer in layers: | |
| material_name = layer.get("material", "") | |
| thickness = layer.get("thickness", 0.0) | |
| if material_name in materials: | |
| material = materials[material_name] | |
| # Thermal resistance | |
| if material.conductivity > 0: | |
| r_value += thickness / material.conductivity | |
| else: | |
| logger.warning(f"Invalid conductivity for material {material_name}: {material.conductivity}") | |
| # Thermal mass | |
| thermal_mass += material.density * material.specific_heat * thickness | |
| # Embodied carbon | |
| embodied_carbon += material.embodied_carbon * material.density * thickness | |
| # Cost | |
| cost += material.price * thickness | |
| else: | |
| logger.warning(f"Material {material_name} not found in materials dictionary") | |
| r_value += R_SE # External surface resistance | |
| u_value = 1.0 / r_value if r_value > 0 else 0.0 | |
| # Categorize thermal mass | |
| if thermal_mass < 30000: | |
| thermal_mass_category = "Low" | |
| elif 30000 <= thermal_mass <= 90000: | |
| thermal_mass_category = "Medium" | |
| else: | |
| thermal_mass_category = "High" | |
| logger.debug(f"Calculated properties: U={u_value:.3f}, R={r_value:.3f}, Thermal Mass={thermal_mass:.1f}, Category={thermal_mass_category}") | |
| return { | |
| "u_value": u_value, | |
| "r_value": r_value, | |
| "thermal_mass": thermal_mass, | |
| "thermal_mass_category": thermal_mass_category, | |
| "embodied_carbon": embodied_carbon, | |
| "cost": cost | |
| } | |
| def check_construction_in_use(construction_name: str, components: Dict) -> bool: | |
| """ | |
| Check if a construction is in use in any building components. | |
| """ | |
| for comp_type in ["walls", "roofs", "floors"]: | |
| if comp_type in components: | |
| for component in components[comp_type]: | |
| if component.get("construction") == construction_name: | |
| return True | |
| return False | |
| def display_construction_help(): | |
| """ | |
| Display help information for the construction page. | |
| """ | |
| st.markdown(""" | |
| ### Construction Library Help | |
| This section allows you to create and manage multi-layer constructions for building envelope components. | |
| **Key Concepts:** | |
| * **Construction**: A multi-layer assembly of materials used for building envelope components. | |
| * **Layers**: Individual material layers that make up a construction, defined from outside to inside. | |
| * **U-Value**: Overall heat transfer coefficient (W/m²·K), lower is better for insulation. | |
| * **R-Value**: Thermal resistance (m²·K/W), higher is better for insulation. | |
| * **Thermal Mass Category**: Heat storage capacity, categorized as Low (< 30,000 J/m²·K), Medium (30,000–90,000 J/m²·K), or High (> 90,000 J/m²·K). | |
| **Workflow:** | |
| 1. Create new constructions or add existing ones from the library to your project. | |
| 2. Define layers for each construction, selecting materials and specifying thicknesses. | |
| 3. Calculate thermal properties to evaluate performance. | |
| 4. Use constructions in the Building Components section to define building elements. | |
| **Tips:** | |
| * Start with the outermost layer (exterior) and work inward when defining layers. | |
| * Consider both thermal resistance and thermal mass for optimal performance. | |
| * Use the Calculate Properties button to evaluate your construction before saving. | |
| * Library constructions cannot be modified, but you can add them to your project and then edit them. | |
| """) | |
| def get_available_constructions() -> Dict[str, Any]: | |
| """ | |
| Get all available constructions (library + project) for use in other modules. | |
| """ | |
| constructions = {} | |
| if "constructions" in st.session_state.project_data: | |
| constructions.update(st.session_state.project_data["constructions"]["library"]) | |
| constructions.update(st.session_state.project_data["constructions"]["project"]) | |
| return constructions |