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