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