""" 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₂e/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₂e/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(""" """, 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₂e/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₂e/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₂e/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