""" Data validation module for HVAC Load Calculator. This module provides validation functions for user inputs. """ import streamlit as st import pandas as pd import numpy as np from typing import Dict, List, Any, Optional, Tuple, Callable import json import os class DataValidation: """Class for data validation functionality.""" @staticmethod def validate_building_info(building_info: Dict[str, Any]) -> Tuple[bool, List[str]]: """ Validate building information inputs. Args: building_info: Dictionary with building information Returns: Tuple containing validation result (True if valid) and list of validation messages """ is_valid = True messages = [] # Check required fields required_fields = [ ("project_name", "Project Name"), ("building_name", "Building Name"), ("location", "Location"), ("climate_zone", "Climate Zone"), ("building_type", "Building Type") ] for field, display_name in required_fields: if field not in building_info or not building_info[field]: is_valid = False messages.append(f"{display_name} is required.") # Check numeric fields numeric_fields = [ ("floor_area", "Floor Area", 0, None), ("num_floors", "Number of Floors", 1, None), ("floor_height", "Floor Height", 2.0, 10.0), ("occupancy", "Occupancy", 0, None) ] for field, display_name, min_val, max_val in numeric_fields: if field in building_info: try: value = float(building_info[field]) if min_val is not None and value < min_val: is_valid = False messages.append(f"{display_name} must be at least {min_val}.") if max_val is not None and value > max_val: is_valid = False messages.append(f"{display_name} must be at most {max_val}.") except (ValueError, TypeError): is_valid = False messages.append(f"{display_name} must be a number.") # Check design conditions if "design_conditions" in building_info: design_conditions = building_info["design_conditions"] # Check summer conditions summer_fields = [ ("summer_outdoor_db", "Summer Outdoor Dry-Bulb", -10.0, 50.0), ("summer_outdoor_wb", "Summer Outdoor Wet-Bulb", -10.0, 40.0), ("summer_indoor_db", "Summer Indoor Dry-Bulb", 18.0, 30.0), ("summer_indoor_rh", "Summer Indoor RH", 30.0, 70.0) ] for field, display_name, min_val, max_val in summer_fields: if field in design_conditions: try: value = float(design_conditions[field]) if min_val is not None and value < min_val: is_valid = False messages.append(f"{display_name} must be at least {min_val}.") if max_val is not None and value > max_val: is_valid = False messages.append(f"{display_name} must be at most {max_val}.") except (ValueError, TypeError): is_valid = False messages.append(f"{display_name} must be a number.") # Check winter conditions winter_fields = [ ("winter_outdoor_db", "Winter Outdoor Dry-Bulb", -40.0, 20.0), ("winter_outdoor_rh", "Winter Outdoor RH", 0.0, 100.0), ("winter_indoor_db", "Winter Indoor Dry-Bulb", 18.0, 25.0), ("winter_indoor_rh", "Winter Indoor RH", 20.0, 60.0) ] for field, display_name, min_val, max_val in winter_fields: if field in design_conditions: try: value = float(design_conditions[field]) if min_val is not None and value < min_val: is_valid = False messages.append(f"{display_name} must be at least {min_val}.") if max_val is not None and value > max_val: is_valid = False messages.append(f"{display_name} must be at most {max_val}.") except (ValueError, TypeError): is_valid = False messages.append(f"{display_name} must be a number.") # Check that wet-bulb is less than dry-bulb if "summer_outdoor_db" in design_conditions and "summer_outdoor_wb" in design_conditions: try: db = float(design_conditions["summer_outdoor_db"]) wb = float(design_conditions["summer_outdoor_wb"]) if wb > db: is_valid = False messages.append("Summer Outdoor Wet-Bulb temperature must be less than or equal to Dry-Bulb temperature.") except (ValueError, TypeError): pass # Already handled above return is_valid, messages @staticmethod def validate_components(components: Dict[str, List[Any]]) -> Tuple[bool, List[str]]: """ Validate building components. Args: components: Dictionary with building components Returns: Tuple containing validation result (True if valid) and list of validation messages """ is_valid = True messages = [] # Check if any components exist if not any(components.values()): is_valid = False messages.append("At least one building component (wall, roof, floor, window, or door) is required.") # Check wall components for i, wall in enumerate(components.get("walls", [])): # Check required fields if not wall.name: is_valid = False messages.append(f"Wall #{i+1}: Name is required.") # Check numeric fields if wall.area <= 0: is_valid = False messages.append(f"Wall #{i+1}: Area must be greater than zero.") if wall.u_value <= 0: is_valid = False messages.append(f"Wall #{i+1}: U-value must be greater than zero.") # Check roof components for i, roof in enumerate(components.get("roofs", [])): # Check required fields if not roof.name: is_valid = False messages.append(f"Roof #{i+1}: Name is required.") # Check numeric fields if roof.area <= 0: is_valid = False messages.append(f"Roof #{i+1}: Area must be greater than zero.") if roof.u_value <= 0: is_valid = False messages.append(f"Roof #{i+1}: U-value must be greater than zero.") # Check floor components for i, floor in enumerate(components.get("floors", [])): # Check required fields if not floor.name: is_valid = False messages.append(f"Floor #{i+1}: Name is required.") # Check numeric fields if floor.area <= 0: is_valid = False messages.append(f"Floor #{i+1}: Area must be greater than zero.") if floor.u_value <= 0: is_valid = False messages.append(f"Floor #{i+1}: U-value must be greater than zero.") # Check window components for i, window in enumerate(components.get("windows", [])): # Check required fields if not window.name: is_valid = False messages.append(f"Window #{i+1}: Name is required.") # Check numeric fields if window.area <= 0: is_valid = False messages.append(f"Window #{i+1}: Area must be greater than zero.") if window.u_value <= 0: is_valid = False messages.append(f"Window #{i+1}: U-value must be greater than zero.") if window.shgc <= 0 or window.shgc > 1: is_valid = False messages.append(f"Window #{i+1}: SHGC must be between 0 and 1.") # Check door components for i, door in enumerate(components.get("doors", [])): # Check required fields if not door.name: is_valid = False messages.append(f"Door #{i+1}: Name is required.") # Check numeric fields if door.area <= 0: is_valid = False messages.append(f"Door #{i+1}: Area must be greater than zero.") if door.u_value <= 0: is_valid = False messages.append(f"Door #{i+1}: U-value must be greater than zero.") # Check for minimum requirements if not components.get("walls", []): messages.append("Warning: No walls defined. At least one wall is recommended.") if not components.get("roofs", []): messages.append("Warning: No roofs defined. At least one roof is recommended.") if not components.get("floors", []): messages.append("Warning: No floors defined. At least one floor is recommended.") return is_valid, messages @staticmethod def validate_internal_loads(internal_loads: Dict[str, Any]) -> Tuple[bool, List[str]]: """ Validate internal loads inputs. Args: internal_loads: Dictionary with internal loads information Returns: Tuple containing validation result (True if valid) and list of validation messages """ is_valid = True messages = [] # Check people loads people = internal_loads.get("people", []) for i, person in enumerate(people): # Check required fields if not person.get("name"): is_valid = False messages.append(f"People Load #{i+1}: Name is required.") # Check numeric fields if person.get("num_people", 0) < 0: is_valid = False messages.append(f"People Load #{i+1}: Number of people must be non-negative.") if person.get("hours_in_operation", 0) <= 0: is_valid = False messages.append(f"People Load #{i+1}: Hours in operation must be positive.") # Check lighting loads lighting = internal_loads.get("lighting", []) for i, light in enumerate(lighting): # Check required fields if not light.get("name"): is_valid = False messages.append(f"Lighting Load #{i+1}: Name is required.") # Check numeric fields if light.get("power", 0) < 0: is_valid = False messages.append(f"Lighting Load #{i+1}: Power must be non-negative.") if light.get("usage_factor", 0) < 0 or light.get("usage_factor", 0) > 1: is_valid = False messages.append(f"Lighting Load #{i+1}: Usage factor must be between 0 and 1.") if light.get("hours_in_operation", 0) <= 0: is_valid = False messages.append(f"Lighting Load #{i+1}: Hours in operation must be positive.") # Check equipment loads equipment = internal_loads.get("equipment", []) for i, equip in enumerate(equipment): # Check required fields if not equip.get("name"): is_valid = False messages.append(f"Equipment Load #{i+1}: Name is required.") # Check numeric fields if equip.get("power", 0) < 0: is_valid = False messages.append(f"Equipment Load #{i+1}: Power must be non-negative.") if equip.get("usage_factor", 0) < 0 or equip.get("usage_factor", 0) > 1: is_valid = False messages.append(f"Equipment Load #{i+1}: Usage factor must be between 0 and 1.") if equip.get("radiation_fraction", 0) < 0 or equip.get("radiation_fraction", 0) > 1: is_valid = False messages.append(f"Equipment Load #{i+1}: Radiation fraction must be between 0 and 1.") if equip.get("hours_in_operation", 0) <= 0: is_valid = False messages.append(f"Equipment Load #{i+1}: Hours in operation must be positive.") return is_valid, messages @staticmethod def validate_calculation_settings(settings: Dict[str, Any]) -> Tuple[bool, List[str]]: """ Validate calculation settings. Args: settings: Dictionary with calculation settings Returns: Tuple containing validation result (True if valid) and list of validation messages """ is_valid = True messages = [] # Check infiltration rate if "infiltration_rate" in settings: try: infiltration_rate = float(settings["infiltration_rate"]) if infiltration_rate < 0: is_valid = False messages.append("Infiltration rate must be non-negative.") except (ValueError, TypeError): is_valid = False messages.append("Infiltration rate must be a number.") # Check ventilation rate if "ventilation_rate" in settings: try: ventilation_rate = float(settings["ventilation_rate"]) if ventilation_rate < 0: is_valid = False messages.append("Ventilation rate must be non-negative.") except (ValueError, TypeError): is_valid = False messages.append("Ventilation rate must be a number.") # Check safety factors safety_factors = ["cooling_safety_factor", "heating_safety_factor"] for factor in safety_factors: if factor in settings: try: value = float(settings[factor]) if value < 0: is_valid = False messages.append(f"{factor.replace('_', ' ').title()} must be non-negative.") except (ValueError, TypeError): is_valid = False messages.append(f"{factor.replace('_', ' ').title()} must be a number.") return is_valid, messages @staticmethod def display_validation_messages(messages: List[str], container=None) -> None: """ Display validation messages in Streamlit. Args: messages: List of validation messages container: Optional Streamlit container to display messages in """ if not messages: return # Separate errors and warnings errors = [msg for msg in messages if not msg.startswith("Warning:")] warnings = [msg for msg in messages if msg.startswith("Warning:")] # Use provided container or st directly display = container if container is not None else st # Display errors if errors: error_msg = "Please fix the following errors:\n" + "\n".join([f"- {msg}" for msg in errors]) display.error(error_msg) # Display warnings if warnings: warning_msg = "Warnings:\n" + "\n".join([f"- {msg[8:]}" for msg in warnings]) display.warning(warning_msg) @staticmethod def validate_and_proceed( session_state: Dict[str, Any], validation_function: Callable[[Dict[str, Any]], Tuple[bool, List[str]]], data_key: str, success_message: str = "Validation successful!", proceed_callback: Optional[Callable] = None ) -> bool: """ Validate data and proceed if valid. Args: session_state: Streamlit session state validation_function: Function to validate data data_key: Key for data in session state success_message: Message to display on success proceed_callback: Optional callback function to execute if validation succeeds Returns: Boolean indicating whether validation succeeded """ if data_key not in session_state: st.error(f"No {data_key.replace('_', ' ').title()} data found.") return False # Validate data is_valid, messages = validation_function(session_state[data_key]) # Display validation messages DataValidation.display_validation_messages(messages) # Proceed if valid if is_valid: st.success(success_message) # Execute callback if provided if proceed_callback is not None: proceed_callback() return True return False # Create a singleton instance data_validation = DataValidation() # Example usage if __name__ == "__main__": import streamlit as st # Initialize session state with dummy data for testing if "building_info" not in st.session_state: st.session_state["building_info"] = { "project_name": "Test Project", "building_name": "Test Building", "location": "New York", "climate_zone": "4A", "building_type": "Office", "floor_area": 1000.0, "num_floors": 2, "floor_height": 3.0, "orientation": "NORTH", "occupancy": 50, "operating_hours": "8:00-18:00", "design_conditions": { "summer_outdoor_db": 35.0, "summer_outdoor_wb": 25.0, "summer_indoor_db": 24.0, "summer_indoor_rh": 50.0, "winter_outdoor_db": -5.0, "winter_outdoor_rh": 80.0, "winter_indoor_db": 21.0, "winter_indoor_rh": 40.0 } } # Test validation st.header("Test Building Information Validation") # Add some invalid data for testing if st.button("Make Data Invalid"): st.session_state["building_info"]["floor_area"] = -100.0 st.session_state["building_info"]["design_conditions"]["summer_outdoor_wb"] = 40.0 # Validate building info if st.button("Validate Building Info"): data_validation.validate_and_proceed( st.session_state, data_validation.validate_building_info, "building_info", "Building information is valid!" )