|
|
""" |
|
|
Material Library for HVAC Load Calculator |
|
|
Updated 2025-05-16: Removed mass_per_meter, added thermal_mass method, updated CSV handling. |
|
|
Updated 2025-05-16: Fixed U-value calculation in Material.get_u_value. |
|
|
Updated 2025-05-16: Updated MaterialCategory to Finishing Materials, Structural Materials, Sub-Structural Materials, Insulation. |
|
|
Updated 2025-05-16: Fixed Construction.get_thermal_mass to use avg_specific_heat. |
|
|
|
|
|
Developed by: Dr Majed Abuseif, Deakin University |
|
|
© 2025 |
|
|
""" |
|
|
|
|
|
from typing import Dict, List, Optional, Tuple |
|
|
from enum import Enum |
|
|
import pandas as pd |
|
|
import streamlit as st |
|
|
import uuid |
|
|
import csv |
|
|
import io |
|
|
|
|
|
class MaterialCategory(Enum): |
|
|
FINISHING_MATERIALS = "Finishing Materials" |
|
|
STRUCTURAL_MATERIALS = "Structural Materials" |
|
|
SUB_STRUCTURAL_MATERIALS = "Sub-Structural Materials" |
|
|
INSULATION = "Insulation" |
|
|
|
|
|
class ThermalMass(Enum): |
|
|
HIGH = "High" |
|
|
MEDIUM = "Medium" |
|
|
LOW = "Low" |
|
|
NO_MASS = "No Mass" |
|
|
|
|
|
class Material: |
|
|
def __init__(self, name: str, category: MaterialCategory, conductivity: float, density: float, |
|
|
specific_heat: float, default_thickness: float, embodied_carbon: float, |
|
|
solar_absorption: float, price: float, is_library: bool = True): |
|
|
self.name = name |
|
|
self.category = category |
|
|
self.conductivity = max(0.01, conductivity) |
|
|
self.density = max(1.0, density) |
|
|
self.specific_heat = max(100.0, specific_heat) |
|
|
self.default_thickness = max(0.01, default_thickness) |
|
|
self.embodied_carbon = max(0.0, embodied_carbon) |
|
|
self.solar_absorption = min(max(0.0, solar_absorption), 1.0) |
|
|
self.price = max(0.0, price) |
|
|
self.is_library = is_library |
|
|
|
|
|
def get_thermal_mass(self) -> ThermalMass: |
|
|
if self.density < 100.0 or self.specific_heat < 800.0: |
|
|
return ThermalMass.NO_MASS |
|
|
elif self.density > 2000.0 and self.specific_heat > 800.0: |
|
|
return ThermalMass.HIGH |
|
|
elif 1000.0 <= self.density <= 2000.0 and 800.0 <= self.specific_heat <= 1200.0: |
|
|
return ThermalMass.MEDIUM |
|
|
else: |
|
|
return ThermalMass.LOW |
|
|
|
|
|
def get_u_value(self) -> float: |
|
|
return self.conductivity / self.default_thickness if self.default_thickness > 0 else 0.1 |
|
|
|
|
|
class Construction: |
|
|
def __init__(self, name: str, component_type: str, layers: List[Dict], is_library: bool = True): |
|
|
self.name = name |
|
|
self.component_type = component_type |
|
|
self.layers = layers or [] |
|
|
self.is_library = is_library |
|
|
self.u_value = self.calculate_u_value() |
|
|
self.total_thickness = sum(layer["thickness"] for layer in self.layers) |
|
|
self.embodied_carbon = sum(layer["material"].embodied_carbon * layer["material"].density * layer["thickness"] |
|
|
for layer in self.layers) |
|
|
self.solar_absorption = max(layer["material"].solar_absorption for layer in self.layers) if self.layers else 0.6 |
|
|
self.price = sum(layer["material"].price * layer["thickness"] / layer["material"].default_thickness |
|
|
for layer in self.layers) |
|
|
|
|
|
def calculate_u_value(self) -> float: |
|
|
if not self.layers: |
|
|
return 0.1 |
|
|
r_total = sum(layer["thickness"] / layer["material"].conductivity for layer in self.layers) |
|
|
return 1 / r_total if r_total > 0 else 0.1 |
|
|
|
|
|
def get_thermal_mass(self) -> ThermalMass: |
|
|
if not self.layers: |
|
|
return ThermalMass.NO_MASS |
|
|
total_thickness = self.total_thickness |
|
|
if total_thickness == 0: |
|
|
return ThermalMass.NO_MASS |
|
|
avg_density = sum(layer["material"].density * layer["thickness"] for layer in self.layers) / total_thickness |
|
|
avg_specific_heat = sum(layer["material"].specific_heat * layer["thickness"] for layer in self.layers) / total_thickness |
|
|
if avg_density < 100.0 or avg_specific_heat < 800.0: |
|
|
return ThermalMass.NO_MASS |
|
|
elif avg_density > 2000.0 and avg_specific_heat > 800.0: |
|
|
return ThermalMass.HIGH |
|
|
elif 1000.0 <= avg_density <= 2000.0 and 800.0 <= avg_specific_heat <= 1200.0: |
|
|
return ThermalMass.MEDIUM |
|
|
else: |
|
|
return ThermalMass.LOW |
|
|
|
|
|
class MaterialLibrary: |
|
|
def __init__(self): |
|
|
self.library_materials = self.initialize_materials() |
|
|
self.library_constructions = self.initialize_constructions() |
|
|
|
|
|
def initialize_materials(self) -> Dict[str, Material]: |
|
|
materials = [ |
|
|
Material("Fiberglass Insulation", MaterialCategory.INSULATION, 0.04, 12.0, 840.0, 0.1, 0.5, 0.6, 5.0), |
|
|
Material("Concrete", MaterialCategory.STRUCTURAL_MATERIALS, 1.4, 2400.0, 900.0, 0.2, 1.5, 0.65, 20.0), |
|
|
Material("Steel", MaterialCategory.SUB_STRUCTURAL_MATERIALS, 50.0, 7800.0, 500.0, 0.003, 2.0, 0.7, 100.0), |
|
|
Material("Oak Wood", MaterialCategory.SUB_STRUCTURAL_MATERIALS, 0.15, 700.0, 2400.0, 0.05, 0.7, 0.5, 30.0), |
|
|
Material("Float Glass", MaterialCategory.INSULATION, 0.96, 2500.0, 840.0, 0.006, 1.0, 0.8, 50.0), |
|
|
Material("Clay Brick", MaterialCategory.STRUCTURAL_MATERIALS, 0.72, 1900.0, 900.0, 0.115, 1.2, 0.7, 15.0), |
|
|
Material("Gypsum Plaster", MaterialCategory.FINISHING_MATERIALS, 0.22, 1200.0, 1000.0, 0.013, 0.6, 0.4, 10.0), |
|
|
Material("Asphalt Shingle", MaterialCategory.FINISHING_MATERIALS, 0.16, 1100.0, 1000.0, 0.003, 0.8, 0.9, 8.0) |
|
|
] |
|
|
return {mat.name: mat for mat in materials} |
|
|
|
|
|
def initialize_constructions(self) -> Dict[str, Construction]: |
|
|
constructions = [ |
|
|
Construction("Exterior Wall", "Wall", [ |
|
|
{"material": self.library_materials["Clay Brick"], "thickness": 0.115}, |
|
|
{"material": self.library_materials["Fiberglass Insulation"], "thickness": 0.1}, |
|
|
{"material": self.library_materials["Gypsum Plaster"], "thickness": 0.013} |
|
|
]), |
|
|
Construction("Flat Roof", "Roof", [ |
|
|
{"material": self.library_materials["Asphalt Shingle"], "thickness": 0.003}, |
|
|
{"material": self.library_materials["Fiberglass Insulation"], "thickness": 0.15}, |
|
|
{"material": self.library_materials["Concrete"], "thickness": 0.1} |
|
|
]), |
|
|
Construction("Slab Floor", "Floor", [ |
|
|
{"material": self.library_materials["Concrete"], "thickness": 0.1}, |
|
|
{"material": self.library_materials["Fiberglass Insulation"], "thickness": 0.05} |
|
|
]) |
|
|
] |
|
|
return {cons.name: cons for cons in constructions} |
|
|
|
|
|
def get_all_materials(self, project_materials: Optional[Dict[str, Material]] = None) -> List[Material]: |
|
|
materials = list(self.library_materials.values()) |
|
|
if project_materials: |
|
|
materials.extend(list(project_materials.values())) |
|
|
return materials |
|
|
|
|
|
def add_project_material(self, material: Material, project_materials: Dict[str, Material]) -> Tuple[bool, str]: |
|
|
if len(project_materials) >= 20: |
|
|
return False, "Maximum 20 project materials allowed." |
|
|
if material.name in project_materials or material.name in self.library_materials: |
|
|
return False, f"Material name '{material.name}' already exists." |
|
|
project_materials[material.name] = material |
|
|
return True, f"Material '{material.name}' added to project materials." |
|
|
|
|
|
def edit_project_material(self, old_name: str, new_material: Material, project_materials: Dict[str, Material], |
|
|
components: Dict[str, List]) -> Tuple[bool, str]: |
|
|
if old_name not in project_materials: |
|
|
return False, f"Material '{old_name}' not found in project materials." |
|
|
if new_material.name != old_name and (new_material.name in project_materials or new_material.name in self.library_materials): |
|
|
return False, f"Material name '{new_material.name}' already exists." |
|
|
for comp_list in components.values(): |
|
|
for comp in comp_list: |
|
|
if comp.construction and any(layer["material"].name == old_name for layer in comp.layers): |
|
|
comp.layers = [{"material": new_material if layer["material"].name == old_name else layer["material"], |
|
|
"thickness": layer["thickness"]} for layer in comp.layers] |
|
|
comp.construction = Construction( |
|
|
name=comp.construction.name, |
|
|
component_type=comp.construction.component_type, |
|
|
layers=comp.layers, |
|
|
is_library=comp.construction.is_library |
|
|
) |
|
|
project_materials.pop(old_name) |
|
|
project_materials[new_material.name] = new_material |
|
|
return True, f"Material '{old_name}' updated to '{new_material.name}'." |
|
|
|
|
|
def delete_project_material(self, name: str, project_materials: Dict[str, Material], components: Dict[str, List[Dict]]) -> Tuple[bool, str]: |
|
|
if name not in project_materials: |
|
|
return False, f"Material '{name}' not found in project materials." |
|
|
|
|
|
|
|
|
for cons in self.project_constructions.values(): |
|
|
if any(layer["material"].name == name for layer in cons.layers): |
|
|
return False, f"Cannot delete '{name}' as it is used in construction '{cons.name}'." |
|
|
|
|
|
|
|
|
for cons in self.library_constructions.values(): |
|
|
if any(layer["material"].name == name for layer in cons.layers): |
|
|
return False, f"Cannot delete '{name}' as it is used in library construction '{cons.name}'." |
|
|
|
|
|
|
|
|
for comp_type, comp_list in components.items(): |
|
|
for comp in comp_list: |
|
|
if 'layers' in comp and any(layer["material"].name == name for layer in comp["layers"]): |
|
|
return False, f"Cannot delete '{name}' as it is used in component '{comp['name']}' ({comp_type})." |
|
|
|
|
|
del project_materials[name] |
|
|
return True, f"Material '{name}' deleted successfully." |
|
|
|
|
|
def add_project_construction(self, construction: Construction, project_constructions: Dict[str, Construction]) -> Tuple[bool, str]: |
|
|
if len(project_constructions) >= 20: |
|
|
return False, "Maximum 20 project constructions allowed." |
|
|
if construction.name in project_constructions or construction.name in self.library_constructions: |
|
|
return False, f"Construction name '{construction.name}' already exists." |
|
|
project_constructions[construction.name] = construction |
|
|
return True, f"Construction '{construction.name}' added to project constructions." |
|
|
|
|
|
def edit_project_construction(self, old_name: str, new_construction: Construction, |
|
|
project_constructions: Dict[str, Construction], |
|
|
components: Dict[str, List]) -> Tuple[bool, str]: |
|
|
if old_name not in project_constructions: |
|
|
return False, f"Construction '{old_name}' not found in project constructions." |
|
|
if new_construction.name != old_name and (new_construction.name in project_constructions or new_construction.name in self.library_constructions): |
|
|
return False, f"Construction name '{new_construction.name}' already exists." |
|
|
for comp_list in components.values(): |
|
|
for comp in comp_list: |
|
|
if comp.construction and comp.construction.name == old_name: |
|
|
comp.construction = new_construction |
|
|
comp.layers = new_construction.layers |
|
|
comp.u_value = new_construction.u_value |
|
|
project_constructions.pop(old_name) |
|
|
project_constructions[new_construction.name] = new_construction |
|
|
return True, f"Construction '{old_name}' updated to '{new_construction.name}'." |
|
|
|
|
|
def delete_project_construction(self, name: str, project_constructions: Dict[str, Construction], |
|
|
components: Dict[str, List]) -> Tuple[bool, str]: |
|
|
if name not in project_constructions: |
|
|
return False, f"Construction '{name}' not found in project constructions." |
|
|
for comp_list in components.values(): |
|
|
for comp in comp_list: |
|
|
if comp.construction and comp.construction.name == name: |
|
|
return False, f"Construction '{name}' is used in component '{comp.name}'." |
|
|
project_constructions.pop(name) |
|
|
return True, f"Construction '{name}' deleted." |
|
|
|
|
|
def to_dataframe(self, data_type: str, project_materials: Optional[Dict[str, Material]] = None, |
|
|
project_constructions: Optional[Dict[str, Construction]] = None, |
|
|
only_project: bool = False) -> pd.DataFrame: |
|
|
if data_type == "materials": |
|
|
data = [] |
|
|
materials = project_materials.values() if only_project else self.get_all_materials(project_materials) |
|
|
for mat in materials: |
|
|
data.append({ |
|
|
"Name": mat.name, |
|
|
"Category": mat.category.value, |
|
|
"Conductivity (W/m·K)": mat.conductivity, |
|
|
"Density (kg/m³)": mat.density, |
|
|
"Specific Heat (J/kg·K)": mat.specific_heat, |
|
|
"Default Thickness (m)": mat.default_thickness, |
|
|
"Embodied Carbon (kgCO₂e/kg)": mat.embodied_carbon, |
|
|
"Solar Absorption": mat.solar_absorption, |
|
|
"Price (USD/m²)": mat.price, |
|
|
"Source": "Project" if not mat.is_library else "Library" |
|
|
}) |
|
|
return pd.DataFrame(data) |
|
|
elif data_type == "constructions": |
|
|
data = [] |
|
|
constructions = project_constructions.values() if only_project else list(self.library_constructions.values()) |
|
|
if not only_project and project_constructions: |
|
|
constructions.extend(list(project_constructions.values())) |
|
|
for cons in constructions: |
|
|
layers_str = "; ".join(f"{layer['material'].name} ({layer['thickness']}m)" for layer in cons.layers) |
|
|
data.append({ |
|
|
"Name": cons.name, |
|
|
"Component Type": cons.component_type, |
|
|
"U-Value (W/m²·K)": cons.u_value, |
|
|
"Total Thickness (m)": cons.total_thickness, |
|
|
"Embodied Carbon (kgCO₂e/m²)": cons.embodied_carbon, |
|
|
"Solar Absorption": cons.solar_absorption, |
|
|
"Price (USD/m²)": cons.price, |
|
|
"Layers": layers_str, |
|
|
"Source": "Project" if not cons.is_library else "Library" |
|
|
}) |
|
|
return pd.DataFrame(data) |
|
|
return pd.DataFrame() |
|
|
|
|
|
def display_material_library_ui(self): |
|
|
""" |
|
|
Display the Material Library UI. |
|
|
This function was moved from main.py to reduce code size. |
|
|
""" |
|
|
import uuid |
|
|
|
|
|
st.title("Material Library") |
|
|
st.write("Manage materials and constructions based on ASHRAE 2005 Handbook of Fundamentals.") |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
if st.session_state.get("rerun_pending", False): |
|
|
st.session_state.rerun_pending = False |
|
|
st.rerun() |
|
|
|
|
|
use_library_only = st.checkbox( |
|
|
"Use Library Materials and Constructions Only", |
|
|
value=st.session_state.use_library_only, |
|
|
help="Use predefined materials/constructions only." |
|
|
) |
|
|
st.session_state.use_library_only = use_library_only |
|
|
|
|
|
if not use_library_only and (st.session_state.project_materials or st.session_state.project_constructions): |
|
|
if st.button("Reset Project Materials and Constructions"): |
|
|
if st.checkbox("Confirm reset (deletes all project materials/constructions)"): |
|
|
st.session_state.project_materials = {} |
|
|
st.session_state.project_constructions = {} |
|
|
st.success("Project materials and constructions reset!") |
|
|
st.session_state.rerun_pending = True |
|
|
|
|
|
|
|
|
uploaded_file = st.file_uploader("Upload Materials and Constructions (CSV)", type=["csv"]) |
|
|
if uploaded_file: |
|
|
try: |
|
|
content = uploaded_file.read().decode('utf-8') |
|
|
csv_reader = csv.DictReader(io.StringIO(content)) |
|
|
materials_to_add = [] |
|
|
constructions_to_add = [] |
|
|
current_section = None |
|
|
for row in csv_reader: |
|
|
if row.get('Section') == 'Materials': |
|
|
current_section = 'Materials' |
|
|
materials_to_add.append(row) |
|
|
elif row.get('Section') == 'Constructions': |
|
|
current_section = 'Constructions' |
|
|
constructions_to_add.append(row) |
|
|
elif current_section == 'Materials': |
|
|
materials_to_add.append(row) |
|
|
elif current_section == 'Constructions': |
|
|
constructions_to_add.append(row) |
|
|
|
|
|
|
|
|
for mat_data in materials_to_add: |
|
|
name = mat_data.get('Name', '').strip() |
|
|
if not name: |
|
|
st.warning("Skipping material with empty name.") |
|
|
continue |
|
|
if name in st.session_state.project_materials or name in self.material_library.library_materials: |
|
|
st.warning(f"Material '{name}' already exists.") |
|
|
action = st.radio(f"Action for '{name}'", ["Overwrite", "Ignore"], key=f"mat_action_{name}") |
|
|
if action == "Ignore": |
|
|
continue |
|
|
try: |
|
|
category = mat_data.get('Category', 'Insulation').strip() |
|
|
if not category or category not in [c.value for c in MaterialCategory]: |
|
|
category = 'Insulation' |
|
|
st.warning(f"Invalid or empty category for '{name}', defaulting to Insulation.") |
|
|
conductivity = float(mat_data.get('Conductivity', '0.1') or '0.1') |
|
|
density = float(mat_data.get('Density', '1000.0') or '1000.0') |
|
|
specific_heat = float(mat_data.get('Specific Heat', '1000.0') or '1000.0') |
|
|
default_thickness = float(mat_data.get('Default Thickness', '0.1') or '0.1') |
|
|
embodied_carbon = float(mat_data.get('Embodied Carbon', '0.5') or '0.5') |
|
|
solar_absorption = float(mat_data.get('Solar Absorption', '0.6') or '0.6') |
|
|
price = float(mat_data.get('Price', '50.0') or '50.0') |
|
|
material = Material( |
|
|
name=name, |
|
|
category=MaterialCategory(category), |
|
|
conductivity=conductivity, |
|
|
density=density, |
|
|
specific_heat=specific_heat, |
|
|
default_thickness=default_thickness, |
|
|
embodied_carbon=embodied_carbon, |
|
|
solar_absorption=solar_absorption, |
|
|
price=price, |
|
|
is_library=False |
|
|
) |
|
|
success, message = self.material_library.add_project_material(material, st.session_state.project_materials) |
|
|
if not success: |
|
|
st.error(message) |
|
|
except (ValueError, TypeError) as e: |
|
|
st.error(f"Invalid data for material '{name}': {str(e)}") |
|
|
|
|
|
|
|
|
for cons_data in constructions_to_add: |
|
|
name = cons_data.get('Name', '').strip() |
|
|
if not name: |
|
|
st.warning("Skipping construction with empty name.") |
|
|
continue |
|
|
if name in st.session_state.project_constructions or name in self.material_library.library_constructions: |
|
|
st.warning(f"Construction '{name}' already exists.") |
|
|
action = st.radio(f"Action for '{name}'", ["Overwrite", "Ignore"], key=f"cons_action_{name}") |
|
|
if action == "Ignore": |
|
|
continue |
|
|
try: |
|
|
layers = [] |
|
|
layer_data = cons_data.get('Layers', '').strip() |
|
|
if layer_data: |
|
|
for layer in layer_data.split(';'): |
|
|
layer = layer.strip() |
|
|
if not layer: |
|
|
continue |
|
|
try: |
|
|
mat_name, thickness = layer.split(':') |
|
|
mat_name = mat_name.strip() |
|
|
thickness = float(thickness.strip()) |
|
|
material = (st.session_state.project_materials.get(mat_name) or |
|
|
self.material_library.library_materials.get(mat_name)) |
|
|
if not material: |
|
|
st.error(f"Material '{mat_name}' not found for construction '{name}'.") |
|
|
continue |
|
|
layers.append({"material": material, "thickness": thickness}) |
|
|
except (ValueError, TypeError): |
|
|
st.error(f"Invalid layer format in construction '{name}': {layer}") |
|
|
continue |
|
|
component_type = cons_data.get('Component Type', 'Wall').strip() or 'Wall' |
|
|
construction = Construction( |
|
|
name=name, |
|
|
component_type=component_type, |
|
|
layers=layers, |
|
|
is_library=False |
|
|
) |
|
|
success, message = self.material_library.add_project_construction(construction, st.session_state.project_constructions) |
|
|
if not success: |
|
|
st.error(message) |
|
|
except (ValueError, TypeError) as e: |
|
|
st.error(f"Invalid data for construction '{name}': {str(e)}") |
|
|
st.success("CSV import completed!") |
|
|
st.session_state.rerun_pending = True |
|
|
except Exception as e: |
|
|
st.error(f"Error importing CSV: {str(e)}") |
|
|
|
|
|
|
|
|
if not use_library_only and (st.session_state.project_materials or st.session_state.project_constructions): |
|
|
csv_buffer = io.StringIO() |
|
|
writer = csv.writer(csv_buffer) |
|
|
writer.writerow(['Section', 'Name', 'Category', 'Conductivity', 'Density', 'Specific Heat', 'Default Thickness', |
|
|
'Embodied Carbon', 'Solar Absorption', 'Price', 'Is Library']) |
|
|
|
|
|
used_materials = {} |
|
|
for cons in st.session_state.project_constructions.values(): |
|
|
for layer in cons.layers: |
|
|
mat = layer["material"] |
|
|
used_materials[mat.name] = mat |
|
|
|
|
|
for mat in st.session_state.project_materials.values(): |
|
|
used_materials[mat.name] = mat |
|
|
|
|
|
for mat in used_materials.values(): |
|
|
writer.writerow([ |
|
|
'Materials', |
|
|
mat.name, |
|
|
mat.category.value if mat.category else '', |
|
|
mat.conductivity, |
|
|
mat.density, |
|
|
mat.specific_heat, |
|
|
mat.default_thickness, |
|
|
mat.embodied_carbon, |
|
|
mat.solar_absorption, |
|
|
mat.price, |
|
|
'True' if mat.is_library else 'False' |
|
|
]) |
|
|
writer.writerow(['Section', 'Name', 'Component Type', 'Layers']) |
|
|
for cons in st.session_state.project_constructions.values(): |
|
|
layers_str = ';'.join(f"{layer['material'].name}:{layer['thickness']}" for layer in cons.layers) |
|
|
writer.writerow(['Constructions', cons.name, cons.component_type, layers_str]) |
|
|
st.download_button( |
|
|
label="Download Materials and Constructions (CSV)", |
|
|
data=csv_buffer.getvalue(), |
|
|
file_name="project_materials_constructions.csv", |
|
|
mime="text/csv" |
|
|
) |
|
|
|
|
|
|
|
|
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 'construction_action' not in st.session_state: |
|
|
st.session_state.construction_action = {"action": None, "id": None} |
|
|
if 'rerun_pending' not in st.session_state: |
|
|
st.session_state.rerun_pending = False |
|
|
if 'material_form_state' not in st.session_state: |
|
|
st.session_state.material_form_state = {} |
|
|
if 'construction_form_state' not in st.session_state: |
|
|
st.session_state.construction_form_state = {} |
|
|
|
|
|
|
|
|
tab1, tab2 = st.tabs(["Materials", "Constructions"]) |
|
|
active_tab = "Materials" if st.session_state.active_tab == "Materials" else "Constructions" |
|
|
with tab1: |
|
|
if active_tab == "Materials": |
|
|
st.session_state.active_tab = "Materials" |
|
|
with tab2: |
|
|
if active_tab == "Constructions": |
|
|
st.session_state.active_tab = "Constructions" |
|
|
|
|
|
with tab1: |
|
|
st.subheader("Materials") |
|
|
col1, col2 = st.columns([3, 2]) |
|
|
with col1: |
|
|
|
|
|
filter_options = ["All", "None"] + [c.value for c in MaterialCategory] |
|
|
category = st.selectbox("Filter by Category", filter_options, key="material_filter") |
|
|
|
|
|
st.subheader("Library Materials") |
|
|
with st.container(): |
|
|
library_materials = list(self.material_library.library_materials.values()) |
|
|
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("**Copy**") |
|
|
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().value) |
|
|
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, |
|
|
"solar_absorption": material.solar_absorption, |
|
|
"price": material.price, |
|
|
"is_edit": False |
|
|
} |
|
|
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, |
|
|
"solar_absorption": material.solar_absorption, |
|
|
"price": material.price |
|
|
} |
|
|
st.session_state.active_tab = "Materials" |
|
|
if not use_library_only and cols[4].button("Copy", key=f"copy_lib_mat_{material.name}"): |
|
|
new_name = f"{material.name}_Project" |
|
|
counter = 1 |
|
|
while new_name in st.session_state.project_materials or new_name in self.material_library.library_materials: |
|
|
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, |
|
|
solar_absorption=material.solar_absorption, |
|
|
price=material.price, |
|
|
is_library=False |
|
|
) |
|
|
try: |
|
|
success, message = self.material_library.add_project_material(new_material, st.session_state.project_materials) |
|
|
if success: |
|
|
st.success(message) |
|
|
st.session_state.rerun_pending = True |
|
|
else: |
|
|
st.error(message) |
|
|
except Exception as e: |
|
|
st.error(f"Error copying material: {str(e)}") |
|
|
|
|
|
st.subheader("Project Materials") |
|
|
with st.container(): |
|
|
if not use_library_only and st.session_state.project_materials: |
|
|
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_materials.values(): |
|
|
cols = st.columns([2, 1, 1, 1, 1]) |
|
|
cols[0].write(material.name) |
|
|
cols[1].write(material.get_thermal_mass().value) |
|
|
cols[2].write(f"{material.get_u_value():.3f}") |
|
|
if cols[3].button("Edit", key=f"edit_proj_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, |
|
|
"solar_absorption": material.solar_absorption, |
|
|
"price": material.price, |
|
|
"is_edit": True, |
|
|
"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, |
|
|
"solar_absorption": material.solar_absorption, |
|
|
"price": material.price |
|
|
} |
|
|
st.session_state.active_tab = "Materials" |
|
|
if cols[4].button("Delete", key=f"delete_proj_mat_{material.name}"): |
|
|
success, message = self.material_library.delete_project_material( |
|
|
material.name, st.session_state.project_materials, st.session_state.components |
|
|
) |
|
|
if success: |
|
|
st.success(message) |
|
|
st.session_state.rerun_pending = True |
|
|
else: |
|
|
st.error(message) |
|
|
else: |
|
|
st.write("No project materials added.") |
|
|
|
|
|
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, |
|
|
"solar_absorption": 0.6, |
|
|
"price": 50.0 |
|
|
}) |
|
|
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 [c.value for c in MaterialCategory] |
|
|
else editor_state.get("category", "Insulation")) |
|
|
category_index = ([c.value for c in MaterialCategory].index(default_category) |
|
|
if default_category in [c.value for c in MaterialCategory] else 0) |
|
|
category = st.selectbox( |
|
|
"Category", |
|
|
[c.value for c in MaterialCategory], |
|
|
index=category_index, |
|
|
help="Material type classification", |
|
|
key="material_category_input" |
|
|
) |
|
|
conductivity = st.number_input( |
|
|
"Thermal Conductivity (W/m·K)", |
|
|
min_value=0.01, |
|
|
value=form_state.get("conductivity", editor_state.get("conductivity", 0.1)), |
|
|
help="Heat flow ease", |
|
|
key="material_conductivity_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="Energy to raise temperature", |
|
|
key="material_specific_heat_input" |
|
|
) |
|
|
default_thickness = st.number_input( |
|
|
"Default Thickness (m)", |
|
|
min_value=0.01, |
|
|
value=form_state.get("default_thickness", editor_state.get("default_thickness", 0.1)), |
|
|
help="Standard material 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="Carbon footprint", |
|
|
key="material_embodied_carbon_input" |
|
|
) |
|
|
solar_absorption = st.number_input( |
|
|
"Solar Absorption", |
|
|
min_value=0.0, |
|
|
max_value=1.0, |
|
|
value=form_state.get("solar_absorption", editor_state.get("solar_absorption", 0.6)), |
|
|
help="Solar energy absorbed (0-1)", |
|
|
key="material_solar_absorption_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" |
|
|
) |
|
|
|
|
|
|
|
|
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, |
|
|
"conductivity": conductivity, |
|
|
"density": density, |
|
|
"specific_heat": specific_heat, |
|
|
"default_thickness": default_thickness, |
|
|
"embodied_carbon": embodied_carbon, |
|
|
"solar_absorption": solar_absorption, |
|
|
"price": price |
|
|
} |
|
|
if use_library_only: |
|
|
st.error("Cannot add/edit materials in library-only mode.") |
|
|
else: |
|
|
if not name or not name.strip(): |
|
|
st.error("Material name cannot be empty.") |
|
|
elif (name in st.session_state.project_materials or name in self.material_library.library_materials) and (not is_edit or name != original_name): |
|
|
st.error(f"Material '{name}' already exists.") |
|
|
else: |
|
|
try: |
|
|
|
|
|
save_as_new = is_edit and name != original_name |
|
|
new_material = Material( |
|
|
name=name, |
|
|
category=MaterialCategory(category), |
|
|
conductivity=conductivity, |
|
|
density=density, |
|
|
specific_heat=specific_heat, |
|
|
default_thickness=default_thickness, |
|
|
embodied_carbon=embodied_carbon, |
|
|
solar_absorption=solar_absorption, |
|
|
price=price, |
|
|
is_library=False |
|
|
) |
|
|
if is_edit and not save_as_new: |
|
|
success, message = self.material_library.edit_project_material( |
|
|
original_name, new_material, st.session_state.project_materials, st.session_state.components |
|
|
) |
|
|
else: |
|
|
success, message = self.material_library.add_project_material(new_material, st.session_state.project_materials) |
|
|
if success: |
|
|
st.success(message) |
|
|
st.session_state.material_editor = {} |
|
|
st.session_state.material_form_state = { |
|
|
"name": "", |
|
|
"category": "Insulation", |
|
|
"conductivity": 0.1, |
|
|
"density": 1000.0, |
|
|
"specific_heat": 1000.0, |
|
|
"default_thickness": 0.1, |
|
|
"embodied_carbon": 0.5, |
|
|
"solar_absorption": 0.6, |
|
|
"price": 50.0 |
|
|
} |
|
|
st.session_state.material_action = {"action": None, "id": None} |
|
|
st.session_state.rerun_trigger = None |
|
|
st.session_state.rerun_pending = True |
|
|
else: |
|
|
st.error(f"Failed to save material: {message}") |
|
|
except Exception as e: |
|
|
st.error(f"Error saving material: {str(e)}") |
|
|
|
|
|
if not use_library_only: |
|
|
st.subheader("Project Materials") |
|
|
material_df = self.material_library.to_dataframe("materials", |
|
|
project_materials=st.session_state.project_materials, |
|
|
only_project=True) |
|
|
if not material_df.empty: |
|
|
st.dataframe(material_df, use_container_width=True) |
|
|
else: |
|
|
st.write("No project materials to display.") |
|
|
|
|
|
with tab2: |
|
|
st.subheader("Constructions") |
|
|
col1, col2 = st.columns([3, 2]) |
|
|
with col1: |
|
|
filter_options = ["All", "Wall", "Roof", "Floor"] |
|
|
construction_filter = st.selectbox("Filter by Component Type", filter_options, key="construction_filter") |
|
|
|
|
|
st.subheader("Library Constructions") |
|
|
with st.container(): |
|
|
library_constructions = list(self.material_library.library_constructions.values()) |
|
|
if construction_filter != "All": |
|
|
library_constructions = [c for c in library_constructions if c.component_type == construction_filter] |
|
|
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("**Copy**") |
|
|
for construction in library_constructions: |
|
|
cols = st.columns([2, 1, 1, 1, 1]) |
|
|
cols[0].write(construction.name) |
|
|
cols[1].write(construction.get_thermal_mass().value) |
|
|
cols[2].write(f"{construction.u_value:.3f}") |
|
|
if cols[3].button("Preview", key=f"preview_lib_cons_{construction.name}"): |
|
|
if st.session_state.get("rerun_trigger") != f"preview_cons_{construction.name}": |
|
|
st.session_state.rerun_trigger = f"preview_cons_{construction.name}" |
|
|
st.session_state.construction_editor = { |
|
|
"name": construction.name, |
|
|
"component_type": construction.component_type, |
|
|
"layers": [{"material_name": layer["material"].name, "thickness": layer["thickness"]} |
|
|
for layer in construction.layers], |
|
|
"is_edit": False |
|
|
} |
|
|
st.session_state.construction_form_state = { |
|
|
"name": construction.name, |
|
|
"component_type": construction.component_type, |
|
|
"num_layers": len(construction.layers), |
|
|
"layers": [{"material_name": layer["material"].name, "thickness": layer["thickness"]} |
|
|
for layer in construction.layers] |
|
|
} |
|
|
st.session_state.active_tab = "Constructions" |
|
|
if not use_library_only and cols[4].button("Copy", key=f"copy_lib_cons_{construction.name}"): |
|
|
new_name = f"{construction.name}_Project" |
|
|
counter = 1 |
|
|
while new_name in st.session_state.project_constructions or new_name in self.material_library.library_constructions: |
|
|
new_name = f"{construction.name}_Project_{counter}" |
|
|
counter += 1 |
|
|
|
|
|
new_layers = [] |
|
|
for layer in construction.layers: |
|
|
material = layer["material"] |
|
|
if material.is_library: |
|
|
mat_name = f"{material.name}_Project" |
|
|
counter = 1 |
|
|
while mat_name in st.session_state.project_materials or mat_name in self.material_library.library_materials: |
|
|
mat_name = f"{material.name}_Project_{counter}" |
|
|
counter += 1 |
|
|
new_material = Material( |
|
|
name=mat_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, |
|
|
solar_absorption=material.solar_absorption, |
|
|
price=material.price, |
|
|
is_library=False |
|
|
) |
|
|
success, message = self.material_library.add_project_material(new_material, st.session_state.project_materials) |
|
|
if not success: |
|
|
st.error(f"Failed to copy material '{material.name}': {message}") |
|
|
break |
|
|
material = new_material |
|
|
new_layers.append({"material": material, "thickness": layer["thickness"]}) |
|
|
else: |
|
|
new_construction = Construction( |
|
|
name=new_name, |
|
|
component_type=construction.component_type, |
|
|
layers=new_layers, |
|
|
is_library=False |
|
|
) |
|
|
success, message = self.material_library.add_project_construction(new_construction, st.session_state.project_constructions) |
|
|
if success: |
|
|
st.success(message) |
|
|
st.session_state.rerun_pending = True |
|
|
else: |
|
|
st.error(message) |
|
|
|
|
|
st.subheader("Project Constructions") |
|
|
with st.container(): |
|
|
if not use_library_only and st.session_state.project_constructions: |
|
|
project_constructions = list(st.session_state.project_constructions.values()) |
|
|
if construction_filter != "All": |
|
|
project_constructions = [c for c in project_constructions if c.component_type == construction_filter] |
|
|
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 construction in project_constructions: |
|
|
cols = st.columns([2, 1, 1, 1, 1]) |
|
|
cols[0].write(construction.name) |
|
|
cols[1].write(construction.get_thermal_mass().value) |
|
|
cols[2].write(f"{construction.u_value:.3f}") |
|
|
if cols[3].button("Edit", key=f"edit_proj_cons_{construction.name}"): |
|
|
if st.session_state.get("rerun_trigger") != f"edit_cons_{construction.name}": |
|
|
st.session_state.rerun_trigger = f"edit_cons_{construction.name}" |
|
|
st.session_state.construction_editor = { |
|
|
"name": construction.name, |
|
|
"component_type": construction.component_type, |
|
|
"layers": [{"material_name": layer["material"].name, "thickness": layer["thickness"]} |
|
|
for layer in construction.layers], |
|
|
"is_edit": True, |
|
|
"original_name": construction.name |
|
|
} |
|
|
st.session_state.construction_form_state = { |
|
|
"name": construction.name, |
|
|
"component_type": construction.component_type, |
|
|
"num_layers": len(construction.layers), |
|
|
"layers": [{"material_name": layer["material"].name, "thickness": layer["thickness"]} |
|
|
for layer in construction.layers] |
|
|
} |
|
|
st.session_state.active_tab = "Constructions" |
|
|
if cols[4].button("Delete", key=f"delete_proj_cons_{construction.name}"): |
|
|
success, message = self.material_library.delete_project_construction( |
|
|
construction.name, st.session_state.project_constructions, st.session_state.components |
|
|
) |
|
|
if success: |
|
|
st.success(message) |
|
|
st.session_state.rerun_pending = True |
|
|
else: |
|
|
st.error(message) |
|
|
else: |
|
|
st.write("No project constructions added.") |
|
|
|
|
|
with col2: |
|
|
st.subheader("Construction Editor/Creator") |
|
|
with st.container(): |
|
|
|
|
|
is_preview = st.session_state.get("rerun_trigger", "") and st.session_state.get("rerun_trigger", "").startswith("preview_cons_") |
|
|
materials = (list(self.material_library.library_materials.values()) if is_preview |
|
|
else list(st.session_state.project_materials.values())) |
|
|
if not use_library_only: |
|
|
materials.extend(list(self.material_library.library_materials.values())) |
|
|
|
|
|
if not materials and not is_preview: |
|
|
st.warning("No materials available. Please add materials first.") |
|
|
else: |
|
|
with st.form("construction_editor_form", clear_on_submit=False): |
|
|
editor_state = st.session_state.get("construction_editor", {}) |
|
|
form_state = st.session_state.get("construction_form_state", { |
|
|
"name": "", |
|
|
"component_type": "Wall", |
|
|
"num_layers": 1, |
|
|
"layers": [{"material_name": "", "thickness": 0.1}] |
|
|
}) |
|
|
is_edit = editor_state.get("is_edit", False) |
|
|
original_name = editor_state.get("original_name", "") |
|
|
name = st.text_input( |
|
|
"Construction Name", |
|
|
value=form_state.get("name", editor_state.get("name", "")), |
|
|
help="Unique construction identifier", |
|
|
key="construction_name_input" |
|
|
) |
|
|
filter_component = st.session_state.get("construction_filter", "All") |
|
|
default_component = (filter_component if filter_component in ["Wall", "Roof", "Floor"] |
|
|
else editor_state.get("component_type", "Wall")) |
|
|
component_type = st.selectbox( |
|
|
"Component Type", |
|
|
["Wall", "Roof", "Floor"], |
|
|
index=["Wall", "Roof", "Floor"].index(default_component), |
|
|
help="Building element type", |
|
|
key="construction_component_type_input" |
|
|
) |
|
|
|
|
|
|
|
|
num_layers = st.number_input( |
|
|
"Number of Layers", |
|
|
min_value=1, |
|
|
max_value=10, |
|
|
value=form_state.get("num_layers", len(editor_state.get("layers", [{"material_name": "", "thickness": 0.1}]))), |
|
|
help="Number of material layers", |
|
|
key="construction_num_layers_input" |
|
|
) |
|
|
|
|
|
|
|
|
layers = form_state.get("layers", editor_state.get("layers", [{"material_name": "", "thickness": 0.1}])) |
|
|
if len(layers) < num_layers: |
|
|
layers.extend([{"material_name": "", "thickness": 0.1}] * (num_layers - len(layers))) |
|
|
elif len(layers) > num_layers: |
|
|
layers = layers[:num_layers] |
|
|
|
|
|
|
|
|
st.write("Layers (from outside to inside):") |
|
|
for i in range(num_layers): |
|
|
col1, col2 = st.columns([3, 1]) |
|
|
with col1: |
|
|
material_names = [m.name for m in materials] |
|
|
default_index = 0 |
|
|
if layers[i]["material_name"] in material_names: |
|
|
default_index = material_names.index(layers[i]["material_name"]) |
|
|
layers[i]["material_name"] = st.selectbox( |
|
|
f"Layer {i+1} Material", |
|
|
material_names, |
|
|
index=default_index, |
|
|
key=f"construction_layer_{i}_material" |
|
|
) |
|
|
with col2: |
|
|
layers[i]["thickness"] = st.number_input( |
|
|
f"Layer {i+1} Thickness (m)", |
|
|
min_value=0.001, |
|
|
value=float(layers[i].get("thickness", 0.1)), |
|
|
key=f"construction_layer_{i}_thickness" |
|
|
) |
|
|
|
|
|
|
|
|
st.session_state.construction_form_state = { |
|
|
"name": name, |
|
|
"component_type": component_type, |
|
|
"num_layers": num_layers, |
|
|
"layers": layers |
|
|
} |
|
|
|
|
|
|
|
|
if st.form_submit_button("Save Construction"): |
|
|
action_id = str(uuid.uuid4()) |
|
|
if st.session_state.construction_action.get("id") != action_id: |
|
|
st.session_state.construction_action = {"action": "save", "id": action_id} |
|
|
if use_library_only: |
|
|
st.error("Cannot add/edit constructions in library-only mode.") |
|
|
else: |
|
|
if not name or not name.strip(): |
|
|
st.error("Construction name cannot be empty.") |
|
|
elif (name in st.session_state.project_constructions or name in self.material_library.library_constructions) and (not is_edit or name != original_name): |
|
|
st.error(f"Construction '{name}' already exists.") |
|
|
else: |
|
|
try: |
|
|
|
|
|
construction_layers = [] |
|
|
for layer in layers: |
|
|
material_name = layer["material_name"] |
|
|
thickness = layer["thickness"] |
|
|
material = (st.session_state.project_materials.get(material_name) or |
|
|
self.material_library.library_materials.get(material_name)) |
|
|
if not material: |
|
|
st.error(f"Material '{material_name}' not found.") |
|
|
break |
|
|
construction_layers.append({"material": material, "thickness": thickness}) |
|
|
else: |
|
|
|
|
|
save_as_new = is_edit and name != original_name |
|
|
new_construction = Construction( |
|
|
name=name, |
|
|
component_type=component_type, |
|
|
layers=construction_layers, |
|
|
is_library=False |
|
|
) |
|
|
if is_edit and not save_as_new: |
|
|
success, message = self.material_library.edit_project_construction( |
|
|
original_name, new_construction, st.session_state.project_constructions, st.session_state.components |
|
|
) |
|
|
else: |
|
|
success, message = self.material_library.add_project_construction(new_construction, st.session_state.project_constructions) |
|
|
if success: |
|
|
st.success(message) |
|
|
st.session_state.construction_editor = {} |
|
|
st.session_state.construction_form_state = { |
|
|
"name": "", |
|
|
"component_type": "Wall", |
|
|
"num_layers": 1, |
|
|
"layers": [{"material_name": "", "thickness": 0.1}] |
|
|
} |
|
|
st.session_state.construction_action = {"action": None, "id": None} |
|
|
st.session_state.rerun_trigger = None |
|
|
st.session_state.rerun_pending = True |
|
|
else: |
|
|
st.error(f"Failed to save construction: {message}") |
|
|
except Exception as e: |
|
|
st.error(f"Error saving construction: {str(e)}") |
|
|
|
|
|
if not use_library_only: |
|
|
st.subheader("Project Constructions") |
|
|
construction_df = self.material_library.to_dataframe("constructions", |
|
|
project_constructions=st.session_state.project_constructions, |
|
|
only_project=True) |
|
|
if not construction_df.empty: |
|
|
st.dataframe(construction_df, use_container_width=True) |
|
|
else: |
|
|
st.write("No project constructions to display.") |
|
|
|
|
|
|
|
|
with st.expander("Help"): |
|
|
st.markdown(""" |
|
|
### Material Library Help |
|
|
|
|
|
This section allows you to manage materials and constructions for your building components. |
|
|
|
|
|
**Materials:** |
|
|
- **Library Materials**: Pre-defined materials based on ASHRAE standards |
|
|
- **Project Materials**: Custom materials you create for your project |
|
|
- **Material Editor**: Create new materials or edit existing ones |
|
|
|
|
|
**Constructions:** |
|
|
- **Library Constructions**: Pre-defined construction assemblies |
|
|
- **Project Constructions**: Custom constructions you create |
|
|
- **Construction Editor**: Create multi-layer constructions from materials |
|
|
|
|
|
**Tips:** |
|
|
- Use the filter options to find materials by category |
|
|
- Click "Preview" to view material properties without editing |
|
|
- Click "Copy" to create a project copy of a library material/construction |
|
|
- Materials must be created before they can be used in constructions |
|
|
- Constructions must be created before they can be assigned to building components |
|
|
""") |
|
|
|
|
|
col1, col2 = st.columns(2) |
|
|
with col1: |
|
|
st.button("Back to Climate Data and Design Requirements", key="material_library_back_to_climate", |
|
|
on_click=lambda: setattr(st.session_state, "page", "Climate Data and Design Requirements")) |
|
|
with col2: |
|
|
st.button("Continue to Building Components", key="material_library_to_building_components", |
|
|
on_click=lambda: setattr(st.session_state, "page", "Building Components")) |
|
|
|