Spaces:
Sleeping
Sleeping
| """ | |
| BuildSustain - Material Library Module | |
| This module handles the material library functionality of the BuildSustain application, | |
| allowing users to manage building materials and fenestrations (windows, skylights). | |
| It provides both predefined library materials and the ability to create project-specific materials. | |
| 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 | |
| from enum import Enum | |
| # Import default data and constants from centralized module | |
| 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__) | |
| # Define constants | |
| MATERIAL_CATEGORIES = [ | |
| "Insulation", | |
| "Structural", | |
| "Finishing", | |
| "Sub-Structural" | |
| ] | |
| # Surface resistances (EN ISO 6946 or ASHRAE standards) | |
| R_SI = 0.12 # Internal surface resistance (m虏路K/W) | |
| R_SE = 0.04 # External surface resistance (m虏路K/W) | |
| class MaterialCategory(Enum): | |
| INSULATION = "Insulation" | |
| STRUCTURAL = "Structural" | |
| FINISHING = "Finishing" | |
| SUB_STRUCTURAL = "Sub-Structural" | |
| class Material: | |
| def __init__(self, name: str, category: MaterialCategory, conductivity: float, density: float, | |
| specific_heat: float, default_thickness: float, embodied_carbon: float, | |
| absorptivity: float, price: float, emissivity: float, is_library: bool = True): | |
| self.name = name | |
| self.category = category | |
| self.conductivity = conductivity | |
| self.density = density | |
| self.specific_heat = specific_heat | |
| self.default_thickness = default_thickness | |
| self.embodied_carbon = embodied_carbon | |
| self.absorptivity = absorptivity | |
| self.price = price | |
| self.emissivity = emissivity | |
| self.is_library = is_library | |
| def get_thermal_mass(self) -> str: | |
| # Calculate areal heat capacity: 蟻 * cp * d (J/m虏路K) | |
| thermal_mass = self.density * self.specific_heat * self.default_thickness | |
| # Categorize based on thresholds | |
| if thermal_mass < 30000: | |
| return "Low" | |
| elif 30000 <= thermal_mass <= 90000: | |
| return "Medium" | |
| else: | |
| return "High" | |
| def get_u_value(self) -> float: | |
| # Calculate U-value: U = 1 / (R_si + (d / 位) + R_se) (W/m虏路K) | |
| if self.default_thickness > 0 and self.conductivity > 0: | |
| r_value = R_SI + (self.default_thickness / self.conductivity) + R_SE | |
| return 1.0 / r_value | |
| return 0.0 | |
| class GlazingMaterial: | |
| def __init__(self, name: str, shgc: float, u_value: float, h_o: float, | |
| visible_transmittance: float, embodied_carbon: float, price: float, | |
| is_library: bool = True): | |
| self.name = name | |
| self.shgc = shgc | |
| self.u_value = u_value | |
| self.h_o = h_o | |
| self.visible_transmittance = visible_transmittance | |
| self.embodied_carbon = embodied_carbon | |
| self.price = price | |
| self.is_library = is_library | |
| class MaterialLibrary: | |
| def __init__(self): | |
| self.library_materials = {} | |
| self.library_glazing_materials = {} | |
| def to_dataframe(self, material_type: str, **kwargs): | |
| if material_type == "materials": | |
| project_materials = kwargs.get("project_materials", {}) | |
| data = [ | |
| { | |
| "Name": m.name, | |
| "Category": m.category.value, | |
| "U-Value (W/m虏路K)": m.get_u_value(), | |
| "Density (kg/m鲁)": m.density, | |
| "Specific Heat (J/kg路K)": m.specific_heat, | |
| "Default Thickness (m)": m.default_thickness, | |
| "Embodied Carbon (kgCO鈧俥/kg)": m.embodied_carbon, | |
| "Absorptivity": m.absorptivity, | |
| "Price (USD/m虏)": m.price, | |
| "Emissivity": m.emissivity, | |
| "Thermal Mass Category": m.get_thermal_mass() | |
| } | |
| for m in project_materials.values() | |
| ] | |
| return pd.DataFrame(data) | |
| elif material_type == "glazing": | |
| project_glazing_materials = kwargs.get("project_glazing_materials", {}) | |
| data = [ | |
| { | |
| "Name": g.name, | |
| "SHGC": g.shgc, | |
| "U-Value (W/m虏路K)": g.u_value, | |
| "Exterior Conductance (W/m虏路K)": g.h_o, | |
| "Visible Transmittance": g.visible_transmittance, | |
| "Embodied Carbon (kgCO鈧俥/m虏)": g.embodied_carbon, | |
| "Price (USD/m虏)": g.price | |
| } | |
| for g in project_glazing_materials.values() | |
| ] | |
| return pd.DataFrame(data) | |
| return pd.DataFrame() | |
| def add_project_material(self, material: Material, project_materials: Dict): | |
| try: | |
| if material.name in project_materials or material.name in self.library_materials: | |
| return False, f"Material '{material.name}' already exists." | |
| project_materials[material.name] = material | |
| return True, f"Material '{material.name}' added successfully!" | |
| except Exception as e: | |
| return False, f"Error adding material: {str(e)}" | |
| def edit_project_material(self, original_name: str, new_material: Material, project_materials: Dict, components: Dict): | |
| try: | |
| if original_name not in project_materials: | |
| return False, f"Material '{original_name}' not found." | |
| if new_material.name != original_name and (new_material.name in project_materials or new_material.name in self.library_materials): | |
| return False, f"Material '{new_material.name}' already exists." | |
| project_materials[new_material.name] = new_material | |
| if new_material.name != original_name: | |
| del project_materials[original_name] | |
| return True, f"Material '{new_material.name}' updated successfully!" | |
| except Exception as e: | |
| return False, f"Error editing material: {str(e)}" | |
| def delete_project_material(self, name: str, project_materials: Dict, components: Dict): | |
| try: | |
| if name not in project_materials: | |
| return False, f"Material '{name}' not found." | |
| del project_materials[name] | |
| return True, f"Material '{name}' deleted successfully!" | |
| except Exception as e: | |
| return False, f"Error deleting material: {str(e)}" | |
| def display_materials_page(): | |
| """ | |
| Display the material library page. | |
| This is the main function called by main.py when the Material Library page is selected. | |
| """ | |
| st.title("Material Library") | |
| st.write("Manage building materials and fenestrations for thermal analysis.") | |
| # 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 materials and fenestrations in session state if not present | |
| initialize_materials_and_fenestrations() | |
| # Check for rerun trigger | |
| if st.session_state.get("materials_rerun_pending", False): | |
| st.session_state.materials_rerun_pending = False | |
| st.rerun() | |
| # Initialize session state | |
| if 'active_tab' not in st.session_state: | |
| st.session_state.active_tab = "Materials" | |
| if 'material_action' not in st.session_state: | |
| st.session_state.material_action = {"action": None, "id": None} | |
| if 'fenestration_action' not in st.session_state: | |
| st.session_state.fenestration_action = {"action": None, "id": None} | |
| if 'materials_rerun_pending' not in st.session_state: | |
| st.session_state.materials_rerun_pending = False | |
| if 'material_form_state' not in st.session_state: | |
| st.session_state.material_form_state = {} | |
| if 'fenestration_form_state' not in st.session_state: | |
| st.session_state.fenestration_form_state = {} | |
| # Create tabs and set active tab | |
| tab1, tab2 = st.tabs(["Materials", "Fenestrations"]) | |
| active_tab = st.session_state.active_tab | |
| with tab1: | |
| if active_tab == "Materials": | |
| st.session_state.active_tab = "Materials" | |
| with tab2: | |
| if active_tab == "Fenestrations": | |
| st.session_state.active_tab = "Fenestrations" | |
| # Initialize material library | |
| material_library = MaterialLibrary() | |
| material_library.library_materials = {} | |
| for k, m in st.session_state.project_data["materials"]["library"].items(): | |
| try: | |
| category_str = m["category"].upper().replace("-", "_") | |
| if category_str not in MaterialCategory.__members__: | |
| logger.error(f"Invalid category for material {k}: {category_str}") | |
| continue | |
| material_library.library_materials[k] = Material( | |
| name=k, | |
| category=MaterialCategory[category_str], | |
| conductivity=m["thermal_properties"]["conductivity"], | |
| density=m["thermal_properties"]["density"], | |
| specific_heat=m["thermal_properties"]["specific_heat"], | |
| default_thickness=m["thickness_range"]["default"], | |
| embodied_carbon=m["embodied_carbon"], | |
| absorptivity=m.get("absorptivity", DEFAULT_MATERIAL_PROPERTIES["absorptivity"]), | |
| price=m["cost"]["material"], | |
| emissivity=m.get("emissivity", DEFAULT_MATERIAL_PROPERTIES["emissivity"]), | |
| is_library=True | |
| ) | |
| logger.debug(f"Loaded material: {k}, Category: {m['category']}") | |
| except (KeyError, ValueError) as e: | |
| logger.error(f"Error processing material {k}: {str(e)}") | |
| continue | |
| material_library.library_glazing_materials = {} | |
| for k, f in st.session_state.project_data["fenestrations"]["library"].items(): | |
| try: | |
| material_library.library_glazing_materials[k] = GlazingMaterial( | |
| name=k, | |
| shgc=f["performance"]["shgc"], | |
| u_value=f["performance"]["u_value"], | |
| h_o=f.get("h_o", DEFAULT_WINDOW_PROPERTIES["h_o"]), | |
| visible_transmittance=f["performance"].get("visible_transmittance", 0.7), | |
| embodied_carbon=f.get("embodied_carbon", 25.0), | |
| price=f["cost"].get("material", 100.0), | |
| is_library=True | |
| ) | |
| logger.debug(f"Loaded fenestration: {k}, Type: {f['type']}") | |
| except KeyError as e: | |
| logger.error(f"Error processing fenestration {k}: Missing key {e}") | |
| continue | |
| with tab1: | |
| display_materials_tab(material_library) | |
| with tab2: | |
| display_fenestrations_tab(material_library) | |
| # Navigation buttons | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| if st.button("Back to Climate Data", key="material_back_to_climate"): | |
| st.session_state.current_page = "Climate Data" | |
| st.rerun() | |
| with col2: | |
| if st.button("Continue to Construction", key="material_to_components"): | |
| st.session_state.current_page = "Construction" | |
| st.rerun() | |
| def initialize_materials_and_fenestrations(): | |
| """Initialize materials and fenestrations in session state if not present.""" | |
| if "project_data" not in st.session_state: | |
| st.session_state.project_data = {} | |
| if "materials" not in st.session_state.project_data: | |
| st.session_state.project_data["materials"] = { | |
| "library": dict(SAMPLE_MATERIALS), | |
| "project": {} | |
| } | |
| logger.info(f"Initialized materials in session state with {len(SAMPLE_MATERIALS)} materials") | |
| if "fenestrations" not in st.session_state.project_data: | |
| st.session_state.project_data["fenestrations"] = { | |
| "library": dict(SAMPLE_FENESTRATIONS), | |
| "project": {} | |
| } | |
| logger.info(f"Initialized fenestrations in session state with {len(SAMPLE_FENESTRATIONS)} fenestrations") | |
| def display_materials_tab(material_library: MaterialLibrary): | |
| """Display the materials tab content with two-column layout.""" | |
| col1, col2 = st.columns([3, 2]) | |
| with col1: | |
| st.subheader("Materials") | |
| # Category filter | |
| filter_options = ["All", "None"] + MATERIAL_CATEGORIES | |
| category = st.selectbox("Filter by Category", filter_options, key="material_filter") | |
| st.subheader("Library Materials") | |
| with st.container(): | |
| library_materials = list(material_library.library_materials.values()) | |
| if not library_materials: | |
| st.warning("No library materials loaded. Check data initialization.") | |
| # Fallback: Display raw material names from session state | |
| raw_materials = st.session_state.project_data["materials"]["library"] | |
| if raw_materials: | |
| st.write("Raw material names available in session state:") | |
| st.write(list(raw_materials.keys())) | |
| if category == "None": | |
| library_materials = [] | |
| elif category != "All": | |
| library_materials = [m for m in library_materials if m.category.value == category] | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write("**Name**") | |
| cols[1].write("**Thermal Mass**") | |
| cols[2].write("**U-Value (W/m虏路K)**") | |
| cols[3].write("**Preview**") | |
| cols[4].write("**Clone**") | |
| for material in library_materials: | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write(material.name) | |
| cols[1].write(material.get_thermal_mass()) | |
| cols[2].write(f"{material.get_u_value():.3f}") | |
| if cols[3].button("Preview", key=f"preview_lib_mat_{material.name}"): | |
| if st.session_state.get("rerun_trigger") != f"preview_mat_{material.name}": | |
| st.session_state.rerun_trigger = f"preview_mat_{material.name}" | |
| st.session_state.material_editor = { | |
| "name": material.name, | |
| "category": material.category.value, | |
| "conductivity": material.conductivity, | |
| "density": material.density, | |
| "specific_heat": material.specific_heat, | |
| "default_thickness": material.default_thickness, | |
| "embodied_carbon": material.embodied_carbon, | |
| "absorptivity": material.absorptivity, | |
| "price": material.price, | |
| "emissivity": material.emissivity, | |
| "is_edit": False, | |
| "edit_source": "library" | |
| } | |
| st.session_state.material_form_state = { | |
| "name": material.name, | |
| "category": material.category.value, | |
| "conductivity": material.conductivity, | |
| "density": material.density, | |
| "specific_heat": material.specific_heat, | |
| "default_thickness": material.default_thickness, | |
| "embodied_carbon": material.embodied_carbon, | |
| "absorptivity": material.absorptivity, | |
| "price": material.price, | |
| "emissivity": material.emissivity | |
| } | |
| st.session_state.active_tab = "Materials" | |
| if cols[4].button("Clone", key=f"copy_lib_mat_{material.name}"): | |
| new_name = f"{material.name}_Project" | |
| counter = 1 | |
| while new_name in st.session_state.project_data["materials"]["project"] or new_name in st.session_state.project_data["materials"]["library"]: | |
| new_name = f"{material.name}_Project_{counter}" | |
| counter += 1 | |
| new_material = Material( | |
| name=new_name, | |
| category=material.category, | |
| conductivity=material.conductivity, | |
| density=material.density, | |
| specific_heat=material.specific_heat, | |
| default_thickness=material.default_thickness, | |
| embodied_carbon=material.embodied_carbon, | |
| absorptivity=material.absorptivity, | |
| price=material.price, | |
| emissivity=material.emissivity, | |
| is_library=False | |
| ) | |
| success, message = material_library.add_project_material(new_material, st.session_state.project_data["materials"]["project"]) | |
| if success: | |
| st.success(message) | |
| st.session_state.materials_rerun_pending = True | |
| else: | |
| st.error(message) | |
| st.subheader("Project Materials") | |
| with st.container(): | |
| if st.session_state.project_data["materials"]["project"]: | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write("**Name**") | |
| cols[1].write("**Thermal Mass**") | |
| cols[2].write("**U-Value (W/m虏路K)**") | |
| cols[3].write("**Edit**") | |
| cols[4].write("**Delete**") | |
| for material in st.session_state.project_data["materials"]["project"].values(): | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write(material.name) | |
| cols[1].write(material.get_thermal_mass()) | |
| cols[2].write(f"{material.get_u_value():.3f}") | |
| if cols[3].button("Edit", key=f"edit_mat_{material.name}"): | |
| if st.session_state.get("rerun_trigger") != f"edit_mat_{material.name}": | |
| st.session_state.rerun_trigger = f"edit_mat_{material.name}" | |
| st.session_state.material_editor = { | |
| "name": material.name, | |
| "category": material.category.value, | |
| "conductivity": material.conductivity, | |
| "density": material.density, | |
| "specific_heat": material.specific_heat, | |
| "default_thickness": material.default_thickness, | |
| "embodied_carbon": material.embodied_carbon, | |
| "absorptivity": material.absorptivity, | |
| "price": material.price, | |
| "emissivity": material.emissivity, | |
| "is_edit": True, | |
| "edit_source": "project", | |
| "original_name": material.name | |
| } | |
| st.session_state.material_form_state = { | |
| "name": material.name, | |
| "category": material.category.value, | |
| "conductivity": material.conductivity, | |
| "density": material.density, | |
| "specific_heat": material.specific_heat, | |
| "default_thickness": material.default_thickness, | |
| "embodied_carbon": material.embodied_carbon, | |
| "absorptivity": material.absorptivity, | |
| "price": material.price, | |
| "emissivity": material.emissivity | |
| } | |
| st.session_state.active_tab = "Materials" | |
| if cols[4].button("Delete", key=f"delete_mat_{material.name}"): | |
| success, message = material_library.delete_project_material( | |
| material.name, st.session_state.project_data["materials"]["project"], st.session_state.get("components", {}) | |
| ) | |
| if success: | |
| st.success(message) | |
| st.session_state.materials_rerun_pending = True | |
| else: | |
| st.error(message) | |
| else: | |
| st.write("No project materials added.") | |
| st.subheader("Project Materials") | |
| try: | |
| material_df = material_library.to_dataframe("materials", project_materials=st.session_state.project_data["materials"]["project"]) | |
| if not material_df.empty: | |
| st.dataframe(material_df, use_container_width=True) | |
| else: | |
| st.write("No project materials to display.") | |
| except Exception as e: | |
| st.error(f"Error displaying project materials: {str(e)}") | |
| st.write("No project materials to display.") | |
| with col2: | |
| st.subheader("Material Editor/Creator") | |
| with st.container(): | |
| with st.form("material_editor_form", clear_on_submit=False): | |
| editor_state = st.session_state.get("material_editor", {}) | |
| form_state = st.session_state.get("material_form_state", { | |
| "name": "", | |
| "category": "Insulation", | |
| "conductivity": 0.1, | |
| "density": 1000.0, | |
| "specific_heat": 1000.0, | |
| "default_thickness": 0.1, | |
| "embodied_carbon": 0.5, | |
| "absorptivity": DEFAULT_MATERIAL_PROPERTIES["absorptivity"], | |
| "price": 50.0, | |
| "emissivity": DEFAULT_MATERIAL_PROPERTIES["emissivity"] | |
| }) | |
| is_edit = editor_state.get("is_edit", False) | |
| original_name = editor_state.get("original_name", "") | |
| name = st.text_input( | |
| "Material Name", | |
| value=form_state.get("name", editor_state.get("name", "")), | |
| help="Unique material identifier", | |
| key="material_name_input" | |
| ) | |
| filter_category = st.session_state.get("material_filter", "All") | |
| default_category = (filter_category if filter_category in MATERIAL_CATEGORIES | |
| else editor_state.get("category", "Insulation")) | |
| category_index = (MATERIAL_CATEGORIES.index(default_category) | |
| if default_category in MATERIAL_CATEGORIES else 0) | |
| category = st.selectbox( | |
| "Category", | |
| MATERIAL_CATEGORIES, | |
| index=category_index, | |
| help="Material type classification", | |
| key="material_category_input" | |
| ) | |
| density = st.number_input( | |
| "Density (kg/m鲁)", | |
| min_value=1.0, | |
| value=form_state.get("density", editor_state.get("density", 1000.0)), | |
| help="Mass per volume", | |
| key="material_density_input" | |
| ) | |
| specific_heat = st.number_input( | |
| "Specific Heat (J/kg路K)", | |
| min_value=100.0, | |
| value=form_state.get("specific_heat", editor_state.get("specific_heat", 1000.0)), | |
| help="Heat storage capacity", | |
| key="material_specific_heat_input" | |
| ) | |
| default_thickness = st.number_input( | |
| "Default Thickness (m)", | |
| min_value=0.001, | |
| value=form_state.get("default_thickness", editor_state.get("default_thickness", 0.1)), | |
| help="Typical layer thickness", | |
| key="material_default_thickness_input" | |
| ) | |
| embodied_carbon = st.number_input( | |
| "Embodied Carbon (kgCO鈧俥/kg)", | |
| min_value=0.0, | |
| value=form_state.get("embodied_carbon", editor_state.get("embodied_carbon", 0.5)), | |
| help="Production carbon emissions", | |
| key="material_embodied_carbon_input" | |
| ) | |
| absorptivity = st.number_input( | |
| "Absorptivity", | |
| min_value=0.0, | |
| max_value=1.0, | |
| value=form_state.get("absorptivity", editor_state.get("absorptivity", DEFAULT_MATERIAL_PROPERTIES["absorptivity"])), | |
| help="Solar radiation absorbed", | |
| key="material_absorptivity_input" | |
| ) | |
| price = st.number_input( | |
| "Price (USD/m虏)", | |
| min_value=0.0, | |
| value=form_state.get("price", editor_state.get("price", 50.0)), | |
| help="Cost per area", | |
| key="material_price_input" | |
| ) | |
| emissivity = st.number_input( | |
| "Emissivity", | |
| min_value=0.0, | |
| max_value=1.0, | |
| value=form_state.get("emissivity", editor_state.get("emissivity", DEFAULT_MATERIAL_PROPERTIES["emissivity"])), | |
| help="Ratio of radiation emitted by the material (0.0 to 1.0)", | |
| key="material_emissivity_input" | |
| ) | |
| if st.form_submit_button("Save Material"): | |
| action_id = str(uuid.uuid4()) | |
| if st.session_state.material_action.get("id") != action_id: | |
| st.session_state.material_action = {"action": "save", "id": action_id} | |
| st.session_state.material_form_state = { | |
| "name": name, | |
| "category": category, | |
| "density": density, | |
| "specific_heat": specific_heat, | |
| "default_thickness": default_thickness, | |
| "embodied_carbon": embodied_carbon, | |
| "absorptivity": absorptivity, | |
| "price": price, | |
| "emissivity": emissivity | |
| } | |
| if not name or not name.strip(): | |
| st.error("Material name cannot be empty.") | |
| elif (name in st.session_state.project_data["materials"]["project"] or | |
| name in st.session_state.project_data["materials"]["library"]) and (not is_edit or name != original_name): | |
| st.error(f"Material '{name}' already exists.") | |
| else: | |
| try: | |
| # Use default conductivity from library or a fallback value | |
| conductivity = form_state.get("conductivity", editor_state.get("conductivity", 0.1)) | |
| new_material = Material( | |
| name=name, | |
| category=MaterialCategory[category.upper().replace("-", "_")], | |
| conductivity=conductivity, | |
| density=density, | |
| specific_heat=specific_heat, | |
| default_thickness=default_thickness, | |
| embodied_carbon=embodied_carbon, | |
| absorptivity=absorptivity, | |
| price=price, | |
| emissivity=emissivity, | |
| is_library=False | |
| ) | |
| if is_edit and editor_state.get("edit_source") == "project": | |
| success, message = material_library.edit_project_material( | |
| original_name, new_material, st.session_state.project_data["materials"]["project"], st.session_state.get("components", {}) | |
| ) | |
| else: | |
| success, message = material_library.add_project_material(new_material, st.session_state.project_data["materials"]["project"]) | |
| if success: | |
| st.success(message) | |
| st.session_state.material_editor = {} | |
| st.session_state.material_form_state = { | |
| "name": "", | |
| "category": "Insulation", | |
| "density": 1000.0, | |
| "specific_heat": 1000.0, | |
| "default_thickness": 0.1, | |
| "embodied_carbon": 0.5, | |
| "absorptivity": DEFAULT_MATERIAL_PROPERTIES["absorptivity"], | |
| "price": 50.0, | |
| "emissivity": DEFAULT_MATERIAL_PAGE["emissivity"] | |
| } | |
| st.session_state.material_action = {"action": None, "id": None} | |
| st.session_state.rerun_trigger = None | |
| st.session_state.materials_rerun_pending = True | |
| else: | |
| st.error(f"Failed to save material: {message}") | |
| except Exception as e: | |
| st.error(f"Error saving material: {str(e)}") | |
| def display_fenestrations_tab(material_library: MaterialLibrary): | |
| """Display the fenestrations tab content with two-column layout.""" | |
| col1, col2 = st.columns([3, 2]) | |
| with col1: | |
| st.subheader("Fenestrations") | |
| st.subheader("Library Fenestrations") | |
| with st.container(): | |
| library_fenestrations = list(material_library.library_glazing_materials.values()) | |
| if not library_fenestrations: | |
| st.warning("No library fenestrations loaded. Check data initialization.") | |
| raw_fenestrations = st.session_state.project_data["fenestrations"]["library"] | |
| if raw_fenestrations: | |
| st.write("Raw fenestration names available in session state:") | |
| st.write(list(raw_fenestrations.keys())) | |
| cols = st.columns([2, 1, 1, 1, 1, 1, 1]) | |
| cols[0].write("**Name**") | |
| cols[1].write("**SHGC**") | |
| cols[2].write("**U-Value (W/m虏路K)**") | |
| cols[3].write("**Vis. Trans.**") | |
| cols[4].write("**Embodied Carbon**") | |
| cols[5].write("**Price**") | |
| cols[6].write("**Clone**") | |
| for fenestration in library_fenestrations: | |
| cols = st.columns([2, 1, 1, 1, 1, 1, 1]) | |
| cols[0].write(fenestration.name) | |
| cols[1].write(f"{fenestration.shgc:.2f}") | |
| cols[2].write(f"{fenestration.u_value:.3f}") | |
| cols[3].write(f"{fenestration.visible_transmittance:.2f}") | |
| cols[4].write(f"{fenestration.embodied_carbon:.2f}") | |
| cols[5].write(f"{fenestration.price:.2f}") | |
| if cols[6].button("Clone", key=f"copy_lib_fen_{fenestration.name}"): | |
| new_name = f"{fenestration.name}_Project" | |
| counter = 1 | |
| while new_name in st.session_state.project_data["fenestrations"]["project"] or new_name in st.session_state.project_data["fenestrations"]["library"]: | |
| new_name = f"{fenestration.name}_Project_{counter}" | |
| counter += 1 | |
| new_fenestration = GlazingMaterial( | |
| name=new_name, | |
| shgc=fenestration.shgc, | |
| u_value=fenestration.u_value, | |
| h_o=fenestration.h_o, | |
| visible_transmittance=fenestration.visible_transmittance, | |
| embodied_carbon=fenestration.embodied_carbon, | |
| price=fenestration.price, | |
| is_library=False | |
| ) | |
| st.session_state.project_data["fenestrations"]["project"][new_name] = new_fenestration | |
| st.success(f"Fenestration '{new_name}' cloned successfully!") | |
| st.session_state.materials_rerun_pending = True | |
| st.subheader("Project Fenestrations") | |
| with st.container(): | |
| if st.session_state.project_data["fenestrations"]["project"]: | |
| cols = st.columns([2, 1, 1, 1, 1, 1, 1, 1]) | |
| cols[0].write("**Name**") | |
| cols[1].write("**SHGC**") | |
| cols[2].write("**U-Value (W/m虏路K)**") | |
| cols[3].write("**Vis. Trans.**") | |
| cols[4].write("**Embodied Carbon**") | |
| cols[5].write("**Price**") | |
| cols[6].write("**Edit**") | |
| cols[7].write("**Delete**") | |
| for fenestration in st.session_state.project_data["fenestrations"]["project"].values(): | |
| cols = st.columns([2, 1, 1, 1, 1, 1, 1, 1]) | |
| cols[0].write(fenestration.name) | |
| cols[1].write(f"{fenestration.shgc:.2f}") | |
| cols[2].write(f"{fenestration.u_value:.3f}") | |
| cols[3].write(f"{fenestration.visible_transmittance:.2f}") | |
| cols[4].write(f"{fenestration.embodied_carbon:.2f}") | |
| cols[5].write(f"{fenestration.price:.2f}") | |
| if cols[6].button("Edit", key=f"edit_fen_{fenestration.name}"): | |
| if st.session_state.get("rerun_trigger") != f"edit_fen_{fenestration.name}": | |
| st.session_state.rerun_trigger = f"edit_fen_{fenestration.name}" | |
| st.session_state.fenestration_editor = { | |
| "name": fenestration.name, | |
| "shgc": fenestration.shgc, | |
| "u_value": fenestration.u_value, | |
| "h_o": fenestration.h_o, | |
| "visible_transmittance": fenestration.visible_transmittance, | |
| "embodied_carbon": fenestration.embodied_carbon, | |
| "price": fenestration.price, | |
| "is_edit": True, | |
| "edit_source": "project", | |
| "original_name": fenestration.name | |
| } | |
| st.session_state.fenestration_form_state = { | |
| "name": fenestration.name, | |
| "shgc": fenestration.shgc, | |
| "u_value": fenestration.u_value, | |
| "h_o": fenestration.h_o, | |
| "visible_transmittance": fenestration.visible_transmittance, | |
| "embodied_carbon": fenestration.embodied_carbon, | |
| "price": fenestration.price | |
| } | |
| st.session_state.active_tab = "Fenestrations" | |
| if cols[7].button("Delete", key=f"delete_fen_{fenestration.name}"): | |
| if any(comp.get("fenestration_material") and comp["fenestration_material"].name == fenestration.name | |
| for comp_list in st.session_state.get("components", {}).values() for comp in comp_list): | |
| st.error(f"Fenestration '{fenestration.name}' is used in components and cannot be deleted.") | |
| else: | |
| del st.session_state.project_data["fenestrations"]["project"][fenestration.name] | |
| st.success(f"Fenestration '{fenestration.name}' deleted successfully!") | |
| st.session_state.materials_rerun_pending = True | |
| else: | |
| st.write("No project fenestrations added.") | |
| st.subheader("Project Fenestrations") | |
| if st.session_state.project_data["fenestrations"]["project"]: | |
| try: | |
| fenestration_df = material_library.to_dataframe("glazing", project_glazing_materials=st.session_state.project_data["fenestrations"]["project"]) | |
| if not fenestration_df.empty: | |
| st.dataframe(fenestration_df, use_container_width=True) | |
| else: | |
| fenestration_data = [ | |
| { | |
| "Name": f.name, | |
| "SHGC": f.shgc, | |
| "U-Value (W/m虏路K)": f.u_value, | |
| "Exterior Conductance (W/m虏路K)": f.h_o, | |
| "Visible Transmittance": f.visible_transmittance, | |
| "Embodied Carbon (kgCO鈧俥/m虏)": f.embodied_carbon, | |
| "Price (USD/m虏)": f.price | |
| } | |
| for f in st.session_state.project_data["fenestrations"]["project"].values() | |
| ] | |
| fenestration_df = pd.DataFrame(fenestration_data) | |
| if not fenestration_df.empty: | |
| st.dataframe(fenestration_df, use_container_width=True) | |
| else: | |
| st.write("No project fenestrations to display.") | |
| except Exception as e: | |
| st.error(f"Error displaying project fenestrations: {str(e)}") | |
| st.write("No project fenestrations to display.") | |
| else: | |
| st.write("No project fenestrations to display.") | |
| with col2: | |
| st.subheader("Fenestration Editor/Creator") | |
| with st.container(): | |
| with st.form("fenestration_editor_form", clear_on_submit=False): | |
| editor_state = st.session_state.get("fenestration_editor", {}) | |
| form_state = st.session_state.get("fenestration_form_state", { | |
| "name": "", | |
| "shgc": 0.7, | |
| "u_value": 5.0, | |
| "h_o": DEFAULT_WINDOW_PROPERTIES["h_o"], | |
| "visible_transmittance": 0.7, | |
| "embodied_carbon": 25.0, | |
| "price": 100.0 | |
| }) | |
| is_edit = editor_state.get("is_edit", False) | |
| original_name = editor_state.get("original_name", "") | |
| name = st.text_input( | |
| "Fenestration Name", | |
| value=form_state.get("name", editor_state.get("name", "")), | |
| help="Unique fenestration identifier", | |
| key="fenestration_name_input" | |
| ) | |
| shgc = st.number_input( | |
| "Solar Heat Gain Coefficient (SHGC)", | |
| min_value=0.0, | |
| max_value=1.0, | |
| value=form_state.get("shgc", editor_state.get("shgc", 0.7)), | |
| help="Fraction of solar radiation admitted", | |
| key="fenestration_shgc_input" | |
| ) | |
| u_value = st.number_input( | |
| "U-Value (W/m虏路K)", | |
| min_value=0.1, | |
| value=form_state.get("u_value", editor_state.get("u_value", 5.0)), | |
| help="Thermal transmittance", | |
| key="fenestration_u_value_input" | |
| ) | |
| h_o = st.number_input( | |
| "Exterior Surface Conductance (W/m虏路K)", | |
| min_value=0.0, | |
| value=form_state.get("h_o", editor_state.get("h_o", DEFAULT_WINDOW_PROPERTIES["h_o"])), | |
| help="Exterior surface heat transfer coefficient", | |
| key="fenestration_h_o_input" | |
| ) | |
| visible_transmittance = st.number_input( | |
| "Visible Transmittance", | |
| min_value=0.0, | |
| max_value=1.0, | |
| value=form_state.get("visible_transmittance", editor_state.get("visible_transmittance", 0.7)), | |
| help="Fraction of visible light transmitted", | |
| key="fenestration_visible_transmittance_input" | |
| ) | |
| embodied_carbon = st.number_input( | |
| "Embodied Carbon (kgCO鈧俥/m虏)", | |
| min_value=0.0, | |
| value=form_state.get("embodied_carbon", editor_state.get("embodied_carbon", 25.0)), | |
| help="Production carbon emissions per square meter", | |
| key="fenestration_embodied_carbon_input" | |
| ) | |
| price = st.number_input( | |
| "Price (USD/m虏)", | |
| min_value=0.0, | |
| value=form_state.get("price", editor_state.get("price", 100.0)), | |
| help="Cost per area", | |
| key="fenestration_price_input" | |
| ) | |
| if st.form_submit_button("Save Fenestration"): | |
| action_id = str(uuid.uuid4()) | |
| if st.session_state.fenestration_action.get("id") != action_id: | |
| st.session_state.fenestration_action = {"action": "save", "id": action_id} | |
| st.session_state.fenestration_form_state = { | |
| "name": name, | |
| "shgc": shgc, | |
| "u_value": u_value, | |
| "h_o": h_o, | |
| "visible_transmittance": visible_transmittance, | |
| "embodied_carbon": embodied_carbon, | |
| "price": price | |
| } | |
| if not name or not name.strip(): | |
| st.error("Fenestration name cannot be empty.") | |
| elif (name in st.session_state.project_data["fenestrations"]["project"] or | |
| name in st.session_state.project_data["fenestrations"]["library"]) and (not is_edit or name != original_name): | |
| st.error(f"Fenestration '{name}' already exists.") | |
| else: | |
| try: | |
| new_fenestration = GlazingMaterial( | |
| name=name, | |
| shgc=shgc, | |
| u_value=u_value, | |
| h_o=h_o, | |
| visible_transmittance=visible_transmittance, | |
| embodied_carbon=embodied_carbon, | |
| price=price, | |
| is_library=False | |
| ) | |
| if is_edit and editor_state.get("edit_source") == "project": | |
| st.session_state.project_data["fenestrations"]["project"][original_name] = new_fenestration | |
| if name != original_name: | |
| del st.session_state.project_data["fenestrations"]["project"][original_name] | |
| st.session_state.project_data["fenestrations"]["project"][name] = new_fenestration | |
| st.success(f"Fenestration '{name}' updated successfully!") | |
| else: | |
| st.session_state.project_data["fenestrations"]["project"][name] = new_fenestration | |
| st.success(f"Fenestration '{name}' added successfully!") | |
| st.session_state.fenestration_editor = {} | |
| st.session_state.fenestration_form_state = { | |
| "name": "", | |
| "shgc": 0.7, | |
| "u_value": 5.0, | |
| "h_o": DEFAULT_WINDOW_PROPERTIES["h_o"], | |
| "visible_transmittance": 0.7, | |
| "embodied_carbon": 25.0, | |
| "price": 100.0 | |
| } | |
| st.session_state.fenestration_action = {"action": None, "id": None} | |
| st.session_state.rerun_trigger = None | |
| st.session_state.materials_rerun_pending = True | |
| except Exception as e: | |
| st.error(f"Error saving fenestration: {str(e)}") | |
| def get_available_materials(): | |
| """Get all available materials (library + project) for use in other modules.""" | |
| materials = {} | |
| if "materials" in st.session_state.project_data: | |
| materials.update(st.session_state.project_data["materials"]["library"]) | |
| materials.update(st.session_state.project_data["materials"]["project"]) | |
| return materials | |
| def get_available_fenestrations(): | |
| """Get all available fenestrations (library + project) for use in other modules.""" | |
| fenestrations = {} | |
| if "fenestrations" in st.session_state.project_data: | |
| fenestrations.update(st.session_state.project_data["fenestrations"]["library"]) | |
| fenestrations.update(st.session_state.project_data["fenestrations"]["project"]) | |
| return fenestrations |