Spaces:
Sleeping
Sleeping
| """ | |
| 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)") |