BuildSustain-02 / app /materials_library.py
mabuseif's picture
Update app/materials_library.py
ee8c13b verified
"""
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