BuildSustain-02 / app /internal_loads.py
mabuseif's picture
Update app/internal_loads.py
ff2e49d verified
"""
BuildSustain - Internal Loads Module
This module handles the internal loads functionality of the BuildSustain application,
allowing users to define occupancy, lighting, equipment, ventilation, infiltration, and schedules.
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
import plotly.graph_objects as go
import plotly.express as px
from typing import Dict, List, Any, Optional, Tuple, Union
# Import data from i_l_data.py
from app.i_l_data import PEOPLE_ACTIVITY_LEVELS, DEFAULT_BUILDING_INTERNALS, DEFAULT_SCHEDULE_TEMPLATES, display_internal_loads_help, LIGHTING_FIXTURE_TYPES, DEFAULT_EQUIPMENT_LOADS, DEFAULT_OCCUPANT_DENSITY
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# Define constants
LOAD_TYPES = ["Schedules", "People", "Lighting", "Equipment", "Ventilation", "Infiltration"]
def display_internal_loads_page():
"""
Display the internal loads page.
This is the main function called by main.py when the Internal Loads page is selected.
"""
st.title("Internal Loads")
st.write("Define internal heat gains from people, lighting, equipment, ventilation, and infiltration based on ASHRAE 2021 Handbook.")
# Check if building type is set
building_type = st.session_state.project_data["building_info"].get("building_type")
if not building_type:
st.error("Please select a building type in Building Information.")
if st.button("Go to Building Information", key="internal_to_building"):
st.session_state.current_page = "Building Information"
st.rerun()
return
# Display help information in an expandable section
with st.expander("Help & Information"):
display_internal_loads_help()
# Initialize rerun flag if not present
if "internal_loads_rerun_pending" not in st.session_state:
st.session_state.internal_loads_rerun_pending = False
# Check if rerun is pending
if st.session_state.internal_loads_rerun_pending:
st.session_state.internal_loads_rerun_pending = False
st.rerun()
# Create tabs for different load types
tabs = st.tabs(LOAD_TYPES)
for i, load_type in enumerate(LOAD_TYPES):
with tabs[i]:
if load_type == "Schedules":
display_schedules_tab()
elif load_type == "People":
display_people_tab()
elif load_type == "Lighting":
display_lighting_tab()
elif load_type == "Equipment":
display_equipment_tab()
elif load_type == "Ventilation":
display_ventilation_tab()
elif load_type == "Infiltration":
display_infiltration_tab()
# Navigation buttons
col1, col2 = st.columns(2)
with col1:
if st.button("Back to Building Components", key="back_to_components"):
st.session_state.current_page = "Building Components"
st.rerun()
with col2:
if st.button("Continue to HVAC Loads", key="continue_to_hvac_loads"):
st.session_state.current_page = "HVAC Loads"
st.rerun()
def initialize_internal_loads():
"""
Initialize internal loads in session state if not present.
Logging a warning if initialization is needed for debugging.
"""
if "internal_loads" not in st.session_state.project_data:
logger.warning("Internal loads not initialized by main.py, using fallback initialization")
st.session_state.project_data["internal_loads"] = {
"schedules": dict(DEFAULT_SCHEDULE_TEMPLATES),
"people": [],
"lighting": [],
"equipment": [],
"ventilation": [],
"infiltration": []
}
def display_people_tab():
"""Display the people tab content with a two-column layout similar to components.py."""
# Get people from session state
people_groups = st.session_state.project_data["internal_loads"]["people"]
# Split the display into two columns
col1, col2 = st.columns([3, 2])
with col1:
st.subheader("Saved People Groups")
if people_groups:
display_people_table(people_groups)
else:
st.write("No people groups defined.")
with col2:
st.subheader("People Group Editor/Creator")
# Initialize editor and action states
if "people_editor" not in st.session_state:
st.session_state.people_editor = {}
if "people_action" not in st.session_state:
st.session_state.people_action = {"action": None, "id": None}
# Get building type and floor area for default number of people
building_type = st.session_state.project_data["building_info"].get("building_type")
floor_area = st.session_state.project_data["building_info"].get("floor_area", 100.0)
schedule_type = DEFAULT_BUILDING_INTERNALS.get(building_type, DEFAULT_BUILDING_INTERNALS["Other"])["schedule_type"]
occupant_density = DEFAULT_OCCUPANT_DENSITY.get(schedule_type, {"occupant_densities_PEOPLE_m2": 0.1})["occupant_densities_PEOPLE_m2"]
default_num_people = int(np.ceil(occupant_density * floor_area))
# Display the editor form
with st.form("people_editor_form", clear_on_submit=True):
editor_state = st.session_state.get("people_editor", {})
is_edit = editor_state.get("is_edit", False)
# Group name
name = st.text_input(
"Group Name",
value=editor_state.get("name", ""),
help="Enter a unique name for this people group."
)
# Number of people
num_people = st.number_input(
"Number of People",
min_value=1,
max_value=1000,
value=int(editor_state.get("num_people", default_num_people)),
help="Number of people in this group."
)
# Activity level
activity_level = st.selectbox(
"Activity Level",
list(PEOPLE_ACTIVITY_LEVELS.keys()),
index=list(PEOPLE_ACTIVITY_LEVELS.keys()).index(editor_state.get("activity_level", list(PEOPLE_ACTIVITY_LEVELS.keys())[0])) if editor_state.get("activity_level") in PEOPLE_ACTIVITY_LEVELS else 0,
help="Select the activity level for this group."
)
# Clothing insulation
st.write("**Clothing Insulation:**")
col_summer, col_winter = st.columns(2)
with col_summer:
clo_summer = st.number_input(
"Summer (clo)",
min_value=0.0,
max_value=2.0,
value=float(editor_state.get("clo_summer", 0.5)),
format="%.2f",
help="Clothing insulation for summer conditions."
)
with col_winter:
clo_winter = st.number_input(
"Winter (clo)",
min_value=0.0,
max_value=2.0,
value=float(editor_state.get("clo_winter", 1.0)),
format="%.2f",
help="Clothing insulation for winter conditions."
)
# Schedule selection
available_schedules = list(st.session_state.project_data["internal_loads"]["schedules"].keys())
schedule = st.selectbox(
"Schedule",
available_schedules,
index=available_schedules.index(editor_state.get("schedule", available_schedules[0])) if editor_state.get("schedule") in available_schedules else 0,
help="Select the occupancy schedule for this group."
)
# Submit buttons
col1, col2 = st.columns(2)
with col1:
submit_label = "Update" if is_edit else "Add"
submit = st.form_submit_button(f"{submit_label} People Group")
with col2:
refresh = st.form_submit_button("Refresh")
# Handle form submission
if submit:
# Validate inputs
if not name.strip():
st.error("Group name is required.")
return
# Check for unique name
existing_names = [group["name"] for group in people_groups if not (is_edit and group["name"] == editor_state.get("name"))]
if name.strip() in existing_names:
st.error("Group name must be unique.")
return
# Get activity data
activity_data = PEOPLE_ACTIVITY_LEVELS[activity_level]
# Create people group data
people_data = {
"id": str(uuid.uuid4()),
"name": name.strip(),
"num_people": num_people,
"activity_level": activity_level,
"activity_data": activity_data,
"clo_summer": clo_summer,
"clo_winter": clo_winter,
"schedule": schedule,
"sensible_heat_per_person": (activity_data["sensible_min_w"] + activity_data["sensible_max_w"]) / 2,
"latent_heat_per_person": (activity_data["latent_min_w"] + activity_data["latent_max_w"]) / 2,
"total_sensible_heat": num_people * (activity_data["sensible_min_w"] + activity_data["sensible_max_w"]) / 2,
"total_latent_heat": num_people * (activity_data["latent_min_w"] + activity_data["latent_max_w"]) / 2
}
# Handle edit mode
if is_edit:
index = editor_state.get("index", 0)
st.session_state.project_data["internal_loads"]["people"][index] = people_data
st.success(f"People Group '{name}' updated!")
else:
st.session_state.project_data["internal_loads"]["people"].append(people_data)
st.success(f"People Group '{name}' added!")
# Clear editor state
st.session_state.people_editor = {}
st.session_state.people_action = {"action": "save", "id": str(uuid.uuid4())}
st.session_state.internal_loads_rerun_pending = True
elif refresh:
# Clear editor state
st.session_state.people_editor = {}
st.session_state.people_action = {"action": "refresh", "id": str(uuid.uuid4())}
st.session_state.internal_loads_rerun_pending = True
def display_lighting_tab():
"""Display the lighting tab content with a two-column layout similar to components.py."""
# Get lighting from session state
lighting_systems = st.session_state.project_data["internal_loads"]["lighting"]
# Split the display into two columns
col1, col2 = st.columns([3, 2])
with col1:
st.subheader("Saved Lighting Systems")
if lighting_systems:
display_lighting_table(lighting_systems)
else:
st.write("No lighting systems defined.")
with col2:
st.subheader("Lighting System Editor/Creator")
# Initialize editor and action states
if "lighting_editor" not in st.session_state:
st.session_state.lighting_editor = {}
if "lighting_action" not in st.session_state:
st.session_state.lighting_action = {"action": None, "id": None}
# Get building type for default values
building_type = st.session_state.project_data["building_info"].get("building_type")
default_lighting_density = DEFAULT_BUILDING_INTERNALS.get(building_type, DEFAULT_BUILDING_INTERNALS["Other"])["lighting_density"]
# Display the editor form
with st.form("lighting_editor_form", clear_on_submit=True):
editor_state = st.session_state.get("lighting_editor", {})
is_edit = editor_state.get("is_edit", False)
# System name
name = st.text_input(
"System Name",
value=editor_state.get("name", ""),
help="Enter a unique name for this lighting system."
)
# Area
area = st.number_input(
"Area (m²)",
min_value=1.0,
max_value=100000.0,
value=float(editor_state.get("area", st.session_state.project_data["building_info"].get("floor_area", 100.0))),
format="%.2f",
help="Floor area served by this lighting system."
)
# Lighting power density
lpd = st.number_input(
"Lighting Power Density (W/m²)",
min_value=0.1,
max_value=50.0,
value=float(editor_state.get("lpd", default_lighting_density)),
format="%.2f",
help="Lighting power density in watts per square meter."
)
# Lighting type
lighting_type = st.selectbox(
"Lighting Type",
list(LIGHTING_FIXTURE_TYPES.keys()),
index=list(LIGHTING_FIXTURE_TYPES.keys()).index(editor_state.get("lighting_type", list(LIGHTING_FIXTURE_TYPES.keys())[0])) if editor_state.get("lighting_type") in LIGHTING_FIXTURE_TYPES else 0,
help="Select the type of lighting fixture."
)
# Schedule selection
available_schedules = list(st.session_state.project_data["internal_loads"]["schedules"].keys())
schedule = st.selectbox(
"Schedule",
available_schedules,
index=available_schedules.index(editor_state.get("schedule", available_schedules[0])) if editor_state.get("schedule") in available_schedules else 0,
help="Select the lighting schedule for this system."
)
# Submit buttons
col1, col2 = st.columns(2)
with col1:
submit_label = "Update" if is_edit else "Add"
submit = st.form_submit_button(f"{submit_label} Lighting System")
with col2:
refresh = st.form_submit_button("Refresh")
# Handle form submission
if submit:
# Validate inputs
if not name.strip():
st.error("System name is required.")
return
# Check for unique name
existing_names = [system["name"] for system in lighting_systems if not (is_edit and system["name"] == editor_state.get("name"))]
if name.strip() in existing_names:
st.error("System name must be unique.")
return
# Get lighting fixture data
fixture_data = LIGHTING_FIXTURE_TYPES[lighting_type]
# Create lighting system data
lighting_data = {
"id": str(uuid.uuid4()),
"name": name.strip(),
"area": area,
"lpd": lpd,
"total_power": area * lpd,
"lighting_type": lighting_type,
"radiative_fraction": fixture_data["radiative"] / 100.0,
"convective_fraction": fixture_data["convective"] / 100.0,
"schedule": schedule
}
# Handle edit mode
if is_edit:
index = editor_state.get("index", 0)
st.session_state.project_data["internal_loads"]["lighting"][index] = lighting_data
st.success(f"Lighting System '{name}' updated!")
else:
st.session_state.project_data["internal_loads"]["lighting"].append(lighting_data)
st.success(f"Lighting System '{name}' added!")
# Clear editor state
st.session_state.lighting_editor = {}
st.session_state.lighting_action = {"action": "save", "id": str(uuid.uuid4())}
st.session_state.internal_loads_rerun_pending = True
elif refresh:
# Clear editor state
st.session_state.lighting_editor = {}
st.session_state.lighting_action = {"action": "refresh", "id": str(uuid.uuid4())}
st.session_state.internal_loads_rerun_pending = True
def display_equipment_tab():
"""Display the equipment tab content with a two-column layout similar to components.py."""
# Get equipment from session state
equipment_systems = st.session_state.project_data["internal_loads"]["equipment"]
# Split the display into two columns
col1, col2 = st.columns([3, 2])
with col1:
st.subheader("Saved Equipment")
if equipment_systems:
display_equipment_table(equipment_systems)
else:
st.write("No equipment defined.")
with col2:
st.subheader("Equipment Editor/Creator")
# Initialize editor and action states
if "equipment_editor" not in st.session_state:
st.session_state.equipment_editor = {}
if "equipment_action" not in st.session_state:
st.session_state.equipment_action = {"action": None, "id": None}
# Get building type and corresponding schedule type for default values
building_type = st.session_state.project_data["building_info"].get("building_type")
schedule_type = DEFAULT_BUILDING_INTERNALS.get(building_type, DEFAULT_BUILDING_INTERNALS["Other"])["schedule_type"]
default_equipment_data = DEFAULT_EQUIPMENT_LOADS.get(schedule_type, DEFAULT_EQUIPMENT_LOADS["default"])
# Display the editor form
with st.form("equipment_editor_form", clear_on_submit=True):
editor_state = st.session_state.get("equipment_editor", {})
is_edit = editor_state.get("is_edit", False)
# Equipment name
name = st.text_input(
"Equipment Name",
value=editor_state.get("name", ""),
help="Enter a unique name for this equipment."
)
# Area
area = st.number_input(
"Area (m²)",
min_value=1.0,
max_value=100000.0,
value=float(editor_state.get("area", st.session_state.project_data["building_info"].get("floor_area", 100.0))),
format="%.2f",
help="Floor area served by this equipment."
)
# Equipment load
equipment_load = st.number_input(
"Equipment Load (W/m²)",
min_value=0.0,
max_value=200.0,
value=float(editor_state.get("equipment_load", default_equipment_data["equipment_load_w_m2"])),
format="%.2f",
help="Total equipment load in watts per square meter."
)
# Heat gains
st.write("**Heat Gains (W/m²):**")
col_sens, col_lat = st.columns(2)
with col_sens:
sensible_gain = st.number_input(
"Sensible Heat Gain",
min_value=0.0,
max_value=200.0,
value=float(editor_state.get("sensible_gain", default_equipment_data["equipment_load_w_m2"] * (default_equipment_data["sensible_percent"] / 100))),
format="%.2f",
help="Sensible heat gain in watts per square meter."
)
with col_lat:
latent_gain = st.number_input(
"Latent Heat Gain",
min_value=0.0,
max_value=200.0,
value=float(editor_state.get("latent_gain", default_equipment_data["equipment_load_w_m2"] * (default_equipment_data["latent_percent"] / 100))),
format="%.2f",
help="Latent heat gain in watts per square meter."
)
# Heat distribution
st.write("**Heat Distribution:**")
col_rad, col_conv = st.columns(2)
with col_rad:
radiative_fraction = st.number_input(
"Radiative Fraction",
min_value=0.0,
max_value=1.0,
value=float(editor_state.get("radiative_fraction", default_equipment_data["radiant_percent"] / 100)),
format="%.2f",
help="Fraction of sensible heat released as radiation."
)
with col_conv:
convective_fraction = st.number_input(
"Convective Fraction",
min_value=0.0,
max_value=1.0,
value=float(editor_state.get("convective_fraction", default_equipment_data["convective_percent"] / 100)),
format="%.2f",
help="Fraction of sensible heat released as convection."
)
# Schedule selection
available_schedules = list(st.session_state.project_data["internal_loads"]["schedules"].keys())
schedule = st.selectbox(
"Schedule",
available_schedules,
index=available_schedules.index(editor_state.get("schedule", schedule_type)) if editor_state.get("schedule") in available_schedules else available_schedules.index(schedule_type) if schedule_type in available_schedules else 0,
help="Select the equipment schedule."
)
# Submit buttons
col1, col2 = st.columns(2)
with col1:
submit_label = "Update" if is_edit else "Add"
submit = st.form_submit_button(f"{submit_label} Equipment")
with col2:
refresh = st.form_submit_button("Refresh")
# Handle form submission
if submit:
# Validate inputs
if not name.strip():
st.error("Equipment name is required.")
return
if abs(radiative_fraction + convective_fraction - 1.0) > 0.01:
st.error("Radiative and convective fractions must sum to 1.0")
return
if abs(sensible_gain + latent_gain - equipment_load) > 0.01:
st.error("Sensible and latent heat gains must sum to the equipment load.")
return
# Check for unique name
existing_names = [system["name"] for system in equipment_systems if not (is_edit and system["name"] == editor_state.get("name"))]
if name.strip() in existing_names:
st.error("Equipment name must be unique.")
return
# Create equipment data
equipment_data = {
"id": str(uuid.uuid4()),
"name": name.strip(),
"area": area,
"equipment_load": equipment_load,
"sensible_gain": sensible_gain,
"latent_gain": latent_gain,
"total_sensible_power": area * sensible_gain,
"total_latent_power": area * latent_gain,
"radiative_fraction": radiative_fraction,
"convective_fraction": convective_fraction,
"schedule": schedule
}
# Handle edit mode
if is_edit:
index = editor_state.get("index", 0)
st.session_state.project_data["internal_loads"]["equipment"][index] = equipment_data
st.success(f"Equipment '{name}' updated!")
else:
st.session_state.project_data["internal_loads"]["equipment"].append(equipment_data)
st.success(f"Equipment '{name}' added!")
# Clear editor state
st.session_state.equipment_editor = {}
st.session_state.equipment_action = {"action": "save", "id": str(uuid.uuid4())}
st.session_state.internal_loads_rerun_pending = True
elif refresh:
# Clear editor state
st.session_state.equipment_editor = {}
st.session_state.equipment_action = {"action": "refresh", "id": str(uuid.uuid4())}
st.session_state.internal_loads_rerun_pending = True
def display_ventilation_tab():
"""Display the ventilation tab content with a two-column layout."""
# Get ventilation systems from session state
ventilation_systems = st.session_state.project_data["internal_loads"].setdefault("ventilation", [])
# Split the display into two columns
col1, col2 = st.columns([3, 2])
with col1:
st.subheader("Saved Ventilation Systems")
if ventilation_systems:
display_ventilation_table(ventilation_systems)
else:
st.write("No ventilation systems defined.")
with col2:
st.subheader("Ventilation System Editor/Creator")
# Initialize editor and action states
if "ventilation_editor" not in st.session_state:
st.session_state.ventilation_editor = {}
if "ventilation_action" not in st.session_state:
st.session_state.ventilation_action = {"action": None, "id": None}
# Get building type for default values
building_type = st.session_state.project_data["building_info"].get("building_type")
default_building_data = DEFAULT_BUILDING_INTERNALS.get(building_type, DEFAULT_BUILDING_INTERNALS["Other"])
default_ventilation_rate = default_building_data.get("ventilation_rate", 1.0) # L/s·m²
# System type selection (outside form)
system_type = st.selectbox(
"System Type",
["AirChanges/Hour", "Wind and Stack Open Area", "Balanced Flow", "Heat Recovery"],
key="ventilation_system_type",
help="Select the type of ventilation system."
)
# Display the editor form
with st.form("ventilation_editor_form", clear_on_submit=True):
editor_state = st.session_state.get("ventilation_editor", {})
is_edit = editor_state.get("is_edit", False)
# System name
name = st.text_input(
"System Name",
value=editor_state.get("name", ""),
help="Enter a unique name for this ventilation system."
)
# Area (required for all types)
area = st.number_input(
"Area (m²)",
min_value=1.0,
max_value=100000.0,
value=float(editor_state.get("area", st.session_state.project_data["building_info"].get("floor_area", 100.0))),
format="%.2f",
help="Floor area served by this system."
)
# Schedule selection
available_schedules = list(st.session_state.project_data["internal_loads"]["schedules"].keys())
schedule = st.selectbox(
"Schedule",
available_schedules,
index=available_schedules.index(editor_state.get("schedule", available_schedules[0])) if editor_state.get("schedule") in available_schedules else 0,
help="Select the operation schedule for this system."
)
# Type-specific inputs
if system_type == "AirChanges/Hour":
design_flow_rate = st.number_input(
"Design Flow Rate (L/s·m²)",
min_value=0.1,
max_value=50.0,
value=float(editor_state.get("design_flow_rate", default_ventilation_rate)),
format="%.2f",
help="Ventilation rate in liters per second per square meter."
)
ventilation_type = st.selectbox(
"Ventilation Type",
["Natural", "Mechanical"],
index=["Natural", "Mechanical"].index(editor_state.get("ventilation_type", "Natural")) if editor_state.get("ventilation_type") in ["Natural", "Mechanical"] else 0,
help="Select whether the ventilation is natural or mechanical."
)
fan_pressure_rise = 0.0
fan_efficiency = 0.0
if ventilation_type == "Mechanical":
fan_pressure_rise = st.number_input(
"Fan Pressure Rise (Pa)",
min_value=0.0,
max_value=1000.0,
value=float(editor_state.get("fan_pressure_rise", 200.0)),
format="%.2f",
help="Fan pressure rise in Pascals for power calculation."
)
fan_efficiency = st.number_input(
"Fan Efficiency (0–1)",
min_value=0.0,
max_value=1.0,
value=float(editor_state.get("fan_efficiency", 0.7)),
format="%.2f",
help="Fan efficiency as a fraction between 0 and 1."
)
sensible_effectiveness = 0.0
latent_effectiveness = 0.0
elif system_type == "Wind and Stack Open Area":
opening_effectiveness = st.number_input(
"Opening Effectiveness (%)",
min_value=0.0,
max_value=100.0,
value=float(editor_state.get("opening_effectiveness", 50.0)),
format="%.2f",
help="Effectiveness of the opening for ventilation (0–100%)."
)
design_flow_rate = 0.0
ventilation_type = ""
fan_pressure_rise = 0.0
fan_efficiency = 0.0
sensible_effectiveness = 0.0
latent_effectiveness = 0.0
elif system_type == "Balanced Flow":
design_flow_rate = st.number_input(
"Design Flow Rate (L/s·m²)",
min_value=0.0,
max_value=50.0,
value=float(editor_state.get("design_flow_rate", default_ventilation_rate)),
format="%.2f",
help="Balanced supply and exhaust flow rate in liters per second per square meter."
)
fan_pressure_rise = st.number_input(
"Fan Pressure Rise (Pa)",
min_value=0.0,
max_value=1000.0,
value=float(editor_state.get("fan_pressure_rise", 200.0)),
format="%.2f",
help="Fan pressure rise in Pascals for power calculation."
)
fan_efficiency = st.number_input(
"Fan Total Efficiency (0–1)",
min_value=0.0,
max_value=1.0,
value=float(editor_state.get("fan_efficiency", 0.7)),
format="%.2f",
help="Total fan efficiency as a fraction between 0 and 1."
)
ventilation_type = ""
sensible_effectiveness = 0.0
latent_effectiveness = 0.0
elif system_type == "Heat Recovery":
design_flow_rate = st.number_input(
"Design Flow Rate (L/s·m²)",
min_value=0.0,
max_value=50.0,
value=float(editor_state.get("design_flow_rate", default_ventilation_rate)),
format="%.2f",
help="Balanced supply and exhaust flow rate in liters per second per square meter."
)
fan_pressure_rise = st.number_input(
"Fan Pressure Rise (Pa)",
min_value=0.0,
max_value=1000.0,
value=float(editor_state.get("fan_pressure_rise", 200.0)),
format="%.2f",
help="Fan pressure rise in Pascals for power calculation."
)
fan_efficiency = st.number_input(
"Fan Total Efficiency (0–1)",
min_value=0.0,
max_value=1.0,
value=float(editor_state.get("fan_efficiency", 0.7)),
format="%.2f",
help="Total fan efficiency as a fraction between 0 and 1."
)
sensible_effectiveness = st.number_input(
"Sensible Effectiveness (0–1)",
min_value=0.0,
max_value=1.0,
value=float(editor_state.get("sensible_effectiveness", 0.5)),
format="%.2f",
help="Sensible heat recovery effectiveness as a fraction between 0 and 1."
)
latent_effectiveness = st.number_input(
"Latent Effectiveness (0–1)",
min_value=0.0,
max_value=1.0,
value=float(editor_state.get("latent_effectiveness", 0.5)),
format="%.2f",
help="Latent heat recovery effectiveness as a fraction between 0 and 1."
)
ventilation_type = ""
# Submit buttons
col1, col2 = st.columns(2)
with col1:
submit_label = "Update" if is_edit else "Add"
submit = st.form_submit_button(f"{submit_label} Ventilation System")
with col2:
refresh = st.form_submit_button("Refresh")
# Handle form submission
if submit:
# Validate inputs
if not name.strip():
st.error("System name is required.")
return
# Check for unique name
existing_names = [system["name"] for system in ventilation_systems if not (is_edit and system["name"] == editor_state.get("name"))]
if name.strip() in existing_names:
st.error("System name must be unique.")
return
# Create ventilation data
ventilation_data = {
"id": str(uuid.uuid4()),
"name": name.strip(),
"system_type": system_type,
"area": area,
"schedule": schedule,
"design_flow_rate": design_flow_rate,
"ventilation_type": ventilation_type,
"fan_pressure_rise": fan_pressure_rise,
"fan_efficiency": fan_efficiency,
"opening_effectiveness": opening_effectiveness if system_type == "Wind and Stack Open Area" else 0.0,
"sensible_effectiveness": sensible_effectiveness,
"latent_effectiveness": latent_effectiveness
}
# Handle edit mode
if is_edit:
index = editor_state.get("index", 0)
st.session_state.project_data["internal_loads"]["ventilation"][index] = ventilation_data
st.success(f"Ventilation System '{name}' updated!")
else:
st.session_state.project_data["internal_loads"]["ventilation"].append(ventilation_data)
st.success(f"Ventilation System '{name}' added!")
# Clear editor state
st.session_state.ventilation_editor = {}
st.session_state.ventilation_action = {"action": "save", "id": str(uuid.uuid4())}
st.session_state.internal_loads_rerun_pending = True
elif refresh:
# Clear editor state
st.session_state.ventilation_editor = {}
st.session_state.ventilation_action = {"action": "refresh", "id": str(uuid.uuid4())}
st.session_state.internal_loads_rerun_pending = True
def display_infiltration_tab():
"""Display the infiltration tab content with a two-column layout."""
# Get infiltration systems from session state
infiltration_systems = st.session_state.project_data["internal_loads"].setdefault("infiltration", [])
# Split the display into two columns
col1, col2 = st.columns([3, 2])
with col1:
st.subheader("Saved Infiltration Systems")
if infiltration_systems:
display_infiltration_table(infiltration_systems)
else:
st.write("No infiltration systems defined.")
with col2:
st.subheader("Infiltration System Editor/Creator")
# Initialize editor and action states
if "infiltration_editor" not in st.session_state:
st.session_state.infiltration_editor = {}
if "infiltration_action" not in st.session_state:
st.session_state.infiltration_action = {"action": None, "id": None}
# Get building type for default values
building_type = st.session_state.project_data["building_info"].get("building_type")
default_building_data = DEFAULT_BUILDING_INTERNALS.get(building_type, DEFAULT_BUILDING_INTERNALS["Other"])
default_air_change_rate = default_building_data.get("air_change_rate", 0.3) # ACH
# System type selection (outside form)
system_type = st.selectbox(
"System Type",
["AirChanges/Hour", "Effective Leakage Area", "Flow Coefficient"],
key="infiltration_system_type",
help="Select the type of infiltration system."
)
# Display the editor form
with st.form("infiltration_editor_form", clear_on_submit=True):
editor_state = st.session_state.get("infiltration_editor", {})
is_edit = editor_state.get("is_edit", False)
# System name
name = st.text_input(
"System Name",
value=editor_state.get("name", ""),
help="Enter a unique name for this infiltration system."
)
# Area (required for all types)
area = st.number_input(
"Area (m²)",
min_value=1.0,
max_value=100000.0,
value=float(editor_state.get("area", st.session_state.project_data["building_info"].get("floor_area", 100.0))),
format="%.2f",
help="Floor area served by this system."
)
# Schedule selection
available_schedules = list(st.session_state.project_data["internal_loads"]["schedules"].keys())
schedule = st.selectbox(
"Schedule",
available_schedules,
index=available_schedules.index(editor_state.get("schedule", available_schedules[0])) if editor_state.get("schedule") in available_schedules else 0,
help="Select the operation schedule for this system."
)
# Type-specific inputs
if system_type == "AirChanges/Hour":
design_flow_rate = st.number_input(
"Design Flow Rate (ACH)",
min_value=0.0,
max_value=10.0,
value=float(editor_state.get("design_flow_rate", default_air_change_rate)),
format="%.2f",
help="Air change rate in air changes per hour."
)
effective_air_leakage_area = 0.0
stack_coefficient = 0.0
wind_coefficient = 0.0
flow_coefficient = 0.0
pressure_exponent = 0.0
elif system_type == "Effective Leakage Area":
effective_air_leakage_area = st.number_input(
"Effective Air Leakage Area (cm²)",
min_value=0.0,
max_value=10000.0,
value=float(editor_state.get("effective_air_leakage_area", 100.0)),
format="%.2f",
help="Effective air leakage area in square centimeters at 4 Pa or 10 Pa."
)
stack_coefficient = st.number_input(
"Stack Coefficient",
min_value=0.0,
max_value=1.0,
value=float(editor_state.get("stack_coefficient", 0.0001)),
format="%.6f",
help="Stack coefficient from standards or measurements."
)
wind_coefficient = st.number_input(
"Wind Coefficient",
min_value=0.0,
max_value=1.0,
value=float(editor_state.get("wind_coefficient", 0.0001)),
format="%.6f",
help="Wind coefficient from standards or measurements."
)
design_flow_rate = 0.0
flow_coefficient = 0.0
pressure_exponent = 0.0
elif system_type == "Flow Coefficient":
flow_coefficient = st.number_input(
"Flow Coefficient (m³/s·Paⁿ)",
min_value=0.0,
max_value=1.0,
value=float(editor_state.get("flow_coefficient", 0.0001)),
format="%.6f",
help="Airflow at reference pressure in cubic meters per second per Pascal raised to the pressure exponent."
)
pressure_exponent = st.number_input(
"Pressure Exponent",
min_value=0.0,
max_value=1.0,
value=float(editor_state.get("pressure_exponent", 0.6)),
format="%.2f",
help="Pressure exponent, typically 0.6."
)
stack_coefficient = st.number_input(
"Stack Coefficient",
min_value=0.0,
max_value=1.0,
value=float(editor_state.get("stack_coefficient", 0.0001)),
format="%.6f",
help="Stack coefficient from standards or measurements."
)
wind_coefficient = st.number_input(
"Wind Coefficient",
min_value=0.0,
max_value=1.0,
value=float(editor_state.get("wind_coefficient", 0.0001)),
format="%.6f",
help="Wind coefficient from standards or measurements."
)
design_flow_rate = 0.0
effective_air_leakage_area = 0.0
# Submit buttons
col1, col2 = st.columns(2)
with col1:
submit_label = "Update" if is_edit else "Add"
submit = st.form_submit_button(f"{submit_label} Infiltration System")
with col2:
refresh = st.form_submit_button("Refresh")
# Handle form submission
if submit:
# Validate inputs
if not name.strip():
st.error("System name is required.")
return
# Check for unique name
existing_names = [system["name"] for system in infiltration_systems if not (is_edit and system["name"] == editor_state.get("name"))]
if name.strip() in existing_names:
st.error("System name must be unique.")
return
# Create infiltration data
infiltration_data = {
"id": str(uuid.uuid4()),
"name": name.strip(),
"system_type": system_type,
"area": area,
"schedule": schedule,
"design_flow_rate": design_flow_rate,
"effective_air_leakage_area": effective_air_leakage_area,
"stack_coefficient": stack_coefficient,
"wind_coefficient": wind_coefficient,
"flow_coefficient": flow_coefficient,
"pressure_exponent": pressure_exponent
}
# Handle edit mode
if is_edit:
index = editor_state.get("index", 0)
st.session_state.project_data["internal_loads"]["infiltration"][index] = infiltration_data
st.success(f"Infiltration System '{name}' updated!")
else:
st.session_state.project_data["internal_loads"]["infiltration"].append(infiltration_data)
st.success(f"Infiltration System '{name}' added!")
# Clear editor state
st.session_state.infiltration_editor = {}
st.session_state.infiltration_action = {"action": "save", "id": str(uuid.uuid4())}
st.session_state.internal_loads_rerun_pending = True
elif refresh:
# Clear editor state
st.session_state.infiltration_editor = {}
st.session_state.infiltration_action = {"action": "refresh", "id": str(uuid.uuid4())}
st.session_state.internal_loads_rerun_pending = True
def display_schedules_tab():
st.subheader("Schedules Editor")
DEFAULT_STATE = {
"name": "",
"description": "",
"template": "None",
"weekday": [0.0] * 24,
"weekend": [0.0] * 24,
"is_edit": False,
"original_name": ""
}
# Initialize schedule_editor and schedule_action if not present
if "schedule_editor" not in st.session_state:
st.session_state.schedule_editor = DEFAULT_STATE.copy()
if "schedule_action" not in st.session_state:
st.session_state.schedule_action = {"action": None, "id": None}
editor_state = st.session_state.schedule_editor
schedules = st.session_state.project_data["internal_loads"]["schedules"]
# Get building type and map to schedule type for default template
building_type = st.session_state.project_data["building_info"].get("building_type", "Other")
schedule_type = DEFAULT_BUILDING_INTERNALS.get(building_type, DEFAULT_BUILDING_INTERNALS["Other"])["schedule_type"]
default_template = schedule_type if schedule_type in DEFAULT_SCHEDULE_TEMPLATES else "None"
# Handle template change
template_options = list(DEFAULT_SCHEDULE_TEMPLATES.keys())
selected_template = st.selectbox(
"Select Template",
options=["None"] + template_options,
index=template_options.index(editor_state.get("template", default_template)) + 1
if editor_state.get("template") in template_options else template_options.index(default_template) + 1
if default_template in template_options else 0,
help="Choose a base schedule to prefill values."
)
# Update sliders only if template changes and not in edit mode
if selected_template != editor_state.get("template", "None") and not editor_state.get("is_edit"):
st.session_state.schedule_editor["template"] = selected_template
if selected_template != "None":
tpl = DEFAULT_SCHEDULE_TEMPLATES[selected_template]
st.session_state.schedule_editor["weekday"] = tpl["weekday"]
st.session_state.schedule_editor["weekend"] = tpl["weekend"]
for hour in range(24):
st.session_state[f"weekday_slider_{hour}_value"] = tpl["weekday"][hour]
st.session_state[f"weekend_slider_{hour}_value"] = tpl["weekend"][hour]
else:
for hour in range(24):
st.session_state[f"weekday_slider_{hour}_value"] = 0.0
st.session_state[f"weekend_slider_{hour}_value"] = 0.0
st.session_state.internal_loads_rerun_pending = True
st.rerun()
# UI FORM for name/description and actions
with st.form("schedule_form"):
name = st.text_input("Schedule Name", value=editor_state.get("name", ""))
description = st.text_area("Description", value=editor_state.get("description", ""))
# SLIDERS LAYOUT
st.markdown("### Schedule Sliders")
col_hour, col_wd, col_we = st.columns([0.4, 2.0, 2.0])
with col_hour:
st.markdown("<div style='text-align:center; font-weight:bold; font-size:18px;'>Hour</div>", unsafe_allow_html=True)
with col_wd:
st.markdown("<div style='text-align:center; font-weight:bold; font-size:18px;'>Weekday</div>", unsafe_allow_html=True)
with col_we:
st.markdown("<div style='text-align:center; font-weight:bold; font-size:18px;'>Weekend</div>", unsafe_allow_html=True)
hide_elements = """
<style>
div[data-testid="stSliderTickBarMin"],
div[data-testid="stSliderTickBarMax"] {
display: none;
}
</style>
"""
st.markdown(hide_elements, unsafe_allow_html=True)
weekday_values = []
weekend_values = []
for hour in range(24):
col_hour, col_wd, col_we = st.columns([0.4, 2.0, 2.0])
with col_hour:
st.markdown(f"<div style='text-align:center; font-size:12px'>{hour:02d}</div>", unsafe_allow_html=True)
with col_wd:
val = st.slider(
label=f"Weekday {hour:02d}",
key=f"weekday_slider_{hour}",
min_value=0.0,
max_value=1.0,
step=0.1,
value=st.session_state.get(f"weekday_slider_{hour}_value", 0.0),
label_visibility="collapsed",
format=None
)
weekday_values.append(val)
with col_we:
val = st.slider(
label=f"Weekend {hour:02d}",
key=f"weekend_slider_{hour}",
min_value=0.0,
max_value=1.0,
step=0.1,
value=st.session_state.get(f"weekend_slider_{hour}_value", 0.0),
label_visibility="collapsed",
format=None
)
weekend_values.append(val)
# Action Buttons
col1, col2 = st.columns(2)
with col1:
submit_label = "Update Schedule" if editor_state.get("is_edit") else "Save Schedule"
submitted = st.form_submit_button(submit_label)
with col2:
refresh = st.form_submit_button("Refresh")
# Save logic
if submitted:
if not name.strip():
st.error("Schedule name is required.")
return
if name in schedules and not editor_state.get("is_edit"):
st.error("A schedule with this name already exists.")
return
schedules[name] = {
"description": description,
"weekday": weekday_values,
"weekend": weekend_values
}
if editor_state.get("is_edit") and editor_state.get("original_name") and editor_state.get("original_name") != name:
if editor_state["original_name"] in schedules:
del schedules[editor_state["original_name"]]
st.session_state.schedule_editor = DEFAULT_STATE.copy()
for hour in range(24):
st.session_state[f"weekday_slider_{hour}_value"] = 0.0
st.session_state[f"weekend_slider_{hour}_value"] = 0.0
st.success(f"Schedule '{name}' saved successfully.")
st.session_state.schedule_action = {"action": "save", "id": str(uuid.uuid4())}
st.session_state.internal_loads_rerun_pending = True
# Refresh logic
if refresh:
st.session_state.schedule_editor = DEFAULT_STATE.copy()
for hour in range(24):
st.session_state[f"weekday_slider_{hour}_value"] = 0.0
st.session_state[f"weekend_slider_{hour}_value"] = 0.0
st.session_state.schedule_action = {"action": "refresh", "id": str(uuid.uuid4())}
st.session_state.internal_loads_rerun_pending = True
# Display saved schedules in a table
st.subheader("Saved Schedules")
if schedules:
# Create column headers
cols = st.columns([2, 3, 1, 1, 1, 1])
cols[0].write("**Name**")
cols[1].write("**Description**")
cols[2].write("**Weekday Peak**")
cols[3].write("**Weekend Peak**")
cols[4].write("**Edit**")
cols[5].write("**Delete**")
# Display each schedule
for idx, (name, schedule) in enumerate(schedules.items()):
cols = st.columns([2, 3, 1, 1, 1, 1])
cols[0].write(name)
cols[1].write(schedule.get("description", "No description"))
cols[2].write(f"{max(schedule.get('weekday', [0])):.2f}")
cols[3].write(f"{max(schedule.get('weekend', [0])):.2f}")
# Edit button
edit_key = f"edit_schedule_{name}_{idx}"
with cols[4].container():
if st.button("Edit", key=edit_key):
if name in DEFAULT_SCHEDULE_TEMPLATES:
st.error("Default schedules cannot be edited.")
else:
schedule_data = schedules[name]
st.session_state.schedule_editor = {
"is_edit": True,
"original_name": name,
"name": name,
"description": schedule_data.get("description", ""),
"weekday": schedule_data.get("weekday", [0.0] * 24),
"weekend": schedule_data.get("weekend", [0.0] * 24),
"template": "None"
}
for hour in range(24):
st.session_state[f"weekday_slider_{hour}_value"] = schedule_data.get("weekday", [0.0] * 24)[hour]
st.session_state[f"weekend_slider_{hour}_value"] = schedule_data.get("weekend", [0.0] * 24)[hour]
st.session_state.schedule_action = {"action": "edit", "id": str(uuid.uuid4())}
st.session_state.internal_loads_rerun_pending = True
# Delete button
delete_key = f"delete_schedule_{name}_{idx}"
with cols[5].container():
if name in DEFAULT_SCHEDULE_TEMPLATES:
cols[5].write("(Default)")
elif is_schedule_in_use(name):
cols[5].write("(In Use)")
else:
if st.button("Delete", key=delete_key):
del schedules[name]
st.session_state.schedule_editor = DEFAULT_STATE.copy()
for hour in range(24):
st.session_state[f"weekday_slider_{hour}_value"] = 0.0
st.session_state[f"weekend_slider_{hour}_value"] = 0.0
st.success(f"Schedule '{name}' deleted!")
st.session_state.schedule_action = {"action": "delete", "id": str(uuid.uuid4())}
st.session_state.internal_loads_rerun_pending = True
else:
st.write("No schedules defined.")
def is_schedule_in_use(schedule_name: str) -> bool:
"""
Check if a schedule is in use by any people, lighting, equipment, or ventilation/infiltration systems.
Args:
schedule_name: Name of the schedule to check.
Returns:
bool: True if the schedule is in use, False otherwise.
"""
internal_loads = st.session_state.project_data["internal_loads"]
for group in internal_loads.get("people", []):
if group.get("schedule") == schedule_name:
return True
for system in internal_loads.get("lighting", []):
if system.get("schedule") == schedule_name:
return True
for system in internal_loads.get("equipment", []):
if system.get("schedule") == schedule_name:
return True
for system in internal_loads.get("ventilation_infiltration", []):
if system.get("schedule") == schedule_name:
return True
return False
def display_people_table(people_groups: List[Dict[str, Any]]):
"""Display people groups in a table format with edit/delete buttons."""
# Create column headers
cols = st.columns([2, 1, 1, 1, 1, 1])
cols[0].write("**Name**")
cols[1].write("**Number**")
cols[2].write("**Sensible (W)**")
cols[3].write("**Latent (W)**")
cols[4].write("**Edit**")
cols[5].write("**Delete**")
# Display each group
for idx, group in enumerate(people_groups):
cols = st.columns([2, 1, 1, 1, 1, 1])
cols[0].write(group["name"])
cols[1].write(str(group.get("num_people", 0)))
cols[2].write(f"{group.get('total_sensible_heat', 0):.1f}")
cols[3].write(f"{group.get('total_latent_heat', 0):.1f}")
# Edit button
edit_key = f"edit_people_{group['name']}_{idx}"
with cols[4].container():
if st.button("Edit", key=edit_key):
st.session_state.people_editor = {
"index": idx,
"name": group.get("name", ""),
"num_people": group.get("num_people", 10),
"activity_level": group.get("activity_level", list(PEOPLE_ACTIVITY_LEVELS.keys())[0]),
"clo_summer": group.get("clo_summer", 0.5),
"clo_winter": group.get("clo_winter", 1.0),
"schedule": group.get("schedule", "Continuous"),
"is_edit": True
}
st.session_state.people_action = {"action": "edit", "id": str(uuid.uuid4())}
st.session_state.internal_loads_rerun_pending = True
# Delete button
delete_key = f"delete_people_{group['name']}_{idx}"
with cols[5].container():
if st.button("Delete", key=delete_key):
st.session_state.project_data["internal_loads"]["people"].pop(idx)
st.success(f"People Group '{group['name']}' deleted!")
st.session_state.people_action = {"action": "delete", "id": str(uuid.uuid4())}
st.session_state.internal_loads_rerun_pending = True
def display_lighting_table(lighting_systems: List[Dict[str, Any]]):
"""Display lighting systems in a table format with edit/delete buttons."""
# Create column headers
cols = st.columns([2, 1, 1, 1, 1, 1])
cols[0].write("**Name**")
cols[1].write("**Area (m²)**")
cols[2].write("**LPD (W/m²)**")
cols[3].write("**Total Power (W)**")
cols[4].write("**Edit**")
cols[5].write("**Delete**")
# Display each system
for idx, system in enumerate(lighting_systems):
cols = st.columns([2, 1, 1, 1, 1, 1])
cols[0].write(system["name"])
cols[1].write(f"{system.get('area', 0):.1f}")
cols[2].write(f"{system.get('lpd', 0):.2f}")
cols[3].write(f"{system.get('total_power', 0):.1f}")
# Edit button
edit_key = f"edit_lighting_{system['name']}_{idx}"
with cols[4].container():
if st.button("Edit", key=edit_key):
st.session_state.lighting_editor = {
"index": idx,
"name": system.get("name", ""),
"area": system.get("area", 100.0),
"lpd": system.get("lpd", 12.0),
"lighting_type": system.get("lighting_type", list(LIGHTING_FIXTURE_TYPES.keys())[0]),
"schedule": system.get("schedule", "Continuous"),
"is_edit": True
}
st.session_state.lighting_action = {"action": "edit", "id": str(uuid.uuid4())}
st.session_state.internal_loads_rerun_pending = True
# Delete button
delete_key = f"delete_lighting_{system['name']}_{idx}"
with cols[5].container():
if st.button("Delete", key=delete_key):
st.session_state.project_data["internal_loads"]["lighting"].pop(idx)
st.success(f"Lighting System '{system['name']}' deleted!")
st.session_state.lighting_action = {"action": "delete", "id": str(uuid.uuid4())}
st.session_state.internal_loads_rerun_pending = True
def display_equipment_table(equipment_systems: List[Dict[str, Any]]):
"""Display equipment systems in a table format with edit/delete buttons."""
# Create column headers
cols = st.columns([2, 1, 1, 1, 1, 1])
cols[0].write("**Name**")
cols[1].write("**Area (m²)**")
cols[2].write("**Sensible (W)**")
cols[3].write("**Latent (W)**")
cols[4].write("**Edit**")
cols[5].write("**Delete**")
# Display each system
for idx, system in enumerate(equipment_systems):
cols = st.columns([2, 1, 1, 1, 1, 1])
cols[0].write(system["name"])
cols[1].write(f"{system.get('area', 0):.1f}")
cols[2].write(f"{system.get('total_sensible_power', 0):.1f}")
cols[3].write(f"{system.get('total_latent_power', 0):.1f}")
# Edit button
edit_key = f"edit_equipment_{system['name']}_{idx}"
with cols[4].container():
if st.button("Edit", key=edit_key):
st.session_state.equipment_editor = {
"index": idx,
"name": system.get("name", ""),
"area": system.get("area", 100.0),
"sensible_gain": system.get("sensible_gain", 10.0),
"latent_gain": system.get("latent_gain", 2.0),
"radiative_fraction": system.get("radiative_fraction", 0.5),
"convective_fraction": system.get("convective_fraction", 0.5),
"schedule": system.get("schedule", "Continuous"),
"is_edit": True
}
st.session_state.equipment_action = {"action": "edit", "id": str(uuid.uuid4())}
st.session_state.internal_loads_rerun_pending = True
# Delete button
delete_key = f"delete_equipment_{system['name']}_{idx}"
with cols[5].container():
if st.button("Delete", key=delete_key):
st.session_state.project_data["internal_loads"]["equipment"].pop(idx)
st.success(f"Equipment '{system['name']}' deleted!")
st.session_state.equipment_action = {"action": "delete", "id": str(uuid.uuid4())}
st.session_state.internal_loads_rerun_pending = True
def display_ventilation_table(ventilation_systems: List[Dict[str, Any]]):
"""Display ventilation systems in a table format with edit/delete buttons."""
# Create column headers
cols = st.columns([2, 1, 1, 1, 1, 1])
cols[0].write("**Name**")
cols[1].write("**Type**")
cols[2].write("**Area (m²)**")
cols[3].write("**Rate**")
cols[4].write("**Edit**")
cols[5].write("**Delete**")
# Display each system
for idx, system in enumerate(ventilation_systems):
cols = st.columns([2, 1, 1, 1, 1, 1])
cols[0].write(system["name"])
cols[1].write(system.get("system_type", "Unknown"))
cols[2].write(f"{system.get('area', 0):.1f}")
if system.get("system_type") in ["AirChanges/Hour", "Balanced Flow", "Heat Recovery"]:
rate_info = f"{system.get('design_flow_rate', 0):.2f} L/s·m²"
else:
rate_info = f"{system.get('opening_effectiveness', 0):.2f} %"
cols[3].write(rate_info)
# Edit button
edit_key = f"edit_ventilation_{system['name']}_{idx}"
with cols[4].container():
if st.button("Edit", key=edit_key):
st.session_state.ventilation_editor = {
"index": idx,
"name": system.get("name", ""),
"system_type": system.get("system_type", "AirChanges/Hour"),
"area": system.get("area", 100.0),
"schedule": system.get("schedule", "Continuous"),
"design_flow_rate": system.get("design_flow_rate", 1.0),
"ventilation_type": system.get("ventilation_type", "Natural"),
"fan_pressure_rise": system.get("fan_pressure_rise", 200.0),
"fan_efficiency": system.get("fan_efficiency", 0.7),
"opening_effectiveness": system.get("opening_effectiveness", 50.0),
"sensible_effectiveness": system.get("sensible_effectiveness", 0.8),
"latent_effectiveness": system.get("latent_effectiveness", 0.6),
"is_edit": True
}
st.session_state.ventilation_action = {"action": "edit", "id": str(uuid.uuid4())}
st.session_state.internal_loads_rerun_pending = True
# Delete button
delete_key = f"delete_ventilation_{system['name']}_{idx}"
with cols[5].container():
if st.button("Delete", key=delete_key):
st.session_state.project_data["internal_loads"]["ventilation"].pop(idx)
st.success(f"Ventilation System '{system['name']}' deleted!")
st.session_state.ventilation_action = {"action": "delete", "id": str(uuid.uuid4())}
st.session_state.internal_loads_rerun_pending = True
def display_infiltration_table(infiltration_systems: List[Dict[str, Any]]):
"""Display infiltration systems in a table format with edit/delete buttons."""
# Create column headers
cols = st.columns([2, 1, 1, 1, 1, 1])
cols[0].write("**Name**")
cols[1].write("**Type**")
cols[2].write("**Area (m²)**")
cols[3].write("**Rate**")
cols[4].write("**Edit**")
cols[5].write("**Delete**")
# Display each system
for idx, system in enumerate(infiltration_systems):
cols = st.columns([2, 1, 1, 1, 1, 1])
cols[0].write(system["name"])
cols[1].write(system.get("system_type", "Unknown"))
cols[2].write(f"{system.get('area', 0):.1f}")
if system.get("system_type") == "AirChanges/Hour":
rate_info = f"{system.get('design_flow_rate', 0):.2f} ACH"
elif system.get("system_type") == "Effective Leakage Area":
rate_info = f"{system.get('effective_air_leakage_area', 0):.2f} cm²"
else:
rate_info = f"{system.get('flow_coefficient', 0):.6f} m³/s·Paⁿ"
cols[3].write(rate_info)
# Edit button
edit_key = f"edit_infiltration_{system['name']}_{idx}"
with cols[4].container():
if st.button("Edit", key=edit_key):
st.session_state.infiltration_editor = {
"index": idx,
"name": system.get("name", ""),
"system_type": system.get("system_type", "AirChanges/Hour"),
"area": system.get("area", 100.0),
"schedule": system.get("schedule", "Continuous"),
"design_flow_rate": system.get("design_flow_rate", 0.3),
"effective_air_leakage_area": system.get("effective_air_leakage_area", 100.0),
"stack_coefficient": system.get("stack_coefficient", 0.0001),
"wind_coefficient": system.get("wind_coefficient", 0.0001),
"flow_coefficient": system.get("flow_coefficient", 0.0001),
"pressure_exponent": system.get("pressure_exponent", 0.6),
"is_edit": True
}
st.session_state.infiltration_action = {"action": "edit", "id": str(uuid.uuid4())}
st.session_state.internal_loads_rerun_pending = True
# Delete button
delete_key = f"delete_infiltration_{system['name']}_{idx}"
with cols[5].container():
if st.button("Delete", key=delete_key):
st.session_state.project_data["internal_loads"]["infiltration"].pop(idx)
st.success(f"Infiltration System '{system['name']}' deleted!")
st.session_state.infiltration_action = {"action": "delete", "id": str(uuid.uuid4())}
st.session_state.internal_loads_rerun_pending = True
def display_schedules_table(schedules: Dict[str, Any]):
"""Display schedules in a table format with edit/delete buttons."""
if not schedules:
return
# Create table data
table_data = []
for name, schedule in schedules.items():
weekday_peak = max(schedule.get("weekday", [0]))
weekend_peak = max(schedule.get("weekend", [0]))
table_data.append({
"Name": name,
"Description": schedule.get("description", "No description"),
"Weekday Peak": f"{weekday_peak:.2f}",
"Weekend Peak": f"{weekend_peak:.2f}",
"Actions": name
})
if table_data:
df = pd.DataFrame(table_data)
# Display table
st.dataframe(df.drop('Actions', axis=1), use_container_width=True)
# Display action buttons
st.write("**Actions:**")
for i, row in enumerate(table_data):
schedule_name = row["Actions"]
col1, col2, col3, col4 = st.columns([2, 1, 1, 1])
with col1:
st.write(f"{i+1}. {schedule_name}")
with col2:
if st.button("View", key=f"view_schedule_{schedule_name}_{i}"):
schedule_data = schedules[schedule_name]
display_schedule_chart(schedule_name, schedule_data)
with col3:
if st.button("Edit", key=f"edit_schedule_{schedule_name}_{i}"):
schedule_data = schedules[schedule_name]
st.session_state.schedule_editor = {
"is_edit": True,
"original_name": schedule_name,
"name": schedule_name,
"description": schedule_data.get("description", ""),
"weekday": schedule_data.get("weekday", [0.0] * 24),
"weekend": schedule_data.get("weekend", [0.0] * 24)
}
st.session_state.schedule_action = {"action": "edit", "id": str(uuid.uuid4())}
st.session_state.internal_loads_rerun_pending = True
with col4:
if schedule_name not in DEFAULT_SCHEDULE_TEMPLATES:
if is_schedule_in_use(schedule_name):
st.write("(In Use)")
else:
if st.button("Delete", key=f"delete_schedule_{schedule_name}_{i}"):
del schedules[schedule_name]
st.success(f"Schedule '{schedule_name}' deleted!")
st.session_state.schedule_action = {"action": "delete", "id": str(uuid.uuid4())}
st.session_state.internal_loads_rerun_pending = True
else:
st.write("(Default)")